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] [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};