From 3873f4794af3f301bf371bbc6b8ad9aa003e6d5e Mon Sep 17 00:00:00 2001 From: sigoden Date: Thu, 14 Dec 2023 18:59:28 +0800 Subject: [PATCH] feat: add `--compress` option (#319) --- Cargo.lock | 49 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- README.md | 3 +++ src/args.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++----- src/server.rs | 15 +++++++++-- tests/http.rs | 7 +++++- 6 files changed, 135 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50f94e4..4b77a44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,11 +137,13 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" dependencies = [ + "bzip2", "flate2", "futures-core", "futures-io", "memchr", "pin-project-lite", + "xz2", ] [[package]] @@ -271,6 +273,27 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cc" version = "1.0.83" @@ -1016,6 +1039,17 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1161,6 +1195,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + [[package]] name = "port_check" version = "0.1.5" @@ -2204,3 +2244,12 @@ name = "xml-rs" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] diff --git a/Cargo.toml b/Cargo.toml index e2cd3b0..93e7547 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ percent-encoding = "2.3" serde = { version = "1", features = ["derive"] } serde_json = "1" futures = "0.3" -async_zip = { version = "0.0.15", default-features = false, features = ["deflate", "chrono", "tokio"] } +async_zip = { version = "0.0.15", default-features = false, features = ["deflate", "bzip2", "xz", "chrono", "tokio"] } headers = "0.3" mime_guess = "2.0" if-addrs = "0.10.1" diff --git a/README.md b/README.md index a3c5fe6..2575367 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Options: --render-spa Serve SPA(Single Page Application) --assets Set the path to the assets directory for overriding the built-in assets --log-format Customize http log format + --compress Set zip compress level [default: low] [possible values: none, low, medium, high] --completions Print shell completion script for [possible values: bash, elvish, fish, powershell, zsh] --tls-cert Path to an SSL/TLS certificate to serve with HTTPS --tls-key Path to the SSL/TLS certificate's private key @@ -326,6 +327,7 @@ All options can be set using environment variables prefixed with `DUFS_`. --render-spa DUFS_RENDER_SPA=true --assets DUFS_ASSETS=/assets --log-format DUFS_LOG_FORMAT="" + --compress DUFS_COMPRESS="low" --tls-cert DUFS_TLS_CERT=cert.pem --tls-key DUFS_TLS_KEY=key.pem ``` @@ -361,6 +363,7 @@ render-try-index: true render-spa: true assets: ./assets/ log-format: '$remote_addr "$request" $status $http_user_agent' +compress: low tls-cert: tests/data/cert.pem tls-key: tests/data/key_pkcs1.pem ``` diff --git a/src/args.rs b/src/args.rs index c95941c..f2f80ff 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,6 +1,7 @@ use anyhow::{bail, Context, Result}; -use clap::builder::PossibleValuesParser; -use clap::{value_parser, Arg, ArgAction, ArgMatches, Command}; +use async_zip::Compression; +use clap::builder::{PossibleValue, PossibleValuesParser}; +use clap::{value_parser, Arg, ArgAction, ArgMatches, Command, ValueEnum}; use clap_complete::{generate, Generator, Shell}; use serde::{Deserialize, Deserializer}; use smart_default::SmartDefault; @@ -196,6 +197,15 @@ pub fn build_cli() -> Command { .value_name("format") .help("Customize http log format"), ) + .arg( + Arg::new("compress") + .env("DUFS_COMPRESS") + .hide_env(true) + .value_parser(clap::builder::EnumValueParser::::new()) + .long("compress") + .value_name("level") + .help("Set zip compress level [default: low]") + ) .arg( Arg::new("completions") .long("completions") @@ -270,6 +280,7 @@ pub struct Args { #[serde(deserialize_with = "deserialize_log_http")] #[serde(rename = "log-format")] pub http_logger: HttpLogger, + pub compress: Compress, pub tls_cert: Option, pub tls_key: Option, } @@ -369,10 +380,6 @@ impl Args { args.render_spa = matches.get_flag("render-spa"); } - if let Some(log_format) = matches.get_one::("log-format") { - args.http_logger = log_format.parse()?; - } - if let Some(assets_path) = matches.get_one::("assets") { args.assets = Some(assets_path.clone()); } @@ -381,6 +388,14 @@ impl Args { args.assets = Some(Args::sanitize_assets_path(assets_path)?); } + if let Some(log_format) = matches.get_one::("log-format") { + args.http_logger = log_format.parse()?; + } + + if let Some(compress) = matches.get_one::("compress") { + args.compress = *compress; + } + #[cfg(feature = "tls")] { if let Some(tls_cert) = matches.get_one::("tls-cert") { @@ -403,6 +418,7 @@ impl Args { args.tls_cert = None; args.tls_key = None; } + println!("{args:?}"); Ok(args) } @@ -461,6 +477,47 @@ impl BindAddr { } } +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Compress { + None, + Low, + Medium, + High, +} + +impl Default for Compress { + fn default() -> Self { + Self::Low + } +} + +impl ValueEnum for Compress { + fn value_variants<'a>() -> &'a [Self] { + &[Self::None, Self::Low, Self::Medium, Self::High] + } + + fn to_possible_value(&self) -> Option { + Some(match self { + Compress::None => PossibleValue::new("none"), + Compress::Low => PossibleValue::new("low"), + Compress::Medium => PossibleValue::new("medium"), + Compress::High => PossibleValue::new("high"), + }) + } +} + +impl Compress { + pub fn to_compression(self) -> Compression { + match self { + Compress::None => Compression::Stored, + Compress::Low => Compression::Deflate, + Compress::Medium => Compression::Bz, + Compress::High => Compression::Xz, + } + } +} + fn deserialize_bind_addrs<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, diff --git a/src/server.rs b/src/server.rs index 8d9a5e3..306e22d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -581,8 +581,18 @@ impl Server { let path = path.to_owned(); let hidden = self.args.hidden.clone(); let running = self.running.clone(); + let compression = self.args.compress.to_compression(); tokio::spawn(async move { - if let Err(e) = zip_dir(&mut writer, &path, access_paths, &hidden, running).await { + if let Err(e) = zip_dir( + &mut writer, + &path, + access_paths, + &hidden, + compression, + running, + ) + .await + { error!("Failed to zip {}, {}", path.display(), e); } }); @@ -1422,6 +1432,7 @@ async fn zip_dir( dir: &Path, access_paths: AccessPaths, hidden: &[String], + compression: Compression, running: Arc, ) -> Result<()> { let mut writer = ZipFileWriter::with_tokio(writer); @@ -1475,7 +1486,7 @@ async fn zip_dir( None => continue, }; let (datetime, mode) = get_file_mtime_and_mode(&zip_path).await?; - let builder = ZipEntryBuilder::new(filename.into(), Compression::Deflate) + let builder = ZipEntryBuilder::new(filename.into(), compression) .unix_permissions(mode) .last_modification_date(ZipDateTime::from_chrono(&datetime)); let mut file = File::open(&zip_path).await?; diff --git a/tests/http.rs b/tests/http.rs index fc6583d..67fb328 100644 --- a/tests/http.rs +++ b/tests/http.rs @@ -40,7 +40,12 @@ fn head_dir_404(server: TestServer) -> Result<(), Error> { } #[rstest] -fn get_dir_zip(#[with(&["-A"])] server: TestServer) -> Result<(), Error> { +#[case(server(&["--allow-archive"] as &[&str]))] +#[case(server(&["--allow-archive", "--compress", "none"]))] +#[case(server(&["--allow-archive", "--compress", "low"]))] +#[case(server(&["--allow-archive", "--compress", "medium"]))] +#[case(server(&["--allow-archive", "--compress", "high"]))] +fn get_dir_zip(#[case] server: TestServer) -> Result<(), Error> { let resp = reqwest::blocking::get(format!("{}?zip", server.url()))?; assert_eq!(resp.status(), 200); assert_eq!(