diff --git a/cli/transbeam-cli b/cli/transbeam-cli index ace59ae..411716f 100755 --- a/cli/transbeam-cli +++ b/cli/transbeam-cli @@ -65,11 +65,11 @@ async def file_loader(files): file_progress.update(len(data)) yield data -async def send(paths, host, password, lifetime, collection_name=None): +async def send(paths, host, password, lifetime, collection_name=None, relpaths=False): paths = [path for path in paths if path.is_file()] fileMetadata = [ { - "name": path.name, + "name": str(path) if relpaths else path.name, "size": path.stat().st_size, "modtime": math.floor(path.stat().st_mtime * 1000), } for path in paths @@ -97,6 +97,7 @@ parser = argparse.ArgumentParser(description="Upload files to transbeam") parser.add_argument("-l", "--lifetime", type=int, default=7, help="Lifetime in days for files (default 7)") parser.add_argument("-H", "--host", type=str, default="transbeam.link", help="transbeam host (default transbeam.link)") parser.add_argument("-n", "--collection-name", type=str, help="Name for a collection of multiple files") +parser.add_argument("-R", "--relative-paths", action="store_true", help="Preserve file paths relative to working directory") parser.add_argument("files", type=pathlib.Path, nargs="+", help="Files to upload") async def main(): @@ -105,6 +106,6 @@ async def main(): print("--collection-name is only applicable when multiple files are being uploaded") exit(1) password = getpass.getpass() - await send(args.files, args.host, password, args.lifetime, args.collection_name) + await send(args.files, args.host, password, args.lifetime, args.collection_name, args.relative_paths) asyncio.run(main()) diff --git a/src/upload.rs b/src/upload.rs index 4f59490..b016f6a 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, fs::File, io::Write}; +use std::{collections::HashSet, fs::File, io::Write, path::Path, path::Component}; use actix::{fut::future::ActorFutureExt, Actor, ActorContext, AsyncContext, StreamHandler}; use actix_http::ws::{CloseReason, Item}; @@ -22,20 +22,42 @@ const MAX_FILES: usize = 256; const FILENAME_DATE_FORMAT: &[time::format_description::FormatItem] = time::macros::format_description!("[year]-[month]-[day]-[hour][minute][second]"); -/// Sanitises a filename after performing unicode normalization, +/// Sanitises a file or directory name after performing unicode normalization, /// optionally reducing the length limit to leave space for an /// extension yet to be added. -fn sanitise(name: &str, extension_length: usize) -> String { +fn sanitise_path_component(name: &str, extension_length: usize) -> String { let name = name.nfd().collect::(); sanitise_with_options( &name, &SanOptions { - length_limit: SanOptions::DEFAULT.length_limit - extension_length, + length_limit: SanOptions::DEFAULT.length_limit.saturating_sub(extension_length), ..SanOptions::DEFAULT }, ) } +fn sanitise_path(path: &str) -> String { + let mut san_path = Path::new(path).components().rfold(String::new(), |subpath, c| { + if subpath.len() >= SanOptions::DEFAULT.length_limit*8 { + return subpath; + } + if let Component::Normal(s) = c { + let mut component = sanitise_path_component(&s.to_string_lossy(), 0); + if !subpath.is_empty() { + component.push('/'); + component.push_str(&subpath); + } + component + } else { + subpath + } + }); + if san_path.is_empty() { + san_path.push('_'); + } + san_path +} + #[derive(thiserror::Error, Debug)] enum Error { #[error("Failed to parse file metadata")] @@ -114,7 +136,7 @@ pub use crate::state::v1::UploadedFile; impl UploadedFile { fn new(name: &str, size: u64, modtime: OffsetDateTime) -> Self { Self { - name: sanitise(name, 0), + name: sanitise_path(name), size, modtime, } @@ -285,7 +307,7 @@ impl Uploader { info!("Wrapping in zipfile generator"); let now = OffsetDateTime::now_utc(); let collection_name = - collection_name.map(|f| sanitise(&f, 4)).unwrap_or_else(|| { + collection_name.map(|f| sanitise_path_component(&f, 4)).unwrap_or_else(|| { super::APP_NAME.to_owned() + &now.format(FILENAME_DATE_FORMAT).unwrap() });