diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/archive.rs | 80 | ||||
-rw-r--r-- | src/listing.rs | 73 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | src/renderer.rs | 35 |
4 files changed, 171 insertions, 19 deletions
diff --git a/src/archive.rs b/src/archive.rs new file mode 100644 index 0000000..cc39207 --- /dev/null +++ b/src/archive.rs @@ -0,0 +1,80 @@ +use bytes::Bytes; +use serde::Deserialize; +use std::fs::{File, OpenOptions}; +use std::io::Read; +use std::path::PathBuf; +use tar::Builder; +use tempfile::tempdir; + +#[derive(Debug)] +pub enum CompressionError { + IOError(std::io::Error), + NoneError(std::option::NoneError), +} + +impl From<std::option::NoneError> for CompressionError { + fn from(err: std::option::NoneError) -> CompressionError { + CompressionError::NoneError(err) + } +} + +impl From<std::io::Error> for CompressionError { + fn from(err: std::io::Error) -> CompressionError { + CompressionError::IOError(err) + } +} + +/// Available compression methods +#[derive(Debug, Deserialize, Clone)] +pub enum CompressionMethod { + /// ZIP + #[serde(alias = "targz")] + TarGz, +} + +impl CompressionMethod { + pub fn to_string(&self) -> String { + match &self { + CompressionMethod::TarGz => "targz", + } + .to_string() + } + + pub fn extension(&self) -> String { + match &self { + CompressionMethod::TarGz => "tar.gz", + } + .to_string() + } +} + +pub fn create_archive_file( + method: &CompressionMethod, + dir: &PathBuf, +) -> Result<(String, usize, Bytes), CompressionError> { + match method { + CompressionMethod::TarGz => tgz_compress(&dir), + } +} + +fn tgz_compress(dir: &PathBuf) -> Result<(String, usize, Bytes), CompressionError> { + let src_dir = dir.display().to_string(); + let inner_folder = dir.file_name()?.to_str()?; + let dst_filename = format!("{}.tar", inner_folder); + let tmp_dir = tempdir()?; + + let dst_filepath = tmp_dir.path().join(dst_filename.clone()); + let tar_file = File::create(&dst_filepath)?; + let mut tar_builder = Builder::new(&tar_file); + tar_builder.append_dir_all(inner_folder, src_dir)?; + tar_builder.finish()?; + + let mut tar_file = OpenOptions::new().read(true).open(&dst_filepath)?; + let mut contents = Vec::new(); + let content_length = tar_file.read_to_end(&mut contents).unwrap(); + + let mut data = Bytes::new(); + data.extend_from_slice(&contents); + + Ok((dst_filename, content_length, data)) +} diff --git a/src/listing.rs b/src/listing.rs index 57bef17..565b5bf 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -1,12 +1,15 @@ -use actix_web::{fs, FromRequest, HttpRequest, HttpResponse, Query, Result}; +use actix_web::{fs, http, Body, FromRequest, HttpRequest, HttpResponse, Query, Result}; use bytesize::ByteSize; +use futures::stream::once; use htmlescape::encode_minimal as escape_html_entity; use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use serde::Deserialize; use std::io; use std::path::Path; use std::time::SystemTime; +use yansi::Paint; +use crate::archive; use crate::renderer; /// Query parameters @@ -14,6 +17,7 @@ use crate::renderer; struct QueryParameters { sort: Option<SortingMethod>, order: Option<SortingOrder>, + download: Option<archive::CompressionMethod>, } /// Available sorting methods @@ -134,11 +138,16 @@ pub fn directory_listing<S>( let is_root = base.parent().is_none() || req.path() == random_route; let page_parent = base.parent().map(|p| p.display().to_string()); - let (sort_method, sort_order) = if let Ok(query) = Query::<QueryParameters>::extract(req) { - (query.sort.clone(), query.order.clone()) - } else { - (None, None) - }; + let (sort_method, sort_order, download) = + if let Ok(query) = Query::<QueryParameters>::extract(req) { + ( + query.sort.clone(), + query.order.clone(), + query.download.clone(), + ) + } else { + (None, None, None) + }; let mut entries: Vec<Entry> = Vec::new(); @@ -218,17 +227,43 @@ pub fn directory_listing<S>( } } - Ok(HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body( - renderer::page( - &title, - entries, - is_root, - page_parent, - sort_method, - sort_order, - ) - .into_string(), - )) + if let Some(compression_method) = &download { + match archive::create_archive_file(&compression_method, &dir.path) { + Ok((filename, content_length, content)) => Ok(HttpResponse::Ok() + .content_type("application/tar") + .content_length(content_length as u64) + .header("Content-Transfer-Encoding", "binary") + .header( + "Content-Disposition", + format!("attachment; filename={:?}", filename), + ) + .chunked() + .body(Body::Streaming(Box::new(once(Ok(content)))))), + Err(err) => { + println!( + "{error} an error occured while compressing {folder}: {err:?}", + error = Paint::red("error:").bold(), + folder = dir.path.display(), + err = err + ); + Ok(HttpResponse::Ok() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body("")) + } + } + } else { + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body( + renderer::page( + &title, + entries, + is_root, + page_parent, + sort_method, + sort_order, + ) + .into_string(), + )) + } } diff --git a/src/main.rs b/src/main.rs index b15088c..9f2cacf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![feature(proc_macro_hygiene)] +#![feature(try_trait)] use actix_web::{fs, middleware, server, App}; use clap::crate_version; @@ -9,6 +10,7 @@ use std::thread; use std::time::Duration; use yansi::{Color, Paint}; +mod archive; mod args; mod auth; mod listing; diff --git a/src/renderer.rs b/src/renderer.rs index 89a9248..b83a67c 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -3,6 +3,7 @@ use chrono_humanize::{Accuracy, HumanTime, Tense}; use maud::{html, Markup, PreEscaped, DOCTYPE}; use std::time::SystemTime; +use crate::archive; use crate::listing; /// Renders the file listing @@ -19,6 +20,9 @@ pub fn page( body { span #top { } h1 { (page_title) } + div.download { + (archive_button(archive::CompressionMethod::TarGz)) + } table { thead { th { (build_link("name", "Name", &sort_method, &sort_order)) } @@ -50,6 +54,18 @@ pub fn page( } } +/// Partial: archive button +fn archive_button(compress_method: archive::CompressionMethod) -> Markup { + let link = format!("?download={}", compress_method.to_string()); + let text = format!("Download .{}", compress_method.extension()); + + html! { + a href=(link) { + (text) + } + } +} + /// Partial: table header link fn build_link( name: &str, @@ -259,6 +275,25 @@ fn css() -> Markup { color: #3498db; text-decoration: none; } + .download { + display: flex; + justify-content: flex-end; + padding: 0.125rem; + } + .download a, .download a:visited { + color: #3498db; + } + .download a { + background: #efefef; + padding: 0.5rem; + border-radius: 0.2rem; + } + .download a:hover { + background: #deeef7a6; + } + .download a:not(:last-of-type) { + margin-right: 1rem; + } @media (max-width: 600px) { h1 { font-size: 1.375em; |