diff options
Diffstat (limited to '')
-rw-r--r-- | Cargo.lock | 40 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/archive.rs | 153 | ||||
-rw-r--r-- | src/args.rs | 15 | ||||
-rw-r--r-- | src/listing.rs | 8 | ||||
-rw-r--r-- | src/main.rs | 13 | ||||
-rw-r--r-- | src/renderer.rs | 9 | ||||
-rw-r--r-- | tests/archive.rs | 6 |
8 files changed, 230 insertions, 15 deletions
@@ -593,6 +593,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "bzip2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bzip2-sys 0.1.8+1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.8+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "cc" version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1580,6 +1598,7 @@ dependencies = [ "tar 0.4.26 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "yansi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zip 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1892,6 +1911,11 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "podio" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "port_check" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3392,6 +3416,18 @@ name = "yansi" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "zip" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum actix 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6c616db5fa4b0c40702fb75201c2af7f8aa8f3a2e2c1dda3b0655772aa949666" "checksum actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9f2c11af4b06dc935d8e1b1491dad56bfb32febc49096a91e773f8535c176453" @@ -3444,6 +3480,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" "checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" "checksum bytesize 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "716960a18f978640f25101b5cbf1c6f6b0d3192fab36a2d98ca96f0ecbe41010" +"checksum bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" +"checksum bzip2-sys 0.1.8+1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "05305b41c5034ff0e93937ac64133d109b5a2660114ec45e9760bc6816d83038" "checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" @@ -3592,6 +3630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" "checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" +"checksum podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "780fb4b6698bbf9cf2444ea5d22411cef2953f0824b98f33cf454ec5615645bd" "checksum port_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f6519412c9e0d4be579b9f0618364d19cb434b324fc6ddb1b27b1e682c7105ed" "checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" "checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" @@ -3754,3 +3793,4 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" "checksum xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" "checksum yansi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" +"checksum zip 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6df134e83b8f0f8153a094c7b0fd79dfebe437f1d76e7715afa18ed95ebe2fd7" @@ -46,6 +46,7 @@ strum = "0.18.0" strum_macros = "0.18.0" sha2 = "0.8.1" hex = "0.4.2" +zip = "0.5.5" [dev-dependencies] assert_cmd = "1.0" diff --git a/src/archive.rs b/src/archive.rs index 268bb47..c96814f 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -1,9 +1,13 @@ use actix_web::http::ContentEncoding; use libflate::gzip::Encoder; +use zip::{ZipWriter, write}; +use std::fs::File; +use std::path::PathBuf; use serde::Deserialize; use std::path::Path; use strum_macros::{Display, EnumIter, EnumString}; use tar::Builder; +use std::io::{Cursor, Write, Read}; use crate::errors::ContextualError; @@ -17,6 +21,9 @@ pub enum CompressionMethod { /// Regular tarball Tar, + + /// Regular zip + Zip, } impl CompressionMethod { @@ -24,6 +31,7 @@ impl CompressionMethod { match self { CompressionMethod::TarGz => "tar.gz", CompressionMethod::Tar => "tar", + CompressionMethod::Zip => "zip", } .to_string() } @@ -32,6 +40,7 @@ impl CompressionMethod { match self { CompressionMethod::TarGz => "application/gzip", CompressionMethod::Tar => "application/tar", + CompressionMethod::Zip => "application/zip", } .to_string() } @@ -40,6 +49,15 @@ impl CompressionMethod { match self { CompressionMethod::TarGz => ContentEncoding::Gzip, CompressionMethod::Tar => ContentEncoding::Identity, + CompressionMethod::Zip => ContentEncoding::Identity, + } + } + + pub fn is_enabled(self, tar_enabled: bool, zip_enabled: bool,) -> bool { + match self { + CompressionMethod::TarGz => tar_enabled, + CompressionMethod::Tar => tar_enabled, + CompressionMethod::Zip => zip_enabled, } } @@ -62,6 +80,7 @@ impl CompressionMethod { match self { CompressionMethod::TarGz => tar_gz(dir, skip_symlinks, out), CompressionMethod::Tar => tar_dir(dir, skip_symlinks, out), + CompressionMethod::Zip => zip_dir(dir, skip_symlinks, out), } } } @@ -159,3 +178,137 @@ where Ok(()) } + +/// Write a zip of `dir` in `out`. +/// +/// The target directory will be saved as a top-level directory in the archive. +/// +/// For example, consider this directory structure: +/// +/// ``` +/// a +/// └── b +/// └── c +/// ├── e +/// ├── f +/// └── g +/// ``` +/// +/// Making a zip out of `"a/b/c"` will result in this archive content: +/// +/// ``` +/// c +/// ├── e +/// ├── f +/// └── g +/// ``` +fn create_zip_from_directory<W>(out: W, directory: &PathBuf, skip_symlinks: bool,) -> Result<(), ContextualError> +where + W: std::io::Write + std::io::Seek +{ + let options = write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); + let mut paths_queue: Vec<PathBuf> = vec![]; + paths_queue.push(directory.clone()); + let zip_root_folder_name = directory.file_name().ok_or_else(|| { + ContextualError::InvalidPathError("Directory name terminates in \"..\"".to_string()) + })?; + + let mut zip_writer = ZipWriter::new(out); + let mut buffer = Vec::new(); + while paths_queue.len() > 0 { + let next = paths_queue.pop().ok_or(ContextualError::CustomError("Could not get path from queue".to_string()))?; + let current_dir = next.as_path(); + let directory_entry_iterator = std::fs::read_dir(current_dir).map_err(|e|{ + ContextualError::IOError("Could not read directory".to_string(), e) + })?; + let zip_directory = Path::new(zip_root_folder_name) + .join(current_dir.strip_prefix(directory).map_err(|_|{ + ContextualError::CustomError("Could not append base directory".to_string()) + })?); + + for entry in directory_entry_iterator { + let entry_path = entry.ok().ok_or( + ContextualError::InvalidPathError("Directory name terminates in \"..\"".to_string()) + )?.path(); + let entry_metadata = std::fs::metadata(entry_path.clone()).map_err(|e|{ + ContextualError::IOError("Could not get file metadata".to_string(), e) + })?; + + if entry_metadata.file_type().is_symlink() && skip_symlinks { + continue; + } + let current_entry_name = entry_path.file_name().ok_or_else(|| { + ContextualError::InvalidPathError("Invalid file or direcotory name".to_string()) + })?; + if entry_metadata.is_file() { + let mut f = File::open(&entry_path).map_err(|e| { + ContextualError::IOError("Could not open file".to_string(), e) + })?; + f.read_to_end(&mut buffer).map_err(|e| { + ContextualError::IOError("Could not read from file".to_string(), e) + })?; + let relative_path = zip_directory.join(current_entry_name); + zip_writer.start_file_from_path(Path::new(&relative_path), options).map_err(|_| { + ContextualError::CustomError("Could not add file path to ZIP".to_string()) + })?; + zip_writer.write(buffer.as_ref()).map_err(|_| { + ContextualError::CustomError("Could not write file to ZIP".to_string()) + })?; + buffer.clear(); + } else if entry_metadata.is_dir() { + let relative_path = zip_directory.join(current_entry_name); + zip_writer.add_directory_from_path(Path::new(&relative_path), options).map_err(|_| { + ContextualError::CustomError("Could not add directory path to ZIP".to_string()) + })?; + paths_queue.push(entry_path.clone()); + } + } + } + + zip_writer.finish().map_err(|_| { + ContextualError::CustomError("Could not finish writing ZIP archive".to_string()) + })?; + Ok(()) +} + +/// Writes a zip of `dir` in `out`. +/// +/// The content of `src_dir` will be saved in the archive as the folder named . +fn zip_data<W>( + src_dir: &Path, + skip_symlinks: bool, + mut out: W, +) -> Result<(), ContextualError> +where + W: std::io::Write, +{ + let mut data = Vec::new(); + let memory_file = Cursor::new(&mut data); + create_zip_from_directory(memory_file, &src_dir.to_path_buf(), skip_symlinks).map_err(|e| { + ContextualError::ArchiveCreationError("Failed to create the ZIP archive".to_string(), Box::new(e)) + })?; + + out.write_all(data.as_mut_slice()).map_err(|e| { + ContextualError::IOError("Failed to write the ZIP archive".to_string(), e) + })?; + + Ok(()) +} + +fn zip_dir<W>(dir: &Path, skip_symlinks: bool, out: W) -> Result<(), ContextualError> +where + W: std::io::Write, +{ + let inner_folder = dir.file_name().ok_or_else(|| { + ContextualError::InvalidPathError("Directory name terminates in \"..\"".to_string()) + })?; + + inner_folder.to_str().ok_or_else(|| { + ContextualError::InvalidPathError( + "Directory name contains invalid UTF-8 characters".to_string(), + ) + })?; + + zip_data(dir, skip_symlinks, out) + .map_err(|e| ContextualError::ArchiveCreationError("zip".to_string(), Box::new(e))) +}
\ No newline at end of file diff --git a/src/args.rs b/src/args.rs index 49fe276..ea4e90f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -84,9 +84,15 @@ struct CLIArgs { #[structopt(short = "o", long = "overwrite-files")] overwrite_files: bool, - /// Disable archive generation - #[structopt(short = "r", long = "disable-archives")] - disable_archives: bool, + /// Enable tar archive generation + #[structopt(short = "r", long = "enable-tar")] + enable_tar: bool, + + /// Enable zip archive generation + /// Zipping large directories can result in out-of-memory exception + /// because zip generation is done in memory and cannot be sent on the fly + #[structopt(short = "z", long = "enable-zip")] + enable_zip: bool, } /// Checks wether an interface is valid, i.e. it can be parsed into an IP address @@ -176,7 +182,8 @@ pub fn parse_args() -> crate::MiniserveConfig { index: args.index, overwrite_files: args.overwrite_files, file_upload: args.file_upload, - archives: !args.disable_archives, + tar_enabled: args.enable_tar, + zip_enabled: args.enable_zip, } } diff --git a/src/listing.rs b/src/listing.rs index d28824c..45b3732 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -136,7 +136,8 @@ pub fn directory_listing<S>( random_route: Option<String>, default_color_scheme: ColorScheme, upload_route: String, - archives_enabled: bool, + tar_enabled: bool, + zip_enabled: bool, ) -> Result<HttpResponse, io::Error> { let serve_path = req.path(); @@ -250,7 +251,7 @@ pub fn directory_listing<S>( let color_scheme = query_params.theme.unwrap_or(default_color_scheme); if let Some(compression_method) = query_params.download { - if !archives_enabled { + if !compression_method.is_enabled(tar_enabled, zip_enabled) { return Ok(HttpResponse::Forbidden() .content_type("text/html; charset=utf-8") .body( @@ -332,7 +333,8 @@ pub fn directory_listing<S>( file_upload, &upload_route, ¤t_dir.display().to_string(), - archives_enabled, + tar_enabled, + zip_enabled, ) .into_string(), )) diff --git a/src/main.rs b/src/main.rs index a1ee303..3ff35c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,8 +63,11 @@ pub struct MiniserveConfig { /// Enable upload to override existing files pub overwrite_files: bool, - /// If false, creation of archives is disabled - pub archives: bool, + /// If false, creation of tar archives is disabled + pub tar_enabled: bool, + + /// If false, creation of zip archives is disabled + pub zip_enabled: bool, } fn main() { @@ -256,7 +259,8 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { let random_route = app.state().random_route.clone(); let default_color_scheme = app.state().default_color_scheme; let file_upload = app.state().file_upload; - let archives_enabled = app.state().archives; + let tar_enabled = app.state().tar_enabled; + let zip_enabled = app.state().zip_enabled; upload_route = if let Some(random_route) = app.state().random_route.clone() { format!("/{}/upload", random_route) } else { @@ -285,7 +289,8 @@ fn configure_app(app: App<MiniserveConfig>) -> App<MiniserveConfig> { random_route.clone(), default_color_scheme, u_r.clone(), - archives_enabled, + tar_enabled, + zip_enabled, ) }) .default_handler(error_404), diff --git a/src/renderer.rs b/src/renderer.rs index face6ff..accb49b 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -22,7 +22,8 @@ pub fn page( file_upload: bool, upload_route: &str, current_dir: &str, - archives: bool, + tar_enabled: bool, + zip_enabled: bool, ) -> Markup { let upload_action = build_upload_action( upload_route, @@ -50,10 +51,12 @@ pub fn page( span#top { } h1.title { "Index of " (serve_path) } div.toolbar { - @if archives { + @if tar_enabled || zip_enabled { div.download { @for compression_method in CompressionMethod::iter() { - (archive_button(compression_method, sort_method, sort_order, color_scheme, default_color_scheme)) + @if compression_method.is_enabled(tar_enabled, zip_enabled) { + (archive_button(compression_method, sort_method, sort_order, color_scheme, default_color_scheme)) + } } } } diff --git a/tests/archive.rs b/tests/archive.rs index e4cca81..c170bc3 100644 --- a/tests/archive.rs +++ b/tests/archive.rs @@ -17,7 +17,6 @@ fn archives_are_disabled(tmpdir: TempDir, port: u16) -> Result<(), Error> { .arg(tmpdir.path()) .arg("-p") .arg(port.to_string()) - .arg("-r") .stdout(Stdio::null()) .spawn()?; @@ -42,6 +41,11 @@ fn archives_are_disabled(tmpdir: TempDir, port: u16) -> Result<(), Error> { .status(), StatusCode::FORBIDDEN ); + assert_eq!( + reqwest::blocking::get(format!("http://localhost:{}/?download=zip", port).as_str())? + .status(), + StatusCode::FORBIDDEN + ); child.kill()?; |