From 09af48037910bdb410790165738db9a61583ecc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Sun, 8 Jun 2025 21:31:50 +0000 Subject: [PATCH 01/14] [wip] impl `Client` for `ureq::Agent` - remove `sharry::alias` and `sharry::share` (reduced to IDs) - use `sharry_share_create` in `AppState` --- src/appstate.rs | 34 +++++++++++++++---------- src/cli.rs | 10 ++++---- src/main.rs | 8 ++++-- src/sharry/alias.rs | 36 --------------------------- src/sharry/client.rs | 38 ++++++++++++++++++---------- src/sharry/file/checked.rs | 2 +- src/sharry/file/mod.rs | 2 -- src/sharry/file/uploading.rs | 2 +- src/sharry/mod.rs | 6 +---- src/sharry/share.rs | 48 ------------------------------------ 10 files changed, 60 insertions(+), 126 deletions(-) delete mode 100644 src/sharry/alias.rs delete mode 100644 src/sharry/share.rs diff --git a/src/appstate.rs b/src/appstate.rs index f3bcbcf..1c0d1f2 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -13,7 +13,9 @@ use serde::{Deserialize, Serialize}; use super::{ cli::Cli, - sharry::{Alias, ChunkState, FileChecked, FileUploading, Share, SharryFile, UploadError}, + sharry::{ + ChunkState, Client, ClientError, FileChecked, FileUploading, SharryFile, UploadError, Uri, + }, }; #[derive(Serialize, Deserialize, Debug)] @@ -23,8 +25,9 @@ pub struct AppState { #[serde(skip)] progress: Option, - alias: Alias, - share: Share, + uri: Uri, + alias_id: String, + share_id: String, files: VecDeque, } @@ -88,27 +91,30 @@ impl AppState { Self { file_name, progress: None, - alias: state.alias, - share: state.share, + uri: state.uri, + alias_id: state.alias_id, + share_id: state.share_id, files: state.files, } }) .ok() } - pub fn from_args(args: &Cli, http: &ureq::Agent) -> Result { + pub fn from_args(args: &Cli, http: &impl Client) -> Result { let file_name = Self::cache_file(args); - let alias = args.get_alias(); - let share = Share::create(http, &alias, args.get_share_request())?; + let uri = args.get_uri(); + let alias_id = args.alias.clone(); + let share_id = http.sharry_share_create(&uri, &alias_id, args.get_share_request())?; let files: VecDeque<_> = args.files.clone().into_iter().map(FileState::C).collect(); Ok(Self { file_name, progress: None, - alias, - share, + uri, + alias_id, + share_id, files, }) } @@ -123,7 +129,9 @@ impl AppState { chunk_size: usize, ) -> Result, UploadError> { let uploading = if let Some(state) = self.files.pop_front() { - state.start_upload(http, &self.alias, &self.share).unwrap() // HACK unwrap + state + .start_upload(http, &self.alias_id, &self.share_id) + .unwrap() // HACK unwrap } else { return Ok(None); }; @@ -152,7 +160,7 @@ impl AppState { bar }); - match uploading.upload_chunk(http, &self.alias, chunk_size) { + match uploading.upload_chunk(http, &self.alias_id, chunk_size) { ChunkState::Ok(upl) => { bar.set_position(upl.get_offset()); self.files.push_front(FileState::U(upl)); @@ -166,7 +174,7 @@ impl AppState { debug!("Finished {:?}!", path.display()); bar.finish(); self.progress = None; - self.share.notify(http, &self.alias).unwrap(); // HACK unwrap + self.share_id.notify(http, &self.alias_id).unwrap(); // HACK unwrap Ok(self.files.front().map(drop)) } diff --git a/src/cli.rs b/src/cli.rs index 6e96e3b..5e853d1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,7 +5,7 @@ use std::{ use clap::{Parser, builder::PossibleValuesParser}; -use super::sharry::{Alias, FileChecked, NewShareRequest, Uri}; +use super::sharry::{FileChecked, NewShareRequest, Uri}; #[derive(Parser, Debug, Hash)] #[command(version, about, long_about = None)] @@ -46,7 +46,7 @@ pub struct Cli { url: String, /// ID of a public alias to use - alias: String, + pub alias: String, /// Files to upload to the new share #[arg(value_name = "FILE", required = true, value_parser = parse_sharry_file)] @@ -66,8 +66,8 @@ impl Cli { (!self.timeout.is_zero()).then_some(self.timeout) } - pub fn get_alias(&self) -> Alias { - Alias::new(Uri::with_protocol(&self.protocol, &self.url), &self.alias) + pub fn get_uri(&self) -> Uri { + Uri::with_protocol(&self.protocol, &self.url) } pub fn get_share_request(&self) -> NewShareRequest { @@ -83,7 +83,7 @@ impl Cli { }; let mut hasher = DefaultHasher::new(); - (self.get_alias(), file_refs).hash(&mut hasher); + (self.get_uri(), &self.alias, file_refs).hash(&mut hasher); format!("{:x}", hasher.finish()) } diff --git a/src/main.rs b/src/main.rs index 7e17e70..10379c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ use ureq::Agent; use appstate::AppState; use cli::Cli; +use sharry::ClientError; fn main() { println!( @@ -64,8 +65,11 @@ fn main() { } Err(e) => { if let Some(cause) = match e { - ureq::Error::StatusCode(403) => Some("Alias ID"), - ureq::Error::Io(_) => Some("URL"), + ClientError::ResponseStatus { + actual: _, + expected: 403, + } => Some("Alias ID"), + ClientError::FileIO(_) => Some("URL"), _ => None, } { info!("handling error: {e:?}"); diff --git a/src/sharry/alias.rs b/src/sharry/alias.rs deleted file mode 100644 index ba51bce..0000000 --- a/src/sharry/alias.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::fmt::{Debug, Display}; - -use log::debug; -use serde::{Deserialize, Serialize}; -use ureq::RequestBuilder; - -use super::api::Uri; - -#[derive(Serialize, Deserialize, Debug, Hash)] -pub struct Alias { - pub(super) uri: Uri, - pub(super) id: String, -} - -pub(super) trait SharryAlias { - fn sharry_header(self, alias: &Alias) -> Self; -} - -impl SharryAlias for RequestBuilder { - fn sharry_header(self, alias: &Alias) -> Self { - self.header("Sharry-Alias", &alias.id) - } -} - -impl Alias { - pub fn new(uri: Uri, id: impl Into) -> Self { - Self { uri, id: id.into() } - } - - pub(super) fn get_endpoint(&self, endpoint: impl Display + Debug) -> String { - let uri = format!("{}/{}", self.uri, endpoint); - debug!("endpoint uri: {uri:?}"); - - uri - } -} diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 85951ee..1adcb5a 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -1,6 +1,7 @@ -use std::{error::Error, io}; +use std::{error::Error, fmt::Display, io}; use log::debug; +use thiserror::Error; use super::{ api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri}, @@ -33,7 +34,7 @@ pub trait Client { // fn sharry_file_patch(&self); } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Error)] pub enum ClientError { #[error("file I/O error: {0}")] FileIO(#[from] io::Error), @@ -49,9 +50,20 @@ pub enum ClientError { #[error("unexpected response content: {0}")] ResponseContent(String), - // - // #[error("could not parse offset header")] - // ResponseOffset, +} + +impl ClientError { + fn req_err(msg: impl Display) -> Self { + ClientError::Request(msg.to_string()) + } + + fn res_parse_err(msg: impl Display) -> Self { + ClientError::ResponseParsing(msg.to_string()) + } + + fn res_content_err(msg: impl Display) -> Self { + ClientError::ResponseContent(msg.to_string()) + } } impl Client for ureq::Agent { @@ -67,10 +79,10 @@ impl Client for ureq::Agent { self.post(endpoint) .header("Sharry-Alias", alias_id) .send_json(data) - .map_err(|e| ClientError::Request(e.to_string()))? + .map_err(|e| ClientError::req_err(e))? .body_mut() .read_json::() - .map_err(|e| ClientError::ResponseParsing(e.to_string()))? + .map_err(|e| ClientError::res_parse_err(e))? }; debug!("response: {res:?}"); @@ -78,7 +90,7 @@ impl Client for ureq::Agent { if res.success && (res.message == "Share created.") { Ok(res.id) } else { - Err(ClientError::ResponseContent(format!("{res:?}"))) + Err(ClientError::res_content_err(format!("{res:?}"))) } } @@ -94,10 +106,10 @@ impl Client for ureq::Agent { self.post(endpoint) .header("Sharry-Alias", alias_id) .send_empty() - .map_err(|e| ClientError::Request(e.to_string()))? + .map_err(|e| ClientError::req_err(e))? .body_mut() .read_json::() - .map_err(|e| ClientError::ResponseParsing(e.to_string()))? + .map_err(|e| ClientError::res_parse_err(e))? }; debug!("response: {res:?}"); @@ -122,7 +134,7 @@ impl Client for ureq::Agent { .header("Sharry-File-Name", file.get_name()) .header("Upload-Length", size) .send_empty() - .map_err(|e| ClientError::Request(e.to_string()))? + .map_err(|e| ClientError::req_err(e))? }; if res.status() != ureq::http::StatusCode::CREATED { @@ -133,9 +145,9 @@ impl Client for ureq::Agent { } let location = (res.headers().get("Location")) - .ok_or_else(|| ClientError::ResponseParsing("Location header not found".to_owned()))? + .ok_or_else(|| ClientError::res_parse_err("Location header not found"))? .to_str() - .map_err(|_| ClientError::ResponseParsing("Location header invalid".to_owned()))? + .map_err(|e| ClientError::res_parse_err(e))? .to_string(); debug!("patch uri: {location}"); diff --git a/src/sharry/file/checked.rs b/src/sharry/file/checked.rs index e31adc9..4a77c23 100644 --- a/src/sharry/file/checked.rs +++ b/src/sharry/file/checked.rs @@ -8,7 +8,7 @@ use log::debug; use serde::{Deserialize, Serialize}; use ureq::http::{HeaderValue, StatusCode}; -use super::{Alias, FileUploading, Share, SharryAlias, SharryFile}; +use super::{FileUploading, SharryFile}; #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct FileChecked { diff --git a/src/sharry/file/mod.rs b/src/sharry/file/mod.rs index c3e4502..49b715d 100644 --- a/src/sharry/file/mod.rs +++ b/src/sharry/file/mod.rs @@ -9,8 +9,6 @@ use std::{ pub use checked::FileChecked; pub use uploading::{ChunkState, FileUploading, UploadError}; -use super::{Alias, Share, alias::SharryAlias}; - pub trait SharryFile<'t> { /// extract the filename part of a `Path` reference /// diff --git a/src/sharry/file/uploading.rs b/src/sharry/file/uploading.rs index 15fa3d9..608a7f3 100644 --- a/src/sharry/file/uploading.rs +++ b/src/sharry/file/uploading.rs @@ -9,7 +9,7 @@ use log::debug; use serde::{Deserialize, Serialize}; use ureq::http::{HeaderValue, StatusCode}; -use super::{Alias, SharryAlias, SharryFile}; +use super::SharryFile; #[derive(Serialize, Deserialize, Debug)] pub struct FileUploading { diff --git a/src/sharry/mod.rs b/src/sharry/mod.rs index 05ce16e..36ed161 100644 --- a/src/sharry/mod.rs +++ b/src/sharry/mod.rs @@ -1,13 +1,9 @@ #![allow(unused_imports)] -mod alias; mod api; mod client; mod file; -mod share; -pub use alias::Alias; pub use api::{NewShareRequest, Uri}; -// pub use client::SharryClient; +pub use client::{Client, ClientError}; pub use file::{ChunkState, FileChecked, FileUploading, SharryFile, UploadError}; -pub use share::Share; diff --git a/src/sharry/share.rs b/src/sharry/share.rs deleted file mode 100644 index 0733bd4..0000000 --- a/src/sharry/share.rs +++ /dev/null @@ -1,48 +0,0 @@ -use log::debug; -use serde::{Deserialize, Serialize}; - -use super::{ - alias::{Alias, SharryAlias}, - api::{NewShareRequest, NewShareResponse, NotifyShareResponse}, -}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Share { - pub(super) id: String, -} - -impl Share { - pub fn create( - http: &ureq::Agent, - alias: &Alias, - data: NewShareRequest, - ) -> Result { - let res = (http.post(alias.get_endpoint("alias/upload/new"))) - .sharry_header(alias) - .send_json(data)? - .body_mut() - .read_json::()?; - - debug!("response: {res:?}"); - - if !(res.success && (res.message == "Share created.")) { - return Err(ureq::Error::Other("unexpected json response".into())); - } - - Ok(Self { id: res.id }) - } - - pub fn notify(&self, http: &ureq::Agent, alias: &Alias) -> Result<(), ureq::Error> { - let endpoint = alias.get_endpoint(format!("alias/mail/notify/{}", self.id)); - - let res = (http.post(endpoint)) - .sharry_header(alias) - .send_empty()? - .body_mut() - .read_json::()?; - - debug!("response: {res:?}"); - - Ok(()) - } -} From c9528a9ac107e536d27096e23a58662f0bffebf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Mon, 9 Jun 2025 00:05:58 +0000 Subject: [PATCH 02/14] [wip] impl `Client` for `ureq::Agent` - use `sharry_file_create` in `AppState` --- src/appstate.rs | 13 +++++++------ src/sharry/file/checked.rs | 34 ---------------------------------- 2 files changed, 7 insertions(+), 40 deletions(-) diff --git a/src/appstate.rs b/src/appstate.rs index 1c0d1f2..06705c0 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -47,12 +47,13 @@ impl FileState { fn start_upload( self, - http: &ureq::Agent, - alias: &Alias, - share: &Share, - ) -> io::Result { + http: &impl Client, + uri: &Uri, + alias_id: &str, + share_id: &str, + ) -> Result { match self { - FileState::C(checked) => checked.start_upload(http, alias, share), + FileState::C(checked) => http.sharry_file_create(uri, alias_id, share_id, checked), FileState::U(uploading) => Ok(uploading), } } @@ -130,7 +131,7 @@ impl AppState { ) -> Result, UploadError> { let uploading = if let Some(state) = self.files.pop_front() { state - .start_upload(http, &self.alias_id, &self.share_id) + .start_upload(http, &self.uri, &self.alias_id, &self.share_id) .unwrap() // HACK unwrap } else { return Ok(None); diff --git a/src/sharry/file/checked.rs b/src/sharry/file/checked.rs index 4a77c23..b56d62d 100644 --- a/src/sharry/file/checked.rs +++ b/src/sharry/file/checked.rs @@ -29,40 +29,6 @@ impl FileChecked { )) } } - - pub fn start_upload( - self, - http: &ureq::Agent, - alias: &Alias, - share: &Share, - ) -> io::Result { - let size = self.get_size(); - - let res = { - let endpoint = alias.get_endpoint(format!("alias/upload/{}/files/tus", share.id)); - - (http.post(endpoint)) - .sharry_header(alias) - .header("Sharry-File-Name", self.get_name()) - .header("Upload-Length", size) - .send_empty() - .map_err(ureq::Error::into_io)? - }; - - if res.status() != StatusCode::CREATED { - return Err(io::Error::other("unexpected response status")); - } - - let location = (res.headers().get("Location")) - .ok_or_else(|| io::Error::other("Location header not found"))? - .to_str() - .map_err(|_| io::Error::other("Location header invalid"))? - .to_string(); - - debug!("patch uri: {location}"); - - Ok(FileUploading::new(self.path, size, location)) - } } impl<'t> SharryFile<'t> for FileChecked { From 69bef4e994e2d7edaaaeefdd897efb5f93fff558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:17:17 +0000 Subject: [PATCH 03/14] [wip] impl `Client` for `ureq::Agent` - impl `sharry_file_patch` - completely rework chunking logic --- src/appstate.rs | 4 +- src/sharry/client.rs | 94 ++++++++++++++++++++------ src/sharry/file/checked.rs | 15 ++--- src/sharry/file/mod.rs | 4 +- src/sharry/file/uploading.rs | 123 +++++++++-------------------------- src/sharry/mod.rs | 2 +- 6 files changed, 112 insertions(+), 130 deletions(-) diff --git a/src/appstate.rs b/src/appstate.rs index 06705c0..4f68f5d 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -13,9 +13,7 @@ use serde::{Deserialize, Serialize}; use super::{ cli::Cli, - sharry::{ - ChunkState, Client, ClientError, FileChecked, FileUploading, SharryFile, UploadError, Uri, - }, + sharry::{Client, ClientError, FileChecked, FileUploading, SharryFile, Uri}, }; #[derive(Serialize, Deserialize, Debug)] diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 1adcb5a..26065f4 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -28,10 +28,17 @@ pub trait Client { uri: &Uri, alias_id: &str, share_id: &str, - file: FileChecked, - ) -> Result; + file_name: &str, + file_size: u64, + ) -> Result; - // fn sharry_file_patch(&self); + fn sharry_file_patch( + &self, + patch_uri: &str, + alias_id: &str, + offset: u64, + chunk: &[u8], + ) -> Result; } #[derive(Debug, Error)] @@ -54,15 +61,29 @@ pub enum ClientError { impl ClientError { fn req_err(msg: impl Display) -> Self { - ClientError::Request(msg.to_string()) + Self::Request(msg.to_string()) } fn res_parse_err(msg: impl Display) -> Self { - ClientError::ResponseParsing(msg.to_string()) + Self::ResponseParsing(msg.to_string()) } fn res_content_err(msg: impl Display) -> Self { - ClientError::ResponseContent(msg.to_string()) + Self::ResponseContent(msg.to_string()) + } + + fn res_check_status(actual: T, expected: T) -> Result<(), Self> + where + T: Into + Eq, + { + if actual == expected { + Ok(()) + } else { + Err(Self::ResponseStatus { + actual: actual.into(), + expected: expected.into(), + }) + } } } @@ -122,36 +143,67 @@ impl Client for ureq::Agent { uri: &Uri, alias_id: &str, share_id: &str, - file: FileChecked, - ) -> Result { - let size = file.get_size(); - + file_name: &str, + file_size: u64, + ) -> Result { let res = { let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id)); self.post(endpoint) .header("Sharry-Alias", alias_id) - .header("Sharry-File-Name", file.get_name()) - .header("Upload-Length", size) + .header("Sharry-File-Name", file_name) + .header("Upload-Length", file_size) .send_empty() - .map_err(|e| ClientError::req_err(e))? + .map_err(ClientError::req_err)? }; - if res.status() != ureq::http::StatusCode::CREATED { - return Err(ClientError::ResponseStatus { - actual: res.status().as_u16(), - expected: ureq::http::StatusCode::CREATED.as_u16(), - }); - } + ClientError::res_check_status(res.status(), ureq::http::StatusCode::CREATED)?; let location = (res.headers().get("Location")) .ok_or_else(|| ClientError::res_parse_err("Location header not found"))? .to_str() - .map_err(|e| ClientError::res_parse_err(e))? + .map_err(ClientError::res_parse_err)? .to_string(); debug!("patch uri: {location}"); - Ok(FileUploading::new(file.into_path(), size, location)) + Ok(location) + } + + fn sharry_file_patch( + &self, + patch_uri: &str, + alias_id: &str, + offset: u64, + chunk: &[u8], + ) -> Result { + let res = self + .patch(patch_uri) + .header("Sharry-Alias", alias_id) + .header("Upload-Offset", offset) + .send(chunk) + .map_err(ClientError::req_err)?; + + ClientError::res_check_status(res.status(), ureq::http::StatusCode::NO_CONTENT)?; + + let res_offset = (res.headers().get("Upload-Offset")) + .ok_or_else(|| ClientError::res_parse_err("Upload-Offset header not found"))? + .to_str() + .map_err(ClientError::res_parse_err)? + .parse::() + .map_err(ClientError::res_parse_err)?; + + // get chunk length as `u64` (we have checked while reading the chunk!) + let chunk_len = u64::try_from(chunk.len()).expect("something's VERY wrong"); + + if offset + chunk_len == res_offset { + Ok(res_offset) + } else { + Err(ClientError::ResponseContent(format!( + "Unexpected Upload-Offset: {} (expected {})", + res_offset, + offset + chunk_len + ))) + } } } diff --git a/src/sharry/file/checked.rs b/src/sharry/file/checked.rs index b56d62d..a6ec6ba 100644 --- a/src/sharry/file/checked.rs +++ b/src/sharry/file/checked.rs @@ -1,18 +1,16 @@ use std::{ - ffi::OsStr, fs, io, path::{Path, PathBuf}, }; -use log::debug; use serde::{Deserialize, Serialize}; -use ureq::http::{HeaderValue, StatusCode}; -use super::{FileUploading, SharryFile}; +use super::SharryFile; #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct FileChecked { - path: PathBuf, + pub(super) path: PathBuf, + pub(super) size: u64, } impl FileChecked { @@ -21,6 +19,7 @@ impl FileChecked { if meta.is_file() { Ok(Self { path: fs::canonicalize(&value)?, + size: meta.len(), }) } else { Err(io::Error::new( @@ -32,10 +31,6 @@ impl FileChecked { } impl<'t> SharryFile<'t> for FileChecked { - fn into_path(self) -> PathBuf { - self.path - } - /// get a reference to the file's name /// /// Uses `SharryFile::extract_file_name`, which may **panic**! @@ -44,6 +39,6 @@ impl<'t> SharryFile<'t> for FileChecked { } fn get_size(&self) -> u64 { - fs::metadata(&self.path).unwrap().len() + self.size } } diff --git a/src/sharry/file/mod.rs b/src/sharry/file/mod.rs index 49b715d..7ae4823 100644 --- a/src/sharry/file/mod.rs +++ b/src/sharry/file/mod.rs @@ -7,7 +7,7 @@ use std::{ }; pub use checked::FileChecked; -pub use uploading::{ChunkState, FileUploading, UploadError}; +pub use uploading::FileUploading; pub trait SharryFile<'t> { /// extract the filename part of a `Path` reference @@ -21,8 +21,6 @@ pub trait SharryFile<'t> { .expect("bad file name") } - fn into_path(self) -> PathBuf; - fn get_name(&'t self) -> &'t str; fn get_size(&self) -> u64; diff --git a/src/sharry/file/uploading.rs b/src/sharry/file/uploading.rs index 608a7f3..1bd9f8a 100644 --- a/src/sharry/file/uploading.rs +++ b/src/sharry/file/uploading.rs @@ -1,50 +1,21 @@ use std::{ - ffi::OsStr, fmt, fs, io::{self, Read, Seek, SeekFrom}, path::PathBuf, }; -use log::debug; use serde::{Deserialize, Serialize}; -use ureq::http::{HeaderValue, StatusCode}; -use super::SharryFile; +use super::{FileChecked, SharryFile}; #[derive(Serialize, Deserialize, Debug)] pub struct FileUploading { path: PathBuf, size: u64, - uri: String, + patch_uri: String, offset: u64, } -#[derive(Debug, thiserror::Error)] -pub enum UploadError { - #[error("file I/O error: {0}")] - FileIO(#[from] io::Error), - - #[error("network request failed")] - Request, - - #[error("unexpected response status")] - ResponseStatus, - - #[error("could not parse offset header")] - ResponseOffset, - // #[error("chunk length conversion failed: {0}")] - // InvalidChunkLength(String), - - // #[error("offset mismatch")] - // ResponseOffsetMismatch, -} - -pub enum ChunkState { - Ok(FileUploading), - Err(FileUploading, UploadError), - Finished(PathBuf), -} - impl fmt::Display for FileUploading { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( @@ -58,87 +29,55 @@ impl fmt::Display for FileUploading { } impl FileUploading { - pub fn new(path: PathBuf, size: u64, uri: String) -> Self { + pub fn new(file: FileChecked, patch_uri: String) -> Self { Self { - path, - size, - uri, + path: file.path, + size: file.size, + patch_uri, offset: 0, } } + pub fn get_patch_uri(&self) -> &str { + &self.patch_uri + } + pub fn get_offset(&self) -> u64 { self.offset } - fn read_chunk(&self, chunk_size: usize) -> io::Result> { + pub fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut f = fs::File::open(&self.path)?; + f.seek(SeekFrom::Start(self.offset))?; + let read_len = f.read(buf)?; - let mut bytes = vec![0; chunk_size]; - let read_len = f.read(&mut bytes)?; - bytes.truncate(read_len); + // convert into `u64` + // + // BOOKMARK this might **panic** on platforms where `usize` has more than 64 bit. + // Also, you're reading more than 2 EiB ... in ONE chunk. + // Whoa! Maybe just chill? + let read_len = u64::try_from(read_len) + .unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", read_len, e)); - Ok(bytes) + self.offset += read_len; + + Ok(read_len) } - pub fn upload_chunk( - mut self, - http: &ureq::Agent, - alias: &Alias, - chunk_size: usize, - ) -> ChunkState { - let chunk = match self.read_chunk(chunk_size) { - Err(e) => return ChunkState::Err(self, UploadError::FileIO(e)), - Ok(value) => value, - }; - - let Ok(res) = (http.patch(&self.uri)) - .sharry_header(alias) - .header("Upload-Offset", self.offset) - .send(&chunk) - else { - return ChunkState::Err(self, UploadError::Request); - }; - - if res.status() != StatusCode::NO_CONTENT { - return ChunkState::Err(self, UploadError::ResponseStatus); + pub fn check_eof(self) -> Result { + if self.offset < self.size { + Ok(self) + } else { + Err(self.path) } - - let Some(Ok(Ok(res_offset))) = (res.headers().get("Upload-Offset")) - .map(HeaderValue::to_str) - .map(|v| v.map(str::parse::)) - else { - return ChunkState::Err(self, UploadError::ResponseOffset); - }; - - // convert chunk.len() into an `u64` - // - // BOOKMARK this might panic on platforms where `usize` > 64 bit. - // Also whoa, you've sent more than 2 EiB ... in ONE chunk. - // Maybe just chill? - let chunk_len = u64::try_from(chunk.len()) - .unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", chunk.len(), e)); - - if self.offset + chunk_len != res_offset { - return ChunkState::Err(self, UploadError::ResponseOffset); - } - - self.offset = res_offset; - - if self.offset == self.size { - return ChunkState::Finished(self.path); - } - - ChunkState::Ok(self) } } impl<'t> SharryFile<'t> for FileUploading { - fn into_path(self) -> PathBuf { - self.path - } - + /// get a reference to the file's name + /// + /// Uses `SharryFile::extract_file_name`, which may **panic**! fn get_name(&'t self) -> &'t str { ::extract_file_name(&self.path) } diff --git a/src/sharry/mod.rs b/src/sharry/mod.rs index 36ed161..2f092d3 100644 --- a/src/sharry/mod.rs +++ b/src/sharry/mod.rs @@ -6,4 +6,4 @@ mod file; pub use api::{NewShareRequest, Uri}; pub use client::{Client, ClientError}; -pub use file::{ChunkState, FileChecked, FileUploading, SharryFile, UploadError}; +pub use file::{FileChecked, FileUploading, SharryFile}; From d607380659514d4ecb8622e0ab1d76b36d504dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:20:52 +0000 Subject: [PATCH 04/14] [wip] impl `Client` for `ureq::Agent` - factored out `file` module - renamed some stuff - decoupled `sharry::client` from `file` --- src/appstate.rs | 28 +++--- src/cli.rs | 11 ++- src/{sharry => }/file/checked.rs | 27 +++-- src/{sharry => }/file/mod.rs | 11 +-- src/{sharry => }/file/uploading.rs | 18 ++-- src/main.rs | 3 +- src/sharry/api.rs | 14 +-- src/sharry/client.rs | 154 +++++++++++++---------------- src/sharry/mod.rs | 6 +- 9 files changed, 134 insertions(+), 138 deletions(-) rename src/{sharry => }/file/checked.rs (60%) rename src/{sharry => }/file/mod.rs (74%) rename src/{sharry => }/file/uploading.rs (83%) diff --git a/src/appstate.rs b/src/appstate.rs index 4f68f5d..7326608 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -13,7 +13,8 @@ use serde::{Deserialize, Serialize}; use super::{ cli::Cli, - sharry::{Client, ClientError, FileChecked, FileUploading, SharryFile, Uri}, + file::{self, FileTrait}, + sharry::{self, Client, Uri}, }; #[derive(Serialize, Deserialize, Debug)] @@ -31,8 +32,8 @@ pub struct AppState { #[derive(Serialize, Deserialize, Debug)] enum FileState { - C(FileChecked), - U(FileUploading), + C(file::Checked), + U(file::Uploading), } impl FileState { @@ -49,9 +50,12 @@ impl FileState { uri: &Uri, alias_id: &str, share_id: &str, - ) -> Result { + ) -> sharry::Result { match self { - FileState::C(checked) => http.sharry_file_create(uri, alias_id, share_id, checked), + FileState::C(checked) => { + let endpoint = &uri.endpoint(format!("alias/upload/{}/files/tus", share_id)); + checked.start_upload(http, endpoint, alias_id) + } FileState::U(uploading) => Ok(uploading), } } @@ -99,12 +103,16 @@ impl AppState { .ok() } - pub fn from_args(args: &Cli, http: &impl Client) -> Result { + pub fn from_args(args: &Cli, http: &impl Client) -> sharry::Result { let file_name = Self::cache_file(args); let uri = args.get_uri(); let alias_id = args.alias.clone(); - let share_id = http.sharry_share_create(&uri, &alias_id, args.get_share_request())?; + let share_id = http.share_create( + &uri.endpoint("alias/upload/new"), + &alias_id, + args.get_share_request(), + )?; let files: VecDeque<_> = args.files.clone().into_iter().map(FileState::C).collect(); @@ -126,11 +134,9 @@ impl AppState { &mut self, http: &ureq::Agent, chunk_size: usize, - ) -> Result, UploadError> { + ) -> sharry::Result> { let uploading = if let Some(state) = self.files.pop_front() { - state - .start_upload(http, &self.uri, &self.alias_id, &self.share_id) - .unwrap() // HACK unwrap + state.start_upload(http, &self.uri, &self.alias_id, &self.share_id)? } else { return Ok(None); }; diff --git a/src/cli.rs b/src/cli.rs index 5e853d1..070d9f7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,7 +5,10 @@ use std::{ use clap::{Parser, builder::PossibleValuesParser}; -use super::sharry::{FileChecked, NewShareRequest, Uri}; +use super::{ + file::Checked, + sharry::{NewShareRequest, Uri}, +}; #[derive(Parser, Debug, Hash)] #[command(version, about, long_about = None)] @@ -50,15 +53,15 @@ pub struct Cli { /// Files to upload to the new share #[arg(value_name = "FILE", required = true, value_parser = parse_sharry_file)] - pub files: Vec, + pub files: Vec, } fn parse_seconds(data: &str) -> Result { data.parse().or(Ok(0)).map(Duration::from_secs) } -fn parse_sharry_file(data: &str) -> Result { - FileChecked::new(data).map_err(|e| e.to_string()) +fn parse_sharry_file(data: &str) -> Result { + Checked::new(data).map_err(|e| e.to_string()) } impl Cli { diff --git a/src/sharry/file/checked.rs b/src/file/checked.rs similarity index 60% rename from src/sharry/file/checked.rs rename to src/file/checked.rs index a6ec6ba..16d16ca 100644 --- a/src/sharry/file/checked.rs +++ b/src/file/checked.rs @@ -5,15 +5,17 @@ use std::{ use serde::{Deserialize, Serialize}; -use super::SharryFile; +use crate::sharry; + +use super::{FileTrait, Uploading}; #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct FileChecked { - pub(super) path: PathBuf, - pub(super) size: u64, +pub struct Checked { + path: PathBuf, + size: u64, } -impl FileChecked { +impl Checked { pub fn new(value: impl AsRef) -> io::Result { let meta = fs::metadata(&value)?; if meta.is_file() { @@ -28,14 +30,25 @@ impl FileChecked { )) } } + + pub fn start_upload( + self, + client: &impl sharry::Client, + endpoint: &str, + alias_id: &str, + ) -> sharry::Result { + let patch_uri = client.file_create(endpoint, alias_id, self.get_name(), self.size)?; + + Ok(Uploading::new(self.path, self.size, patch_uri)) + } } -impl<'t> SharryFile<'t> for FileChecked { +impl<'t> FileTrait<'t> for Checked { /// get a reference to the file's name /// /// Uses `SharryFile::extract_file_name`, which may **panic**! fn get_name(&'t self) -> &'t str { - ::extract_file_name(&self.path) + ::extract_file_name(&self.path) } fn get_size(&self) -> u64 { diff --git a/src/sharry/file/mod.rs b/src/file/mod.rs similarity index 74% rename from src/sharry/file/mod.rs rename to src/file/mod.rs index 7ae4823..513d562 100644 --- a/src/sharry/file/mod.rs +++ b/src/file/mod.rs @@ -1,15 +1,12 @@ mod checked; mod uploading; -use std::{ - ffi::OsStr, - path::{Path, PathBuf}, -}; +use std::{ffi::OsStr, path::Path}; -pub use checked::FileChecked; -pub use uploading::FileUploading; +pub use checked::Checked; +pub use uploading::Uploading; -pub trait SharryFile<'t> { +pub trait FileTrait<'t> { /// extract the filename part of a `Path` reference /// /// # Panics diff --git a/src/sharry/file/uploading.rs b/src/file/uploading.rs similarity index 83% rename from src/sharry/file/uploading.rs rename to src/file/uploading.rs index 1bd9f8a..aded973 100644 --- a/src/sharry/file/uploading.rs +++ b/src/file/uploading.rs @@ -6,17 +6,17 @@ use std::{ use serde::{Deserialize, Serialize}; -use super::{FileChecked, SharryFile}; +use super::FileTrait; #[derive(Serialize, Deserialize, Debug)] -pub struct FileUploading { +pub struct Uploading { path: PathBuf, size: u64, patch_uri: String, offset: u64, } -impl fmt::Display for FileUploading { +impl fmt::Display for Uploading { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -28,11 +28,11 @@ impl fmt::Display for FileUploading { } } -impl FileUploading { - pub fn new(file: FileChecked, patch_uri: String) -> Self { +impl Uploading { + pub(super) fn new(path: PathBuf, size: u64, patch_uri: String) -> Self { Self { - path: file.path, - size: file.size, + path, + size, patch_uri, offset: 0, } @@ -74,12 +74,12 @@ impl FileUploading { } } -impl<'t> SharryFile<'t> for FileUploading { +impl<'t> FileTrait<'t> for Uploading { /// get a reference to the file's name /// /// Uses `SharryFile::extract_file_name`, which may **panic**! fn get_name(&'t self) -> &'t str { - ::extract_file_name(&self.path) + ::extract_file_name(&self.path) } fn get_size(&self) -> u64 { diff --git a/src/main.rs b/src/main.rs index 10379c5..8c15900 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod appstate; mod cli; +mod file; mod sharry; use std::{ @@ -69,7 +70,7 @@ fn main() { actual: _, expected: 403, } => Some("Alias ID"), - ClientError::FileIO(_) => Some("URL"), + // ClientError::FileIO(_) => Some("URL"), _ => None, } { info!("handling error: {e:?}"); diff --git a/src/sharry/api.rs b/src/sharry/api.rs index 079dcda..2b5ddb5 100644 --- a/src/sharry/api.rs +++ b/src/sharry/api.rs @@ -10,19 +10,19 @@ pub struct Uri { } impl Uri { - pub(super) fn get_endpoint(&self, endpoint: impl fmt::Display + fmt::Debug) -> String { - let uri = format!("{}/{}", self, endpoint); - debug!("endpoint uri: {uri:?}"); - - uri - } - pub fn with_protocol(protocol: impl Into, base_url: impl Into) -> Self { Self { protocol: protocol.into(), base_url: base_url.into(), } } + + pub fn endpoint(&self, endpoint: impl fmt::Display) -> String { + let uri = format!("{}/{}", self, endpoint); + debug!("endpoint: {uri:?}"); + + uri + } } impl fmt::Display for Uri { diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 26065f4..bd6a5ea 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -1,51 +1,32 @@ -use std::{error::Error, fmt::Display, io}; +use std::fmt; -use log::debug; +use log::{debug, trace}; use thiserror::Error; -use super::{ - api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri}, - file::{FileChecked, FileUploading, SharryFile}, -}; +use super::api::{NewShareRequest, NewShareResponse, NotifyShareResponse}; + +pub type Result = std::result::Result; pub trait Client { - fn sharry_share_create( - &self, - uri: &Uri, - alias_id: &str, - data: NewShareRequest, - ) -> Result; + fn share_create(&self, endpoint: &str, alias_id: &str, data: NewShareRequest) + -> Result; - fn sharry_share_notify( - &self, - uri: &Uri, - alias_id: &str, - share_id: &str, - ) -> Result<(), ClientError>; + fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()>; - fn sharry_file_create( + fn file_create( &self, - uri: &Uri, + endpoint: &str, alias_id: &str, - share_id: &str, file_name: &str, file_size: u64, - ) -> Result; + ) -> Result; - fn sharry_file_patch( - &self, - patch_uri: &str, - alias_id: &str, - offset: u64, - chunk: &[u8], - ) -> Result; + fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8]) + -> Result; } #[derive(Debug, Error)] pub enum ClientError { - #[error("file I/O error: {0}")] - FileIO(#[from] io::Error), - #[error("network request failed: {0}")] Request(String), @@ -60,19 +41,15 @@ pub enum ClientError { } impl ClientError { - fn req_err(msg: impl Display) -> Self { + fn req_err(msg: impl fmt::Display) -> Self { Self::Request(msg.to_string()) } - fn res_parse_err(msg: impl Display) -> Self { + fn res_parse_err(msg: impl fmt::Display) -> Self { Self::ResponseParsing(msg.to_string()) } - fn res_content_err(msg: impl Display) -> Self { - Self::ResponseContent(msg.to_string()) - } - - fn res_check_status(actual: T, expected: T) -> Result<(), Self> + fn res_check_status(actual: T, expected: T) -> Result<()> where T: Into + Eq, { @@ -88,75 +65,77 @@ impl ClientError { } impl Client for ureq::Agent { - fn sharry_share_create( + fn share_create( &self, - uri: &Uri, + endpoint: &str, alias_id: &str, data: NewShareRequest, - ) -> Result { - let res = { - let endpoint = uri.get_endpoint("alias/upload/new"); + ) -> Result { + // let endpoint = uri.get_endpoint("alias/upload/new"); - self.post(endpoint) - .header("Sharry-Alias", alias_id) - .send_json(data) - .map_err(|e| ClientError::req_err(e))? - .body_mut() - .read_json::() - .map_err(|e| ClientError::res_parse_err(e))? - }; + let mut res = self + .post(endpoint) + .header("Sharry-Alias", alias_id) + .send_json(data) + .map_err(ClientError::req_err)?; - debug!("response: {res:?}"); + trace!("{endpoint:?} response: {res:?}"); + ClientError::res_check_status(res.status(), ureq::http::StatusCode::OK)?; + + let res = res + .body_mut() + .read_json::() + .map_err(ClientError::res_parse_err)?; + + debug!("{res:?}"); if res.success && (res.message == "Share created.") { Ok(res.id) } else { - Err(ClientError::res_content_err(format!("{res:?}"))) + Err(ClientError::ResponseContent(format!("{res:?}"))) } } - fn sharry_share_notify( - &self, - uri: &Uri, - alias_id: &str, - share_id: &str, - ) -> Result<(), ClientError> { - let res = { - let endpoint = uri.get_endpoint(format!("alias/mail/notify/{}", share_id)); + fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> { + // let endpoint = uri.get_endpoint(format!("alias/mail/notify/{}", share_id)); - self.post(endpoint) - .header("Sharry-Alias", alias_id) - .send_empty() - .map_err(|e| ClientError::req_err(e))? - .body_mut() - .read_json::() - .map_err(|e| ClientError::res_parse_err(e))? - }; + let mut res = self + .post(endpoint) + .header("Sharry-Alias", alias_id) + .send_empty() + .map_err(|e| ClientError::req_err(e))?; - debug!("response: {res:?}"); + trace!("{endpoint:?} response: {res:?}"); + ClientError::res_check_status(res.status(), ureq::http::StatusCode::OK)?; + + let res = res + .body_mut() + .read_json::() + .map_err(|e| ClientError::res_parse_err(e))?; + + debug!("{res:?}"); Ok(()) } - fn sharry_file_create( + fn file_create( &self, - uri: &Uri, + endpoint: &str, alias_id: &str, - share_id: &str, file_name: &str, file_size: u64, - ) -> Result { - let res = { - let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id)); + ) -> Result { + // let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id)); - self.post(endpoint) - .header("Sharry-Alias", alias_id) - .header("Sharry-File-Name", file_name) - .header("Upload-Length", file_size) - .send_empty() - .map_err(ClientError::req_err)? - }; + let res = self + .post(endpoint) + .header("Sharry-Alias", alias_id) + .header("Sharry-File-Name", file_name) + .header("Upload-Length", file_size) + .send_empty() + .map_err(ClientError::req_err)?; + trace!("{endpoint:?} response: {res:?}"); ClientError::res_check_status(res.status(), ureq::http::StatusCode::CREATED)?; let location = (res.headers().get("Location")) @@ -165,18 +144,18 @@ impl Client for ureq::Agent { .map_err(ClientError::res_parse_err)? .to_string(); - debug!("patch uri: {location}"); + debug!("{location:?}"); Ok(location) } - fn sharry_file_patch( + fn file_patch( &self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8], - ) -> Result { + ) -> Result { let res = self .patch(patch_uri) .header("Sharry-Alias", alias_id) @@ -184,6 +163,7 @@ impl Client for ureq::Agent { .send(chunk) .map_err(ClientError::req_err)?; + trace!("{patch_uri:?} response: {res:?}"); ClientError::res_check_status(res.status(), ureq::http::StatusCode::NO_CONTENT)?; let res_offset = (res.headers().get("Upload-Offset")) diff --git a/src/sharry/mod.rs b/src/sharry/mod.rs index 2f092d3..e230f9d 100644 --- a/src/sharry/mod.rs +++ b/src/sharry/mod.rs @@ -1,9 +1,5 @@ -#![allow(unused_imports)] - mod api; mod client; -mod file; pub use api::{NewShareRequest, Uri}; -pub use client::{Client, ClientError}; -pub use file::{FileChecked, FileUploading, SharryFile}; +pub use client::{Client, ClientError, Result}; From dc2a330d5810827e24160611bc64ca48cdcab62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:39:08 +0000 Subject: [PATCH 05/14] [wip] impl `Client` for `ureq::Agent` - Chunk implementation --- src/appstate.rs | 54 ++++++++++++++++++++++++++++--------------- src/file/chunk.rs | 36 +++++++++++++++++++++++++++++ src/file/mod.rs | 2 ++ src/file/uploading.rs | 21 ++++------------- src/main.rs | 2 +- src/sharry/client.rs | 19 +++++---------- 6 files changed, 85 insertions(+), 49 deletions(-) create mode 100644 src/file/chunk.rs diff --git a/src/appstate.rs b/src/appstate.rs index 7326608..04ac109 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; use super::{ cli::Cli, file::{self, FileTrait}, - sharry::{self, Client, Uri}, + sharry::{self, Client, ClientError, Uri}, }; #[derive(Serialize, Deserialize, Debug)] @@ -23,6 +23,8 @@ pub struct AppState { file_name: PathBuf, #[serde(skip)] progress: Option, + #[serde(skip)] + buffer: Vec, uri: Uri, alias_id: String, @@ -94,6 +96,7 @@ impl AppState { Self { file_name, progress: None, + buffer: Vec::with_capacity(args.chunk_size), uri: state.uri, alias_id: state.alias_id, share_id: state.share_id, @@ -119,6 +122,7 @@ impl AppState { Ok(Self { file_name, progress: None, + buffer: Vec::with_capacity(args.chunk_size), uri, alias_id, share_id, @@ -130,18 +134,16 @@ impl AppState { self.files.iter().map(FileState::file_name).collect() } - pub fn upload_chunk( - &mut self, - http: &ureq::Agent, - chunk_size: usize, - ) -> sharry::Result> { - let uploading = if let Some(state) = self.files.pop_front() { - state.start_upload(http, &self.uri, &self.alias_id, &self.share_id)? + pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result> { + let mut uploading = if let Some(state) = self.files.pop_front() { + state + .start_upload(http, &self.uri, &self.alias_id, &self.share_id) + .unwrap() // HACK unwrap } else { return Ok(None); }; - debug!("{uploading} chunk {chunk_size}"); + debug!("{uploading} chunk {}", self.buffer.len()); // Initialize or fetch the existing ProgressBar in one call: let bar = &*self.progress.get_or_insert_with(|| { @@ -165,21 +167,35 @@ impl AppState { bar }); - match uploading.upload_chunk(http, &self.alias_id, chunk_size) { - ChunkState::Ok(upl) => { - bar.set_position(upl.get_offset()); - self.files.push_front(FileState::U(upl)); + let chunk = uploading + .read(&mut self.buffer) + .map_err(ClientError::req_err)?; + if chunk.get_length() == 0 { + return Err(ClientError::req_err("wtf")); + } + + http.file_patch( + chunk.get_patch_uri(), + &self.alias_id, + chunk.get_offset(), + chunk.get_data(), + )?; + + match uploading.check_eof() { + Ok(uploading) => { + bar.set_position(uploading.get_offset()); + self.files.push_front(FileState::U(uploading)); Ok(Some(())) } - ChunkState::Err(upl, e) => { - self.files.push_front(FileState::U(upl)); - Err(e) - } - ChunkState::Finished(path) => { + Err(path) => { debug!("Finished {:?}!", path.display()); bar.finish(); self.progress = None; - self.share_id.notify(http, &self.alias_id).unwrap(); // HACK unwrap + + let endpoint = self + .uri + .endpoint(format!("alias/mail/notify/{}", self.share_id)); + http.share_notify(&endpoint, &self.alias_id).unwrap(); // HACK unwrap Ok(self.files.front().map(drop)) } diff --git a/src/file/chunk.rs b/src/file/chunk.rs new file mode 100644 index 0000000..bf57c93 --- /dev/null +++ b/src/file/chunk.rs @@ -0,0 +1,36 @@ +pub struct Chunk<'t> { + data: &'t [u8], + patch_uri: &'t str, + offset: u64, +} + +impl<'t> Chunk<'t> { + pub fn new(data: &'t [u8], patch_uri: &'t str, offset: u64) -> Self { + Self { + data, + patch_uri, + offset, + } + } + + pub fn get_data(&self) -> &[u8] { + self.data + } + + pub fn get_length(&self) -> u64 { + let len = self.data.len(); + + // BOOKMARK this might **panic** on platforms where `usize` has more than 64 bit. + // Also, you've allocated more than 2 EiB ... in ONE chunk. + // Whoa! Maybe just chill? + u64::try_from(len).unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", len, e)) + } + + pub fn get_patch_uri(&self) -> &str { + self.patch_uri + } + + pub fn get_offset(&self) -> u64 { + self.offset + } +} diff --git a/src/file/mod.rs b/src/file/mod.rs index 513d562..4d508e6 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -1,9 +1,11 @@ mod checked; +mod chunk; mod uploading; use std::{ffi::OsStr, path::Path}; pub use checked::Checked; +pub use chunk::Chunk; pub use uploading::Uploading; pub trait FileTrait<'t> { diff --git a/src/file/uploading.rs b/src/file/uploading.rs index aded973..6ce0480 100644 --- a/src/file/uploading.rs +++ b/src/file/uploading.rs @@ -6,7 +6,7 @@ use std::{ use serde::{Deserialize, Serialize}; -use super::FileTrait; +use super::{Chunk, FileTrait}; #[derive(Serialize, Deserialize, Debug)] pub struct Uploading { @@ -38,31 +38,20 @@ impl Uploading { } } - pub fn get_patch_uri(&self) -> &str { - &self.patch_uri - } - pub fn get_offset(&self) -> u64 { self.offset } - pub fn read(&mut self, buf: &mut [u8]) -> io::Result { + pub fn read<'t>(&'t mut self, buf: &'t mut [u8]) -> io::Result> { let mut f = fs::File::open(&self.path)?; f.seek(SeekFrom::Start(self.offset))?; let read_len = f.read(buf)?; - // convert into `u64` - // - // BOOKMARK this might **panic** on platforms where `usize` has more than 64 bit. - // Also, you're reading more than 2 EiB ... in ONE chunk. - // Whoa! Maybe just chill? - let read_len = u64::try_from(read_len) - .unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", read_len, e)); + let chunk = Chunk::new(&buf[..read_len], &self.patch_uri, self.offset); + self.offset += chunk.get_length(); - self.offset += read_len; - - Ok(read_len) + Ok(chunk) } pub fn check_eof(self) -> Result { diff --git a/src/main.rs b/src/main.rs index 8c15900..17d53c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,7 +99,7 @@ fn main() { info!("continuing with state: {state:?}"); loop { - match state.upload_chunk(&agent, args.chunk_size * 1024 * 1024) { + match state.upload_chunk(&agent) { Err(e) => error!("error: {e:?}"), Ok(None) => { info!("all uploads done"); diff --git a/src/sharry/client.rs b/src/sharry/client.rs index bd6a5ea..8c32799 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -21,8 +21,7 @@ pub trait Client { file_size: u64, ) -> Result; - fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8]) - -> Result; + fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>; } #[derive(Debug, Error)] @@ -41,15 +40,15 @@ pub enum ClientError { } impl ClientError { - fn req_err(msg: impl fmt::Display) -> Self { + pub fn req_err(msg: impl fmt::Display) -> Self { Self::Request(msg.to_string()) } - fn res_parse_err(msg: impl fmt::Display) -> Self { + pub fn res_parse_err(msg: impl fmt::Display) -> Self { Self::ResponseParsing(msg.to_string()) } - fn res_check_status(actual: T, expected: T) -> Result<()> + pub fn res_check_status(actual: T, expected: T) -> Result<()> where T: Into + Eq, { @@ -149,13 +148,7 @@ impl Client for ureq::Agent { Ok(location) } - fn file_patch( - &self, - patch_uri: &str, - alias_id: &str, - offset: u64, - chunk: &[u8], - ) -> Result { + fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()> { let res = self .patch(patch_uri) .header("Sharry-Alias", alias_id) @@ -177,7 +170,7 @@ impl Client for ureq::Agent { let chunk_len = u64::try_from(chunk.len()).expect("something's VERY wrong"); if offset + chunk_len == res_offset { - Ok(res_offset) + Ok(()) } else { Err(ClientError::ResponseContent(format!( "Unexpected Upload-Offset: {} (expected {})", From 099367964119afef2f1c958b371f2a4ce12bdaea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:06:08 +0000 Subject: [PATCH 06/14] [wip] impl `Client` for `ureq::Agent` - clippy fix --- src/appstate.rs | 2 +- src/file/chunk.rs | 2 +- src/sharry/api.rs | 2 +- src/sharry/client.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/appstate.rs b/src/appstate.rs index 04ac109..dbec49b 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -55,7 +55,7 @@ impl FileState { ) -> sharry::Result { match self { FileState::C(checked) => { - let endpoint = &uri.endpoint(format!("alias/upload/{}/files/tus", share_id)); + let endpoint = &uri.endpoint(format!("alias/upload/{share_id}/files/tus")); checked.start_upload(http, endpoint, alias_id) } FileState::U(uploading) => Ok(uploading), diff --git a/src/file/chunk.rs b/src/file/chunk.rs index bf57c93..838e66b 100644 --- a/src/file/chunk.rs +++ b/src/file/chunk.rs @@ -23,7 +23,7 @@ impl<'t> Chunk<'t> { // BOOKMARK this might **panic** on platforms where `usize` has more than 64 bit. // Also, you've allocated more than 2 EiB ... in ONE chunk. // Whoa! Maybe just chill? - u64::try_from(len).unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", len, e)) + u64::try_from(len).unwrap_or_else(|e| panic!("usize={len} did not fit into u64: {e}")) } pub fn get_patch_uri(&self) -> &str { diff --git a/src/sharry/api.rs b/src/sharry/api.rs index 2b5ddb5..9f51932 100644 --- a/src/sharry/api.rs +++ b/src/sharry/api.rs @@ -18,7 +18,7 @@ impl Uri { } pub fn endpoint(&self, endpoint: impl fmt::Display) -> String { - let uri = format!("{}/{}", self, endpoint); + let uri = format!("{self}/{endpoint}"); debug!("endpoint: {uri:?}"); uri diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 8c32799..e857faa 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -102,7 +102,7 @@ impl Client for ureq::Agent { .post(endpoint) .header("Sharry-Alias", alias_id) .send_empty() - .map_err(|e| ClientError::req_err(e))?; + .map_err(ClientError::req_err)?; trace!("{endpoint:?} response: {res:?}"); ClientError::res_check_status(res.status(), ureq::http::StatusCode::OK)?; @@ -110,7 +110,7 @@ impl Client for ureq::Agent { let res = res .body_mut() .read_json::() - .map_err(|e| ClientError::res_parse_err(e))?; + .map_err(ClientError::res_parse_err)?; debug!("{res:?}"); From 592e7bf76e263abb0889131d60474005b17052e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:28:02 +0000 Subject: [PATCH 07/14] [wip] impl `Client` for `ureq::Agent` - `AppState` creation --- src/appstate.rs | 76 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/src/appstate.rs b/src/appstate.rs index dbec49b..db8c64b 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -1,6 +1,6 @@ use std::{ collections::VecDeque, - fs, + fmt, fs, io::{self, Write}, path::{Path, PathBuf}, time::Duration, @@ -17,7 +17,7 @@ use super::{ sharry::{self, Client, ClientError, Uri}, }; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize)] pub struct AppState { #[serde(skip)] file_name: PathBuf, @@ -32,6 +32,18 @@ pub struct AppState { files: VecDeque, } +impl fmt::Debug for AppState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AppState") + .field("file_name", &self.file_name) + .field("uri", &self.uri) + .field("alias_id", &self.alias_id) + .field("share_id", &self.share_id) + .field("files", &self.files) + .finish() + } +} + #[derive(Serialize, Deserialize, Debug)] enum FileState { C(file::Checked), @@ -80,29 +92,48 @@ impl AppState { file_name } - fn load(file_name: impl AsRef) -> io::Result { - let content = fs::read_to_string(file_name)?; - serde_json::from_str(&content).map_err(io::Error::other) + fn new( + file_name: PathBuf, + chunk_size: usize, + uri: Uri, + alias_id: String, + share_id: String, + files: VecDeque, + ) -> Self { + Self { + file_name, + progress: None, + buffer: vec![0; chunk_size * 1024 * 1024], + uri, + alias_id, + share_id, + files, + } + } + + fn load(file_name: &Path, chunk_size: usize) -> io::Result { + let file = fs::File::open(file_name)?; + serde_json::from_reader(io::BufReader::new(file)) + .map_err(io::Error::other) + .map(|state: Self| { + debug!("successfully loaded AppState"); + + Self::new( + file_name.to_owned(), + chunk_size, + state.uri, + state.alias_id, + state.share_id, + state.files, + ) + }) } pub fn try_resume(args: &Cli) -> Option { let file_name = Self::cache_file(args); - Self::load(&file_name) + Self::load(&file_name, args.chunk_size) .inspect_err(|e| debug!("could not resume from {:?}: {e}", file_name.display())) - .map(|state| { - debug!("successfully loaded AppState"); - - Self { - file_name, - progress: None, - buffer: Vec::with_capacity(args.chunk_size), - uri: state.uri, - alias_id: state.alias_id, - share_id: state.share_id, - files: state.files, - } - }) .ok() } @@ -119,15 +150,14 @@ impl AppState { let files: VecDeque<_> = args.files.clone().into_iter().map(FileState::C).collect(); - Ok(Self { + Ok(Self::new( file_name, - progress: None, - buffer: Vec::with_capacity(args.chunk_size), + args.chunk_size, uri, alias_id, share_id, files, - }) + )) } pub fn file_names(&self) -> Vec<&str> { From 9b1f7f872cbfe1db799e84cdb2d2dc05fd9acd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Wed, 11 Jun 2025 18:17:16 +0000 Subject: [PATCH 08/14] [wip] impl `Client` for `ureq::Agent` - minor refactorings --- src/main.rs | 38 +++++++++++++++++++++++--------------- src/sharry/client.rs | 38 +++++++++++++++++++++++++------------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/main.rs b/src/main.rs index 17d53c9..d06addd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod file; mod sharry; use std::{ - process::exit, + process::{self, exit}, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -22,22 +22,30 @@ use cli::Cli; use sharry::ClientError; fn main() { + env_logger::init(); + println!( "{} to {}!", style("Welcome").magenta().bold(), style("ShrUpl").yellow().bold(), ); - let stop = Arc::new(AtomicBool::new(false)); + let check_ctrlc = { + let stop = Arc::new(AtomicBool::new(false)); - let stop_ctrlc = stop.clone(); - ctrlc::set_handler(move || { - stop_ctrlc.store(true, Ordering::SeqCst); - info!("stopping as soon as possible ..."); - }) - .expect("Error setting Ctrl-C handler"); + let stop_ctrlc = stop.clone(); + ctrlc::set_handler(move || { + stop_ctrlc.store(true, Ordering::SeqCst); + info!("stopping as soon as possible ..."); + }) + .expect("Error setting Ctrl-C handler"); - env_logger::init(); + move || { + if stop.load(Ordering::SeqCst) { + process::exit(1); + } + } + }; let args = Cli::parse(); info!("args: {args:?}"); @@ -57,7 +65,7 @@ fn main() { .map_or(None, |b| b.then_some(state)) }) .unwrap_or_else(|| { - stop.load(Ordering::SeqCst).then(|| exit(0)); + check_ctrlc(); match AppState::from_args(&args, &agent) { Ok(state) => { @@ -67,8 +75,8 @@ fn main() { Err(e) => { if let Some(cause) = match e { ClientError::ResponseStatus { - actual: _, - expected: 403, + actual: 403, + expected: _, } => Some("Alias ID"), // ClientError::FileIO(_) => Some("URL"), _ => None, @@ -77,8 +85,8 @@ fn main() { println!( "{} probably wrong: {} – {:?}", style("Error!").red().bold(), - style(cause).cyan().italic(), - style(e.to_string()).yellow() + style(cause).cyan(), + style(e.to_string()).yellow().italic() ); } else { error!("unknown error: {e} – {e:?}"); @@ -110,6 +118,6 @@ fn main() { } state.save().unwrap(); // HACK unwrap - stop.load(Ordering::SeqCst).then(|| exit(0)); + check_ctrlc(); } } diff --git a/src/sharry/client.rs b/src/sharry/client.rs index e857faa..7656770 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -32,8 +32,8 @@ pub enum ClientError { #[error("response parsing failed: {0}")] ResponseParsing(String), - #[error("unexpected response status: {actual} (expected {expected})")] - ResponseStatus { actual: u16, expected: u16 }, + #[error("unexpected response status: {actual} (expected {expected:?})")] + ResponseStatus { actual: u16, expected: Option }, #[error("unexpected response content: {0}")] ResponseContent(String), @@ -48,21 +48,33 @@ impl ClientError { Self::ResponseParsing(msg.to_string()) } - pub fn res_check_status(actual: T, expected: T) -> Result<()> + pub fn res_status_check(actual: T, expected: T) -> Result<()> where - T: Into + Eq, + T: PartialEq + Into + Copy, { if actual == expected { Ok(()) } else { Err(Self::ResponseStatus { actual: actual.into(), - expected: expected.into(), + expected: Some(expected.into()), }) } } } +impl From for ClientError { + fn from(value: ureq::Error) -> Self { + match value { + ureq::Error::StatusCode(status) => ClientError::ResponseStatus { + actual: status, + expected: None, + }, + error => Self::Request(error.to_string()), + } + } +} + impl Client for ureq::Agent { fn share_create( &self, @@ -76,10 +88,10 @@ impl Client for ureq::Agent { .post(endpoint) .header("Sharry-Alias", alias_id) .send_json(data) - .map_err(ClientError::req_err)?; + .map_err(ClientError::from)?; trace!("{endpoint:?} response: {res:?}"); - ClientError::res_check_status(res.status(), ureq::http::StatusCode::OK)?; + ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?; let res = res .body_mut() @@ -102,10 +114,10 @@ impl Client for ureq::Agent { .post(endpoint) .header("Sharry-Alias", alias_id) .send_empty() - .map_err(ClientError::req_err)?; + .map_err(ClientError::from)?; trace!("{endpoint:?} response: {res:?}"); - ClientError::res_check_status(res.status(), ureq::http::StatusCode::OK)?; + ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?; let res = res .body_mut() @@ -132,10 +144,10 @@ impl Client for ureq::Agent { .header("Sharry-File-Name", file_name) .header("Upload-Length", file_size) .send_empty() - .map_err(ClientError::req_err)?; + .map_err(ClientError::from)?; trace!("{endpoint:?} response: {res:?}"); - ClientError::res_check_status(res.status(), ureq::http::StatusCode::CREATED)?; + ClientError::res_status_check(res.status(), ureq::http::StatusCode::CREATED)?; let location = (res.headers().get("Location")) .ok_or_else(|| ClientError::res_parse_err("Location header not found"))? @@ -154,10 +166,10 @@ impl Client for ureq::Agent { .header("Sharry-Alias", alias_id) .header("Upload-Offset", offset) .send(chunk) - .map_err(ClientError::req_err)?; + .map_err(ClientError::from)?; trace!("{patch_uri:?} response: {res:?}"); - ClientError::res_check_status(res.status(), ureq::http::StatusCode::NO_CONTENT)?; + ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?; let res_offset = (res.headers().get("Upload-Offset")) .ok_or_else(|| ClientError::res_parse_err("Upload-Offset header not found"))? From 67da081ef9e5fba010c1b7407e4b3d91f25bce44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Thu, 12 Jun 2025 00:54:26 +0000 Subject: [PATCH 09/14] [wip] impl `Client` for `ureq::Agent` - error handling refactoring --- src/main.rs | 49 +++++++++++++++++++++++--------------------- src/sharry/client.rs | 18 +++++++++------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index d06addd..e9925b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,28 @@ use appstate::AppState; use cli::Cli; use sharry::ClientError; +fn print_error(e: &ClientError) { + if let Some(cause) = match e { + ClientError::ResponseStatus { + actual: 403, + expected: _, + } => Some("Alias ID"), + ClientError::StdIo(_) => Some("URL"), + _ => None, + } { + info!("known error: {e:?}"); + println!( + "{} probably wrong: {}", + style("Error!").red().bold(), + style(cause).cyan(), + ); + println!("{}", style(e.to_string()).yellow().italic()); + } else { + error!("unknown error: {e} ({e:?})"); + println!("{}", style("Unknown Error!").red().bold()); + } +} + fn main() { env_logger::init(); @@ -32,8 +54,8 @@ fn main() { let check_ctrlc = { let stop = Arc::new(AtomicBool::new(false)); - let stop_ctrlc = stop.clone(); + ctrlc::set_handler(move || { stop_ctrlc.store(true, Ordering::SeqCst); info!("stopping as soon as possible ..."); @@ -73,39 +95,20 @@ fn main() { state } Err(e) => { - if let Some(cause) = match e { - ClientError::ResponseStatus { - actual: 403, - expected: _, - } => Some("Alias ID"), - // ClientError::FileIO(_) => Some("URL"), - _ => None, - } { - info!("handling error: {e:?}"); - println!( - "{} probably wrong: {} – {:?}", - style("Error!").red().bold(), - style(cause).cyan(), - style(e.to_string()).yellow().italic() - ); - } else { - error!("unknown error: {e} – {e:?}"); - println!("{}", style("Unknown Error!").red().bold()); - } - + print_error(&e); exit(1); } } }); + info!("continuing with state: {state:?}"); + println!( "{} uploading: {}", style("ShrUpl").yellow().bold(), style(state.file_names().join(", ")).magenta(), ); - info!("continuing with state: {state:?}"); - loop { match state.upload_chunk(&agent) { Err(e) => error!("error: {e:?}"), diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 7656770..5299b02 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -26,15 +26,18 @@ pub trait Client { #[derive(Debug, Error)] pub enum ClientError { + #[error(transparent)] + StdIo(#[from] std::io::Error), + #[error("network request failed: {0}")] Request(String), + #[error("unexpected response status: {actual} (expected {expected})")] + ResponseStatus { actual: u16, expected: u16 }, + #[error("response parsing failed: {0}")] ResponseParsing(String), - #[error("unexpected response status: {actual} (expected {expected:?})")] - ResponseStatus { actual: u16, expected: Option }, - #[error("unexpected response content: {0}")] ResponseContent(String), } @@ -57,7 +60,7 @@ impl ClientError { } else { Err(Self::ResponseStatus { actual: actual.into(), - expected: Some(expected.into()), + expected: expected.into(), }) } } @@ -66,11 +69,12 @@ impl ClientError { impl From for ClientError { fn from(value: ureq::Error) -> Self { match value { - ureq::Error::StatusCode(status) => ClientError::ResponseStatus { + ureq::Error::StatusCode(status) => Self::ResponseStatus { actual: status, - expected: None, + expected: 200, }, - error => Self::Request(error.to_string()), + ureq::Error::Io(e) => e.into(), + error => Self::req_err(error), } } } From fb06725f0575e3a97809ea1116555819ef78895a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:26:11 +0000 Subject: [PATCH 10/14] [wip] impl `Client` for `ureq::Agent` - wip: factor out `SavedState` --- src/appstate.rs | 146 +++++++++------------------------------------- src/main.rs | 11 +++- src/savedstate.rs | 133 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 121 deletions(-) create mode 100644 src/savedstate.rs diff --git a/src/appstate.rs b/src/appstate.rs index db8c64b..1a585ab 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -1,10 +1,4 @@ -use std::{ - collections::VecDeque, - fmt, fs, - io::{self, Write}, - path::{Path, PathBuf}, - time::Duration, -}; +use std::{fmt, io, path::PathBuf, time::Duration}; use console::style; use indicatif::{ProgressBar, ProgressStyle}; @@ -13,68 +7,29 @@ use serde::{Deserialize, Serialize}; use super::{ cli::Cli, - file::{self, FileTrait}, - sharry::{self, Client, ClientError, Uri}, + file::FileTrait, + savedstate::SavedState, + sharry::{self, Client, ClientError}, }; #[derive(Serialize, Deserialize)] pub struct AppState { - #[serde(skip)] - file_name: PathBuf, #[serde(skip)] progress: Option, #[serde(skip)] buffer: Vec, - uri: Uri, - alias_id: String, - share_id: String, - files: VecDeque, + inner: SavedState, } impl fmt::Debug for AppState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AppState") - .field("file_name", &self.file_name) - .field("uri", &self.uri) - .field("alias_id", &self.alias_id) - .field("share_id", &self.share_id) - .field("files", &self.files) + .field("inner", &self.inner) .finish() } } -#[derive(Serialize, Deserialize, Debug)] -enum FileState { - C(file::Checked), - U(file::Uploading), -} - -impl FileState { - fn file_name(&self) -> &str { - match self { - FileState::C(checked) => checked.get_name(), - FileState::U(uploading) => uploading.get_name(), - } - } - - fn start_upload( - self, - http: &impl Client, - uri: &Uri, - alias_id: &str, - share_id: &str, - ) -> sharry::Result { - match self { - FileState::C(checked) => { - let endpoint = &uri.endpoint(format!("alias/upload/{share_id}/files/tus")); - checked.start_upload(http, endpoint, alias_id) - } - FileState::U(uploading) => Ok(uploading), - } - } -} - impl AppState { fn cache_dir() -> PathBuf { let dir_name = dirs_next::cache_dir() @@ -92,90 +47,55 @@ impl AppState { file_name } - fn new( - file_name: PathBuf, - chunk_size: usize, - uri: Uri, - alias_id: String, - share_id: String, - files: VecDeque, - ) -> Self { + fn new(chunk_size: usize, inner: SavedState) -> Self { Self { - file_name, progress: None, buffer: vec![0; chunk_size * 1024 * 1024], - uri, - alias_id, - share_id, - files, + inner, } } - fn load(file_name: &Path, chunk_size: usize) -> io::Result { - let file = fs::File::open(file_name)?; - serde_json::from_reader(io::BufReader::new(file)) - .map_err(io::Error::other) - .map(|state: Self| { - debug!("successfully loaded AppState"); - - Self::new( - file_name.to_owned(), - chunk_size, - state.uri, - state.alias_id, - state.share_id, - state.files, - ) - }) - } - pub fn try_resume(args: &Cli) -> Option { let file_name = Self::cache_file(args); - - Self::load(&file_name, args.chunk_size) + let inner = SavedState::load(&file_name) .inspect_err(|e| debug!("could not resume from {:?}: {e}", file_name.display())) - .ok() + .ok()?; + + Some(Self::new(args.chunk_size, inner)) } pub fn from_args(args: &Cli, http: &impl Client) -> sharry::Result { - let file_name = Self::cache_file(args); - let uri = args.get_uri(); - let alias_id = args.alias.clone(); let share_id = http.share_create( &uri.endpoint("alias/upload/new"), - &alias_id, + &args.alias, args.get_share_request(), )?; - let files: VecDeque<_> = args.files.clone().into_iter().map(FileState::C).collect(); - Ok(Self::new( - file_name, args.chunk_size, - uri, - alias_id, - share_id, - files, + SavedState::new( + Self::cache_file(&args), + uri, + args.alias.clone(), + share_id, + &args.files, + ), )) } pub fn file_names(&self) -> Vec<&str> { - self.files.iter().map(FileState::file_name).collect() + self.inner.file_names() } pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result> { - let mut uploading = if let Some(state) = self.files.pop_front() { - state - .start_upload(http, &self.uri, &self.alias_id, &self.share_id) - .unwrap() // HACK unwrap - } else { + let Some(mut uploading) = self.inner.pop_file(http) else { return Ok(None); }; debug!("{uploading} chunk {}", self.buffer.len()); - // Initialize or fetch the existing ProgressBar in one call: + // Initialize or fetch the existing ProgressBar let bar = &*self.progress.get_or_insert_with(|| { // Create a new bar with style let bar = ProgressBar::new(uploading.get_size()) @@ -199,7 +119,7 @@ impl AppState { let chunk = uploading .read(&mut self.buffer) - .map_err(ClientError::req_err)?; + .map_err(ClientError::from)?; if chunk.get_length() == 0 { return Err(ClientError::req_err("wtf")); } @@ -214,7 +134,7 @@ impl AppState { match uploading.check_eof() { Ok(uploading) => { bar.set_position(uploading.get_offset()); - self.files.push_front(FileState::U(uploading)); + self.inner.push_file(uploading); Ok(Some(())) } Err(path) => { @@ -227,26 +147,16 @@ impl AppState { .endpoint(format!("alias/mail/notify/{}", self.share_id)); http.share_notify(&endpoint, &self.alias_id).unwrap(); // HACK unwrap - Ok(self.files.front().map(drop)) + Ok(self.inner.has_file().then_some(())) } } } pub fn save(&self) -> io::Result<()> { - fs::create_dir_all(Self::cache_dir())?; - - let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?; - let mut file = fs::File::create(&self.file_name)?; - file.write_all(json.as_bytes())?; - - trace!("updated {:?}", self.file_name.display()); - Ok(()) + self.inner.save() } pub fn clear(self) -> io::Result<()> { - fs::remove_file(&self.file_name)?; - - trace!("removed {:?}", self.file_name.display()); - Ok(()) + self.inner.clear() } } diff --git a/src/main.rs b/src/main.rs index e9925b1..7b5e8a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ mod appstate; mod cli; mod file; +mod savedstate; mod sharry; use std::{ - process::{self, exit}, + process, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -23,13 +24,16 @@ use sharry::ClientError; fn print_error(e: &ClientError) { if let Some(cause) = match e { + // known errors ClientError::ResponseStatus { actual: 403, expected: _, } => Some("Alias ID"), ClientError::StdIo(_) => Some("URL"), + // unknown error _ => None, } { + // handle known error info!("known error: {e:?}"); println!( "{} probably wrong: {}", @@ -38,6 +42,7 @@ fn print_error(e: &ClientError) { ); println!("{}", style(e.to_string()).yellow().italic()); } else { + // handle unknown error error!("unknown error: {e} ({e:?})"); println!("{}", style("Unknown Error!").red().bold()); } @@ -64,7 +69,7 @@ fn main() { move || { if stop.load(Ordering::SeqCst) { - process::exit(1); + process::exit(255); } } }; @@ -96,7 +101,7 @@ fn main() { } Err(e) => { print_error(&e); - exit(1); + process::exit(1); } } }); diff --git a/src/savedstate.rs b/src/savedstate.rs new file mode 100644 index 0000000..cb392c1 --- /dev/null +++ b/src/savedstate.rs @@ -0,0 +1,133 @@ +use std::{ + collections::VecDeque, + fs, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use log::trace; +use serde::{Deserialize, Serialize}; + +use super::{ + file::{self, FileTrait}, + sharry::{self, Client, Uri}, +}; + +#[derive(Serialize, Deserialize, Debug)] +enum FileState { + C(file::Checked), + U(file::Uploading), +} + +impl FileState { + fn file_name(&self) -> &str { + match self { + FileState::C(c) => c.get_name(), + FileState::U(u) => u.get_name(), + } + } + + fn start_upload( + self, + http: &impl Client, + uri: &Uri, + alias_id: &str, + share_id: &str, + ) -> sharry::Result { + match self { + FileState::C(checked) => { + let endpoint = &uri.endpoint(format!("alias/upload/{share_id}/files/tus")); + checked.start_upload(http, endpoint, alias_id) + } + FileState::U(uploading) => Ok(uploading), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SavedState { + #[serde(skip)] + file_name: PathBuf, + + uri: Uri, + alias_id: String, + share_id: String, + files: VecDeque, +} + +impl SavedState { + pub fn new( + file_name: PathBuf, + uri: Uri, + alias_id: String, + share_id: String, + files: &Vec, + ) -> Self { + Self { + file_name, + uri, + alias_id, + share_id, + files: files.clone().into_iter().map(FileState::C).collect(), + } + } + + pub fn load(file_name: &Path) -> io::Result { + let file = fs::File::open(file_name)?; + let state: Self = + serde_json::from_reader(io::BufReader::new(file)).map_err(io::Error::other)?; + + Ok(Self { + file_name: file_name.to_owned(), + uri: state.uri, + alias_id: state.alias_id, + share_id: state.share_id, + files: state.files, + }) + } + + pub fn file_names(&self) -> Vec<&str> { + self.files.iter().map(FileState::file_name).collect() + } + + pub fn has_file(&self) -> bool { + !self.files.is_empty() + } + + pub fn pop_file(&mut self, http: &impl Client) -> Option { + if let Some(state) = self.files.pop_front() { + Some( + state + .start_upload(http, &self.uri, &self.alias_id, &self.share_id) + .unwrap(), + ) // HACK unwrap + } else { + None + } + } + + pub fn push_file(&mut self, file: file::Uploading) { + self.files.push_front(FileState::U(file)); + } + + pub fn save(&self) -> io::Result<()> { + let cache_dir = self.file_name.parent().ok_or_else(|| { + io::Error::other(format!("orphan file {:?}", self.file_name.display())) + })?; + fs::create_dir_all(cache_dir)?; + + let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?; + let mut file = fs::File::create(&self.file_name)?; + file.write_all(json.as_bytes())?; + + trace!("updated {:?}", self.file_name.display()); + Ok(()) + } + + pub fn clear(self) -> io::Result<()> { + fs::remove_file(&self.file_name)?; + + trace!("removed {:?}", self.file_name.display()); + Ok(()) + } +} From ed10f269c85f0056f503c5bc33f859c7674d79c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:07:54 +0000 Subject: [PATCH 11/14] [wip] impl `Client` for `ureq::Agent` - wip: factor out `SavedState` --- src/appstate.rs | 61 +++++-------------- src/{savedstate.rs => cachefile.rs} | 90 +++++++++++++++++------------ src/main.rs | 2 +- 3 files changed, 69 insertions(+), 84 deletions(-) rename src/{savedstate.rs => cachefile.rs} (54%) diff --git a/src/appstate.rs b/src/appstate.rs index 1a585ab..8cbc2e5 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -1,25 +1,21 @@ -use std::{fmt, io, path::PathBuf, time::Duration}; +use std::{fmt, io, time::Duration}; use console::style; use indicatif::{ProgressBar, ProgressStyle}; -use log::{debug, trace}; -use serde::{Deserialize, Serialize}; +use log::debug; use super::{ + cachefile::CacheFile, cli::Cli, file::FileTrait, - savedstate::SavedState, sharry::{self, Client, ClientError}, }; -#[derive(Serialize, Deserialize)] pub struct AppState { - #[serde(skip)] progress: Option, - #[serde(skip)] buffer: Vec, - inner: SavedState, + inner: CacheFile, } impl fmt::Debug for AppState { @@ -31,23 +27,7 @@ impl fmt::Debug for AppState { } impl AppState { - fn cache_dir() -> PathBuf { - let dir_name = dirs_next::cache_dir() - .expect("could not determine cache directory") - .join("shrupl"); - - trace!("cachedir: {:?}", dir_name.display()); - dir_name - } - - fn cache_file(args: &Cli) -> PathBuf { - let file_name = Self::cache_dir().join(format!("{}.json", args.get_hash())); - - trace!("cachefile: {:?}", file_name.display()); - file_name - } - - fn new(chunk_size: usize, inner: SavedState) -> Self { + fn new(chunk_size: usize, inner: CacheFile) -> Self { Self { progress: None, buffer: vec![0; chunk_size * 1024 * 1024], @@ -56,38 +36,26 @@ impl AppState { } pub fn try_resume(args: &Cli) -> Option { - let file_name = Self::cache_file(args); - let inner = SavedState::load(&file_name) - .inspect_err(|e| debug!("could not resume from {:?}: {e}", file_name.display())) + let inner = CacheFile::try_resume(args) + .inspect_err(|e| debug!("could not resume from hash {:?}: {e}", args.get_hash())) .ok()?; Some(Self::new(args.chunk_size, inner)) } pub fn from_args(args: &Cli, http: &impl Client) -> sharry::Result { - let uri = args.get_uri(); let share_id = http.share_create( - &uri.endpoint("alias/upload/new"), + &args.get_uri().endpoint("alias/upload/new"), &args.alias, args.get_share_request(), )?; Ok(Self::new( args.chunk_size, - SavedState::new( - Self::cache_file(&args), - uri, - args.alias.clone(), - share_id, - &args.files, - ), + CacheFile::from_args(args, share_id), )) } - pub fn file_names(&self) -> Vec<&str> { - self.inner.file_names() - } - pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result> { let Some(mut uploading) = self.inner.pop_file(http) else { return Ok(None); @@ -126,7 +94,7 @@ impl AppState { http.file_patch( chunk.get_patch_uri(), - &self.alias_id, + self.inner.alias_id(), chunk.get_offset(), chunk.get_data(), )?; @@ -142,16 +110,17 @@ impl AppState { bar.finish(); self.progress = None; - let endpoint = self - .uri - .endpoint(format!("alias/mail/notify/{}", self.share_id)); - http.share_notify(&endpoint, &self.alias_id).unwrap(); // HACK unwrap + self.inner.share_notify(http).unwrap(); // HACK unwrap Ok(self.inner.has_file().then_some(())) } } } + pub fn file_names(&self) -> Vec<&str> { + self.inner.file_names() + } + pub fn save(&self) -> io::Result<()> { self.inner.save() } diff --git a/src/savedstate.rs b/src/cachefile.rs similarity index 54% rename from src/savedstate.rs rename to src/cachefile.rs index cb392c1..07bf647 100644 --- a/src/savedstate.rs +++ b/src/cachefile.rs @@ -2,13 +2,14 @@ use std::{ collections::VecDeque, fs, io::{self, Write}, - path::{Path, PathBuf}, + path::PathBuf, }; use log::trace; use serde::{Deserialize, Serialize}; use super::{ + cli::Cli, file::{self, FileTrait}, sharry::{self, Client, Uri}, }; @@ -30,22 +31,18 @@ impl FileState { fn start_upload( self, http: &impl Client, - uri: &Uri, + endpoint: &str, alias_id: &str, - share_id: &str, ) -> sharry::Result { match self { - FileState::C(checked) => { - let endpoint = &uri.endpoint(format!("alias/upload/{share_id}/files/tus")); - checked.start_upload(http, endpoint, alias_id) - } + FileState::C(checked) => checked.start_upload(http, endpoint, alias_id), FileState::U(uploading) => Ok(uploading), } } } #[derive(Serialize, Deserialize, Debug)] -pub struct SavedState { +pub struct CacheFile { #[serde(skip)] file_name: PathBuf, @@ -55,35 +52,47 @@ pub struct SavedState { files: VecDeque, } -impl SavedState { - pub fn new( - file_name: PathBuf, - uri: Uri, - alias_id: String, - share_id: String, - files: &Vec, - ) -> Self { +impl CacheFile { + fn cache_dir() -> PathBuf { + let dir_name = dirs_next::cache_dir() + .expect("could not determine cache directory") + .join("shrupl"); + + trace!("cachedir: {:?}", dir_name.display()); + dir_name + } + + fn cache_file(args: &Cli) -> PathBuf { + let file_name = Self::cache_dir().join(format!("{}.json", args.get_hash())); + + trace!("cachefile: {:?}", file_name.display()); + file_name + } + + pub fn try_resume(args: &Cli) -> io::Result { + let file_name = Self::cache_file(args); + + let state: Self = { + let file = fs::File::open(&file_name)?; + let reader = io::BufReader::new(file); + serde_json::from_reader(reader).map_err(io::Error::other)? + }; + + Ok(Self { file_name, ..state }) + } + + pub fn from_args(args: &Cli, share_id: String) -> Self { Self { - file_name, - uri, - alias_id, + file_name: Self::cache_file(&args), + uri: args.get_uri(), + alias_id: args.alias.clone(), share_id, - files: files.clone().into_iter().map(FileState::C).collect(), + files: args.files.clone().into_iter().map(FileState::C).collect(), } } - pub fn load(file_name: &Path) -> io::Result { - let file = fs::File::open(file_name)?; - let state: Self = - serde_json::from_reader(io::BufReader::new(file)).map_err(io::Error::other)?; - - Ok(Self { - file_name: file_name.to_owned(), - uri: state.uri, - alias_id: state.alias_id, - share_id: state.share_id, - files: state.files, - }) + pub fn alias_id(&self) -> &str { + &self.alias_id } pub fn file_names(&self) -> Vec<&str> { @@ -96,11 +105,10 @@ impl SavedState { pub fn pop_file(&mut self, http: &impl Client) -> Option { if let Some(state) = self.files.pop_front() { - Some( - state - .start_upload(http, &self.uri, &self.alias_id, &self.share_id) - .unwrap(), - ) // HACK unwrap + let endpoint = self + .uri + .endpoint(format!("alias/upload/{}/files/tus", self.share_id)); + Some(state.start_upload(http, &endpoint, &self.alias_id).unwrap()) // HACK unwrap } else { None } @@ -110,6 +118,14 @@ impl SavedState { self.files.push_front(FileState::U(file)); } + pub fn share_notify(&self, http: &impl Client) -> sharry::Result<()> { + let endpoint = self + .uri + .endpoint(format!("alias/mail/notify/{}", self.share_id)); + + http.share_notify(&endpoint, &self.alias_id) + } + pub fn save(&self) -> io::Result<()> { let cache_dir = self.file_name.parent().ok_or_else(|| { io::Error::other(format!("orphan file {:?}", self.file_name.display())) diff --git a/src/main.rs b/src/main.rs index 7b5e8a5..8db94fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ mod appstate; +mod cachefile; mod cli; mod file; -mod savedstate; mod sharry; use std::{ From 2edc690331632d52792ec3a9493f3f11196c371f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:25:15 +0000 Subject: [PATCH 12/14] [wip] impl `Client` for `ureq::Agent` - clippy fix --- src/appstate.rs | 2 +- src/cachefile.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/appstate.rs b/src/appstate.rs index 8cbc2e5..d9b90d2 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -22,7 +22,7 @@ impl fmt::Debug for AppState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AppState") .field("inner", &self.inner) - .finish() + .finish_non_exhaustive() } } diff --git a/src/cachefile.rs b/src/cachefile.rs index 07bf647..fe9b3a9 100644 --- a/src/cachefile.rs +++ b/src/cachefile.rs @@ -83,7 +83,7 @@ impl CacheFile { pub fn from_args(args: &Cli, share_id: String) -> Self { Self { - file_name: Self::cache_file(&args), + file_name: Self::cache_file(args), uri: args.get_uri(), alias_id: args.alias.clone(), share_id, From b9a0e1eeb0873158269b90aabf6886ed50a25ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Thu, 12 Jun 2025 22:26:48 +0000 Subject: [PATCH 13/14] [wip] impl `Client` for `ureq::Agent` - progress bar handling - share notify timing --- src/appstate.rs | 73 ++++++++++++++++++++++++++++--------------- src/file/uploading.rs | 7 +++++ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/appstate.rs b/src/appstate.rs index d9b90d2..dae340e 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -1,4 +1,8 @@ -use std::{fmt, io, time::Duration}; +use std::{ + cell::{Ref, RefCell}, + fmt, io, + time::Duration, +}; use console::style; use indicatif::{ProgressBar, ProgressStyle}; @@ -7,12 +11,12 @@ use log::debug; use super::{ cachefile::CacheFile, cli::Cli, - file::FileTrait, + file::{self, FileTrait}, sharry::{self, Client, ClientError}, }; pub struct AppState { - progress: Option, + current_bar: RefCell>, buffer: Vec, inner: CacheFile, @@ -29,7 +33,7 @@ impl fmt::Debug for AppState { impl AppState { fn new(chunk_size: usize, inner: CacheFile) -> Self { Self { - progress: None, + current_bar: None.into(), buffer: vec![0; chunk_size * 1024 * 1024], inner, } @@ -56,16 +60,9 @@ impl AppState { )) } - pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result> { - let Some(mut uploading) = self.inner.pop_file(http) else { - return Ok(None); - }; - - debug!("{uploading} chunk {}", self.buffer.len()); - - // Initialize or fetch the existing ProgressBar - let bar = &*self.progress.get_or_insert_with(|| { - // Create a new bar with style + fn get_or_create_progressbar(&self, uploading: &file::Uploading) -> Ref<'_, ProgressBar> { + let mut slot = self.current_bar.borrow_mut(); + if slot.is_none() { let bar = ProgressBar::new(uploading.get_size()) .with_style( ProgressStyle::with_template(&format!( @@ -76,21 +73,42 @@ impl AppState { ), style("/").magenta(), )) - .unwrap(), + .unwrap(), // safe as long as the style template is valid ) - .with_message(uploading.get_name().to_owned()) - .with_position(uploading.get_offset()); + .with_position(uploading.get_offset()) + .with_message(uploading.get_name().to_owned()); bar.enable_steady_tick(Duration::from_millis(100)); - bar - }); + *slot = Some(bar); + } + drop(slot); + + // unwrap is safe: We just made sure it's `Some`. + Ref::map(self.current_bar.borrow(), |opt| opt.as_ref().unwrap()) + } + + fn finish_bar(&self) { + let mut slot = self.current_bar.borrow_mut(); + if let Some(bar) = &*slot { + bar.finish(); + *slot = None; + } + } + + pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result> { + let Some(mut uploading) = self.inner.pop_file(http) else { + self.inner.share_notify(http).unwrap(); // HACK unwrap + + return Ok(None); + }; + + self.get_or_create_progressbar(&uploading); + + debug!("{uploading} chunk {}", self.buffer.len()); let chunk = uploading .read(&mut self.buffer) .map_err(ClientError::from)?; - if chunk.get_length() == 0 { - return Err(ClientError::req_err("wtf")); - } http.file_patch( chunk.get_patch_uri(), @@ -101,16 +119,19 @@ impl AppState { match uploading.check_eof() { Ok(uploading) => { + let bar = self.get_or_create_progressbar(&uploading); bar.set_position(uploading.get_offset()); + // BUG in `indicatif` crate? + // `set_position` does not force immediate redraw, so we also call `inc_length` here + bar.inc_length(0); + drop(bar); + self.inner.push_file(uploading); Ok(Some(())) } Err(path) => { debug!("Finished {:?}!", path.display()); - bar.finish(); - self.progress = None; - - self.inner.share_notify(http).unwrap(); // HACK unwrap + self.finish_bar(); Ok(self.inner.has_file().then_some(())) } diff --git a/src/file/uploading.rs b/src/file/uploading.rs index 6ce0480..962c2c6 100644 --- a/src/file/uploading.rs +++ b/src/file/uploading.rs @@ -48,6 +48,13 @@ impl Uploading { f.seek(SeekFrom::Start(self.offset))?; let read_len = f.read(buf)?; + if read_len == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + format!("could not read from file {:?}", self.path.display()), + )); + } + let chunk = Chunk::new(&buf[..read_len], &self.patch_uri, self.offset); self.offset += chunk.get_length(); From 9f1e0cfc6c6f11370a4dfe4f473a30a07384925b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Thu, 12 Jun 2025 23:01:16 +0000 Subject: [PATCH 14/14] [wip] impl `Client` for `ureq::Agent` - Client is now owned by `AppState` - minor cleanups --- src/appstate.rs | 30 +++++++++++++++++++++++------- src/cli.rs | 17 ++++++++++++++++- src/main.rs | 11 ++--------- src/sharry/api.rs | 2 +- src/sharry/client.rs | 14 ++++---------- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/appstate.rs b/src/appstate.rs index dae340e..b7e4709 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -19,6 +19,7 @@ pub struct AppState { current_bar: RefCell>, buffer: Vec, + http: ureq::Agent, inner: CacheFile, } @@ -30,11 +31,19 @@ impl fmt::Debug for AppState { } } +fn new_http(timeout: Option) -> ureq::Agent { + ureq::Agent::config_builder() + .timeout_global(timeout) + .build() + .into() +} + impl AppState { - fn new(chunk_size: usize, inner: CacheFile) -> Self { + fn new(chunk_size: usize, http: ureq::Agent, inner: CacheFile) -> Self { Self { current_bar: None.into(), buffer: vec![0; chunk_size * 1024 * 1024], + http, inner, } } @@ -44,10 +53,16 @@ impl AppState { .inspect_err(|e| debug!("could not resume from hash {:?}: {e}", args.get_hash())) .ok()?; - Some(Self::new(args.chunk_size, inner)) + Some(Self::new( + args.chunk_size, + new_http(args.get_timeout()), + inner, + )) } - pub fn from_args(args: &Cli, http: &impl Client) -> sharry::Result { + pub fn from_args(args: &Cli) -> sharry::Result { + let http = new_http(args.get_timeout()); + let share_id = http.share_create( &args.get_uri().endpoint("alias/upload/new"), &args.alias, @@ -56,6 +71,7 @@ impl AppState { Ok(Self::new( args.chunk_size, + http, CacheFile::from_args(args, share_id), )) } @@ -95,9 +111,9 @@ impl AppState { } } - pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result> { - let Some(mut uploading) = self.inner.pop_file(http) else { - self.inner.share_notify(http).unwrap(); // HACK unwrap + pub fn upload_chunk(&mut self) -> sharry::Result> { + let Some(mut uploading) = self.inner.pop_file(&self.http) else { + self.inner.share_notify(&self.http).unwrap(); // HACK unwrap return Ok(None); }; @@ -110,7 +126,7 @@ impl AppState { .read(&mut self.buffer) .map_err(ClientError::from)?; - http.file_patch( + self.http.file_patch( chunk.get_patch_uri(), self.inner.alias_id(), chunk.get_offset(), diff --git a/src/cli.rs b/src/cli.rs index 070d9f7..d161e11 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,5 @@ use std::{ + fmt, hash::{DefaultHasher, Hash, Hasher}, time::Duration, }; @@ -10,7 +11,7 @@ use super::{ sharry::{NewShareRequest, Uri}, }; -#[derive(Parser, Debug, Hash)] +#[derive(Parser, Hash)] #[command(version, about, long_about = None)] pub struct Cli { /// Timeout in seconds for HTTP actions (set 0 or invalid to disable) @@ -56,6 +57,20 @@ pub struct Cli { pub files: Vec, } +impl fmt::Debug for Cli { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Cli") + .field("uri", &self.get_uri()) + .field("alias", &self.alias) + .field("timeout", &self.get_timeout()) + .field("chunk_size", &self.chunk_size) + .field("share_request", &self.get_share_request()) + .field("files", &self.files) + .field("hash", &self.get_hash()) + .finish_non_exhaustive() + } +} + fn parse_seconds(data: &str) -> Result { data.parse().or(Ok(0)).map(Duration::from_secs) } diff --git a/src/main.rs b/src/main.rs index 8db94fd..2ca9d32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ use clap::Parser; use console::style; use dialoguer::{Confirm, theme::ColorfulTheme}; use log::{error, info}; -use ureq::Agent; use appstate::AppState; use cli::Cli; @@ -76,12 +75,6 @@ fn main() { let args = Cli::parse(); info!("args: {args:?}"); - info!("timeout: {:?}", args.get_timeout()); - - let agent: Agent = Agent::config_builder() - .timeout_global(args.get_timeout()) - .build() - .into(); let mut state = AppState::try_resume(&args) .and_then(|state| { @@ -94,7 +87,7 @@ fn main() { .unwrap_or_else(|| { check_ctrlc(); - match AppState::from_args(&args, &agent) { + match AppState::from_args(&args) { Ok(state) => { state.save().unwrap(); // HACK unwrap state @@ -115,7 +108,7 @@ fn main() { ); loop { - match state.upload_chunk(&agent) { + match state.upload_chunk() { Err(e) => error!("error: {e:?}"), Ok(None) => { info!("all uploads done"); diff --git a/src/sharry/api.rs b/src/sharry/api.rs index 9f51932..5e0ed17 100644 --- a/src/sharry/api.rs +++ b/src/sharry/api.rs @@ -31,7 +31,7 @@ impl fmt::Display for Uri { } } -#[derive(Serialize)] +#[derive(Serialize, Debug)] #[allow(non_snake_case)] pub struct NewShareRequest { name: String, diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 5299b02..68c6e49 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -21,7 +21,7 @@ pub trait Client { file_size: u64, ) -> Result; - fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>; + fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>; } #[derive(Debug, Error)] @@ -86,8 +86,6 @@ impl Client for ureq::Agent { alias_id: &str, data: NewShareRequest, ) -> Result { - // let endpoint = uri.get_endpoint("alias/upload/new"); - let mut res = self .post(endpoint) .header("Sharry-Alias", alias_id) @@ -112,8 +110,6 @@ impl Client for ureq::Agent { } fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> { - // let endpoint = uri.get_endpoint(format!("alias/mail/notify/{}", share_id)); - let mut res = self .post(endpoint) .header("Sharry-Alias", alias_id) @@ -140,8 +136,6 @@ impl Client for ureq::Agent { file_name: &str, file_size: u64, ) -> Result { - // let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id)); - let res = self .post(endpoint) .header("Sharry-Alias", alias_id) @@ -164,15 +158,15 @@ impl Client for ureq::Agent { Ok(location) } - fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()> { + fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()> { let res = self - .patch(patch_uri) + .patch(endpoint) .header("Sharry-Alias", alias_id) .header("Upload-Offset", offset) .send(chunk) .map_err(ClientError::from)?; - trace!("{patch_uri:?} response: {res:?}"); + trace!("{endpoint:?} response: {res:?}"); ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?; let res_offset = (res.headers().get("Upload-Offset"))