mirror of
https://github.com/house-of-vanity/mus-fuse.git
synced 2025-07-06 21:24:09 +00:00
Major usability improvements. Add config, DEB make, systemd unit, arg and flags parser.
This commit is contained in:
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@ -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
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
/target
|
||||
mus-fuse.yml
|
||||
Cargo.lock
|
||||
/mnt
|
||||
.vscode/
|
||||
|
43
Cargo.toml
43
Cargo.toml
@ -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
12
assets/conf.yml
Normal 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
|
@ -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
3
assets/postinst
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
systemctl daemon-reload
|
10
assets/preinst
Executable file
10
assets/preinst
Executable 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
|
||||
|
196
src/main.rs
196
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<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");
|
||||
|
Reference in New Issue
Block a user