From 416d50fd89c23d9c54b05ccb4d5c328dc90a1135 Mon Sep 17 00:00:00 2001 From: Alexandr Bogomyakov Date: Sun, 26 Apr 2020 03:17:03 +0300 Subject: [PATCH] Major usability improvements. Add config, DEB make, systemd unit, arg and flags parser. --- .github/workflows/build.yml | 22 ++- .gitignore | 1 + Cargo.toml | 43 ++++- assets/conf.yml | 12 ++ mus-fuse.service => assets/mus-fuse.service | 5 +- assets/postinst | 3 + assets/preinst | 10 + src/main.rs | 196 ++++++++++++++------ 8 files changed, 217 insertions(+), 75 deletions(-) create mode 100644 assets/conf.yml rename mus-fuse.service => assets/mus-fuse.service (51%) create mode 100755 assets/postinst create mode 100755 assets/preinst diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45f7db8..b04a6c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,10 +13,12 @@ jobs: - name: Checkout code uses: actions/checkout@v2 - name: Pre-build - run: sudo apt install -y libfuse-dev pkg-config + run: sudo apt install -y libfuse-dev pkg-config && cargo install cargo-deb - uses: actions/checkout@v2 - - name: Build + - name: Build binary run: cargo build --verbose --release + - name: Build deb + run: cargo deb - name: Create Release id: create_release uses: actions/create-release@v1 @@ -27,13 +29,23 @@ jobs: release_name: Release ${{ github.ref }} draft: false prerelease: false - - name: Upload Release Asset - id: upload-release-asset + - name: Upload Release Bin + id: upload-release-asset-bin uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps asset_path: ./target/release/mus-fuse - asset_name: mus-fuse + asset_name: mus-fuse-${{ github.ref }} + asset_content_type: application/x-pie-executable + - name: Upload Release Deb + id: upload-release-asset-deb + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./target/debian/mus-fuse_${{ github.ref }}_amd64.deb + asset_name: mus-fuse_${{ github.ref }}_amd64.deb asset_content_type: application/x-pie-executable diff --git a/.gitignore b/.gitignore index 8bbed59..0eee196 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +mus-fuse.yml Cargo.lock /mnt .vscode/ diff --git a/Cargo.toml b/Cargo.toml index 0d6308f..28fad2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "mus-fuse" -version = "0.6.0" +version = "0.7.1" authors = ["AB "] edition = "2018" +license = "WTFPL" +readme = "README.md" +description = """\ +Written safely in Rust, FUSE FS with your own private music library \ +hosted on your server securely.""" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -11,13 +16,31 @@ tokio = { version = "0.2", features = ["full"] } serde = { version = "1.0", features = ["derive"] } clap = {version = "2.33", features = ["yaml"]} serde_json = "1.0" -percent-encoding = "2.1.0" -fuse = "0.3.1" -time = "0.1.42" -libc = "0.2.69" -chrono = "0.4.11" -env_logger = "0.7.1" +percent-encoding = "2.1" +fuse = "0.3" +time = "0.1" +libc = "0.2" +chrono = "0.4" +env_logger = "0.7" log = { version = "^0.4.5", features = ["std"] } -size_format = "1.0.2" -base64 = "0.12.0" -ctrlc = "3.1.4" +size_format = "1.0" +base64 = "0.12" +ctrlc = "3.1" +config = "0.9" + +[package.metadata.deb] +maintainer = "AB " +license-file = ["LICENSE", "0"] +depends = "$auto, systemd, openssl, fuse" +extended-description = """\ +Written safely in Rust, FUSE FS with your own private music library \ +hosted on your server securely.""" +section = "utilities" +priority = "optional" +maintainer-scripts = "assets/" +conf-files = ["etc/mus-fuse.yml"] +assets = [ + ["assets/mus-fuse.service", "/usr/lib/systemd/system/mus-fuse.service", "644"], + ["target/release/mus-fuse", "usr/bin/", "755"], + ["assets/conf.yml", "etc/mus-fuse.yml", "644"], +] diff --git a/assets/conf.yml b/assets/conf.yml new file mode 100644 index 0000000..c96e861 --- /dev/null +++ b/assets/conf.yml @@ -0,0 +1,12 @@ +--- +server: https://mus.test.com +mountpoint: /srv/mus-fuse +http_user: username +http_pass: passwd1337 + +# How many KiB of file beginnings download and store in RAM. +# It's speeding up any metadata operations and media library scanning. +cache_head: 768 + +# How many count of `cache_head` store. +cache_max_count: 10 diff --git a/mus-fuse.service b/assets/mus-fuse.service similarity index 51% rename from mus-fuse.service rename to assets/mus-fuse.service index 7f539c3..03c1ba3 100644 --- a/mus-fuse.service +++ b/assets/mus-fuse.service @@ -3,11 +3,10 @@ Description=Mount mus-fuse [Service] Type=simple +User=mus-fuse RestartSec=5 Restart=always -Environment=HTTP_USER= -Environment=HTTP_PASS= -ExecStart=/usr/local/bin/mus-fuse --mountpoint --server
+ExecStart=/usr/bin/mus-fuse --config /etc/mus-fuse.yml KillSignal=SIGINT [Install] diff --git a/assets/postinst b/assets/postinst new file mode 100755 index 0000000..ab20f7b --- /dev/null +++ b/assets/postinst @@ -0,0 +1,3 @@ +#!/bin/bash + +systemctl daemon-reload diff --git a/assets/preinst b/assets/preinst new file mode 100755 index 0000000..2bec704 --- /dev/null +++ b/assets/preinst @@ -0,0 +1,10 @@ +#!/bin/bash + +if systemctl | grep -Fq 'mus-fuse'; then + sudo systemctl stop mus-fuse.service +fi + +adduser --quiet --system --group --no-create-home --home /run/mus-fuse mus-fuse +mkdir -p /srv/mus-fuse +chown mus-fuse:mus-fuse /srv/mus-fuse + diff --git a/src/main.rs b/src/main.rs index bad37c0..04fc909 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ extern crate time; #[macro_use] extern crate log; extern crate chrono; +extern crate config; use clap::{App, Arg}; use env_logger::Env; @@ -29,9 +30,7 @@ use std::{ }; use time::Timespec; -const CACHE_HEAD: i64 = 768 * 1024; // bytes from beginning of each file cached -const MAX_CACHE_SIZE: i64 = 10; // Count of files cached -static mut HTTP_AUTH: String = String::new(); +static mut HTTP_AUTH: String = String::new(); // Basic Auth string. struct Metrics { http_requests: u64, @@ -126,11 +125,18 @@ struct JsonFilesystem { buffer_head_data: HashMap>, buffer_length: BTreeMap, metrics_inode: u64, + cache_head: u64, + cache_max_count: u64, } #[cfg(target_family = "unix")] impl JsonFilesystem { - fn new(tree: &Vec, server: String) -> JsonFilesystem { + fn new( + tree: &Vec, + server: String, + cache_max_count: u64, + cache_head: u64, + ) -> JsonFilesystem { let mut attrs = BTreeMap::new(); let mut inodes = BTreeMap::new(); let ts = time::now().to_timespec(); @@ -215,6 +221,8 @@ impl JsonFilesystem { buffer_head_index: HashSet::new(), buffer_length: BTreeMap::new(), metrics_inode: metrics_inode, + cache_head: cache_head, + cache_max_count: cache_max_count, } } } @@ -270,7 +278,7 @@ impl Filesystem for JsonFilesystem { } // cleaning cache - if self.buffer_head_index.len() > MAX_CACHE_SIZE as usize { + if self.buffer_head_index.len() > self.cache_max_count as usize { let mut iter = self.buffer_head_index.iter().filter(|&x| *x != ino); let old_entry = iter.next().unwrap(); self.buffer_head_data.remove(old_entry); @@ -351,7 +359,7 @@ impl Filesystem for JsonFilesystem { let range = format!("bytes={}-{}", offset, end_of_chunk - 1); // if it's beginning of file... - if end_of_chunk < CACHE_HEAD { + if end_of_chunk < self.cache_head as i64 { // looking for CACHE_HEAD bytes file beginning in cache if self.buffer_head_data.contains_key(&ino) { // Cache found @@ -378,10 +386,10 @@ impl Filesystem for JsonFilesystem { "Range", format!( "bytes=0-{}", - if CACHE_HEAD > content_length { + if self.cache_head as i64 > content_length { content_length - 1 } else { - CACHE_HEAD - 1 + self.cache_head as i64 - 1 } ), ) @@ -485,7 +493,7 @@ impl Filesystem for JsonFilesystem { fn main() { env_logger::from_env(Env::default().default_filter_or("info")).init(); - info!("Logger initialized. Set RUST_LOG=[debug,error,info,warn,trace] Default: info"); + // Parse opts and args let cli_args = App::new("mus-fuse") .version(env!("CARGO_PKG_VERSION")) .author(env!("CARGO_PKG_AUTHORS")) @@ -494,58 +502,131 @@ fn main() { Arg::with_name("server") .short("s") .long("server") - .value_name("SERVER") - .help("Sets a server hosting your library") - .required(true) + .value_name("ADDRESS") + .help("Sets a server hosting your library with schema. (https or http)") + .required(false) .takes_value(true), ) .arg( Arg::with_name("mountpoint") .short("m") .long("mountpoint") - .value_name("MOUNT POINT") - .help("Mount point for library.") - .required(true) + .value_name("PATH") + .help("Mount point for library") + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name("conf") + .short("c") + .long("config") + .value_name("PATH") + .help("Config file to use") + .default_value("/etc/mus-fuse.yaml") + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name("cache_max_count") + .long("cache-max") + .value_name("COUNT") + .help("How many files store in cache. [default: 10]") + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name("cache_head") + .long("cache-head") + .value_name("KiB") + .help("How many KiB cache in file beginning for speeding up metadata requests. [default: 768]") + .required(false) .takes_value(true), ) .get_matches(); - let server = cli_args.value_of("server").unwrap().to_string(); - let mountpoint = cli_args.value_of("mountpoint").unwrap().to_string(); + info!("Logger initialized. Set RUST_LOG=[debug,error,info,warn,trace] Default: info"); + info!("Mus-Fuse {}", env!("CARGO_PKG_VERSION")); + + // Read config file and env vars + let conf = cli_args.value_of("conf").unwrap(); + let mut settings = config::Config::default(); + settings = match settings.merge(config::File::with_name(conf)) { + Ok(conf_content) => { + info!("Using config file {}", conf); + conf_content.to_owned() + } + Err(e) => { + warn!("Can't read config file {}", e); + config::Config::default() + } + }; + settings = match settings.merge(config::Environment::with_prefix("MUS")) { + Ok(conf) => conf.to_owned(), + Err(_) => config::Config::default(), + }; + let http_user = match settings.get_str("http_user") { + Ok(u) => u, + Err(_) => { + info!("User for basic auth is not defined."); + String::new() + } + }; + let http_pass = match settings.get_str("http_pass") { + Ok(u) => u, + Err(_) => { + info!("User for basic auth is not defined."); + String::new() + } + }; + let server = match settings.get_str("server") { + Ok(server_cfg) => match cli_args.value_of("server") { + Some(server_opt) => server_opt.to_string(), + None => server_cfg, + }, + Err(_) => match cli_args.value_of("server") { + Some(server_opt) => server_opt.to_string(), + None => { + error!("Server is not set in config nor via run options."); + process::exit(0x0001) + } + }, + }; + let mountpoint = match settings.get_str("mountpoint") { + Ok(mountpoint_cfg) => match cli_args.value_of("mountpoint") { + Some(mountpoint_opt) => mountpoint_opt.to_string(), + None => mountpoint_cfg, + }, + Err(_) => match cli_args.value_of("mountpoint") { + Some(mountpoint_opt) => mountpoint_opt.to_string(), + None => { + error!("Mount point is not set in config nor via run options."); + process::exit(0x0001) + } + }, + }; + let cache_head = match settings.get_str("cache_head") { + Ok(cache_head_cfg) => match cli_args.value_of("cache_head") { + Some(cache_head_opt) => 1024 * cache_head_opt.parse::().unwrap(), + None => 1024 * cache_head_cfg.parse::().unwrap(), + }, + Err(_) => match cli_args.value_of("cache_head") { + Some(cache_head_opt) => 1024 * cache_head_opt.parse::().unwrap(), + None => 768 * 1024, + }, + }; + let cache_max_count = match settings.get_str("cache_max_count") { + Ok(cache_max_count_cfg) => match cli_args.value_of("cache_max_count") { + Some(cache_max_count_opt) => cache_max_count_opt.parse::().unwrap(), + None => cache_max_count_cfg.parse::().unwrap(), + }, + Err(_) => match cli_args.value_of("cache_max_count") { + Some(cache_max_count_opt) => cache_max_count_opt.parse::().unwrap(), + None => 10, + }, + }; unsafe { METRICS.server_addr = server.clone(); - } - let http_user_var = "HTTP_USER"; - let http_pass_var = "HTTP_PASS"; - - let http_user = match env::var_os(http_user_var) { - Some(val) => { - info!( - "Variable {} is set. Will be used for http auth as user.", - http_user_var - ); - val.to_str().unwrap().to_string() - } - None => { - info!("{} is not defined in the environment.", http_user_var); - "".to_string() - } - }; - let http_pass = match env::var_os(http_pass_var) { - Some(val) => { - info!( - "Variable {} is set. Will be used for http auth as password.", - http_pass_var - ); - val.to_str().unwrap().to_string() - } - None => { - info!("{} is not defined in the environment.", http_pass_var); - "".to_string() - } - }; - unsafe { let mut buf = String::new(); buf.push_str(&http_user); buf.push_str(":"); @@ -555,16 +636,15 @@ fn main() { let lib = match get_tracks(&server) { Ok(library) => library, Err(err) => { - error!("Can't fetch library from remote server. Probably server not running or auth failed."); + error!("Can't fetch library from remote server. Probably server is not running or auth failed. {}", err); error!( - "Provide Basic Auth credentials by setting envs {} and {}", - http_user_var, http_pass_var + "Provide Basic Auth credentials by setting envs MUS_HTTP_USER and MUS_HTTP_PASS or providing config.", ); - panic!("Error: {}", err); + process::exit(0x0001) } }; info!("Remote library host: {}", &server); - let fs = JsonFilesystem::new(&lib, server); + let fs = JsonFilesystem::new(&lib, server, cache_max_count, cache_head); let options = [ "-o", "ro", @@ -574,17 +654,19 @@ fn main() { "sync_read", "-o", "auto_unmount", + "-o", + "allow_other", ] .iter() .map(|o| o.as_ref()) .collect::>(); info!( - "Caching {}B bytes in head of files.", - SizeFormatterBinary::new(CACHE_HEAD as u64) + "Caching {}B in head of files.", + SizeFormatterBinary::new(cache_head as u64) ); - info!("Max cache is {} files.", MAX_CACHE_SIZE); - info!("Mount options: {:?}", options); + info!("Max cache is {} files.", cache_max_count); + info!("Fuse mount options: {:?}", options); let _mount: fuse::BackgroundSession; unsafe { _mount = fuse::spawn_mount(fs, &mountpoint, &options).expect("Couldn't mount filesystem");