Major usability improvements. Add config, DEB make, systemd unit, arg and flags parser.

This commit is contained in:
Alexandr Bogomyakov
2020-04-26 03:17:03 +03:00
parent 9d8c079a36
commit 416d50fd89
8 changed files with 217 additions and 75 deletions

View File

@ -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

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
/target
mus-fuse.yml
Cargo.lock
/mnt
.vscode/

View File

@ -1,8 +1,13 @@
[package]
name = "mus-fuse"
version = "0.6.0"
version = "0.7.1"
authors = ["AB <ultradesu@hexor.ru>"]
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 <ultradesu@hexor.ru>"
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"],
]

12
assets/conf.yml Normal file
View File

@ -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

View File

@ -3,11 +3,10 @@ Description=Mount mus-fuse
[Service]
Type=simple
User=mus-fuse
RestartSec=5
Restart=always
Environment=HTTP_USER=<USER>
Environment=HTTP_PASS=<PASS>
ExecStart=/usr/local/bin/mus-fuse --mountpoint <PATH> --server <ADDRESS>
ExecStart=/usr/bin/mus-fuse --config /etc/mus-fuse.yml
KillSignal=SIGINT
[Install]

3
assets/postinst Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
systemctl daemon-reload

10
assets/preinst Executable file
View File

@ -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

View File

@ -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<u64, Vec<u8>>,
buffer_length: BTreeMap<String, i64>,
metrics_inode: u64,
cache_head: u64,
cache_max_count: u64,
}
#[cfg(target_family = "unix")]
impl JsonFilesystem {
fn new(tree: &Vec<Track>, server: String) -> JsonFilesystem {
fn new(
tree: &Vec<Track>,
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::<u64>().unwrap(),
None => 1024 * cache_head_cfg.parse::<u64>().unwrap(),
},
Err(_) => match cli_args.value_of("cache_head") {
Some(cache_head_opt) => 1024 * cache_head_opt.parse::<u64>().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::<u64>().unwrap(),
None => cache_max_count_cfg.parse::<u64>().unwrap(),
},
Err(_) => match cli_args.value_of("cache_max_count") {
Some(cache_max_count_opt) => cache_max_count_opt.parse::<u64>().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::<Vec<&OsStr>>();
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");