[wip] impl Client for ureq::Agent

- impl `sharry_file_patch`
- completely rework chunking logic
This commit is contained in:
Jörn-Michael Miehe 2025-06-10 01:17:17 +00:00
parent c9528a9ac1
commit 69bef4e994
6 changed files with 112 additions and 130 deletions

View file

@ -13,9 +13,7 @@ use serde::{Deserialize, Serialize};
use super::{ use super::{
cli::Cli, cli::Cli,
sharry::{ sharry::{Client, ClientError, FileChecked, FileUploading, SharryFile, Uri},
ChunkState, Client, ClientError, FileChecked, FileUploading, SharryFile, UploadError, Uri,
},
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -28,10 +28,17 @@ pub trait Client {
uri: &Uri, uri: &Uri,
alias_id: &str, alias_id: &str,
share_id: &str, share_id: &str,
file: FileChecked, file_name: &str,
) -> Result<FileUploading, ClientError>; file_size: u64,
) -> Result<String, ClientError>;
// fn sharry_file_patch(&self); fn sharry_file_patch(
&self,
patch_uri: &str,
alias_id: &str,
offset: u64,
chunk: &[u8],
) -> Result<u64, ClientError>;
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -54,15 +61,29 @@ pub enum ClientError {
impl ClientError { impl ClientError {
fn req_err(msg: impl Display) -> Self { 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 { 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 { fn res_content_err(msg: impl Display) -> Self {
ClientError::ResponseContent(msg.to_string()) Self::ResponseContent(msg.to_string())
}
fn res_check_status<T>(actual: T, expected: T) -> Result<(), Self>
where
T: Into<u16> + 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, uri: &Uri,
alias_id: &str, alias_id: &str,
share_id: &str, share_id: &str,
file: FileChecked, file_name: &str,
) -> Result<FileUploading, ClientError> { file_size: u64,
let size = file.get_size(); ) -> Result<String, ClientError> {
let res = { let res = {
let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id)); let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id));
self.post(endpoint) self.post(endpoint)
.header("Sharry-Alias", alias_id) .header("Sharry-Alias", alias_id)
.header("Sharry-File-Name", file.get_name()) .header("Sharry-File-Name", file_name)
.header("Upload-Length", size) .header("Upload-Length", file_size)
.send_empty() .send_empty()
.map_err(|e| ClientError::req_err(e))? .map_err(ClientError::req_err)?
}; };
if res.status() != ureq::http::StatusCode::CREATED { ClientError::res_check_status(res.status(), ureq::http::StatusCode::CREATED)?;
return Err(ClientError::ResponseStatus {
actual: res.status().as_u16(),
expected: ureq::http::StatusCode::CREATED.as_u16(),
});
}
let location = (res.headers().get("Location")) let location = (res.headers().get("Location"))
.ok_or_else(|| ClientError::res_parse_err("Location header not found"))? .ok_or_else(|| ClientError::res_parse_err("Location header not found"))?
.to_str() .to_str()
.map_err(|e| ClientError::res_parse_err(e))? .map_err(ClientError::res_parse_err)?
.to_string(); .to_string();
debug!("patch uri: {location}"); 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<u64, ClientError> {
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::<u64>()
.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
)))
}
} }
} }

View file

@ -1,18 +1,16 @@
use std::{ use std::{
ffi::OsStr,
fs, io, fs, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use log::debug;
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct FileChecked { pub struct FileChecked {
path: PathBuf, pub(super) path: PathBuf,
pub(super) size: u64,
} }
impl FileChecked { impl FileChecked {
@ -21,6 +19,7 @@ impl FileChecked {
if meta.is_file() { if meta.is_file() {
Ok(Self { Ok(Self {
path: fs::canonicalize(&value)?, path: fs::canonicalize(&value)?,
size: meta.len(),
}) })
} else { } else {
Err(io::Error::new( Err(io::Error::new(
@ -32,10 +31,6 @@ impl FileChecked {
} }
impl<'t> SharryFile<'t> for FileChecked { impl<'t> SharryFile<'t> for FileChecked {
fn into_path(self) -> PathBuf {
self.path
}
/// get a reference to the file's name /// get a reference to the file's name
/// ///
/// Uses `SharryFile::extract_file_name`, which may **panic**! /// Uses `SharryFile::extract_file_name`, which may **panic**!
@ -44,6 +39,6 @@ impl<'t> SharryFile<'t> for FileChecked {
} }
fn get_size(&self) -> u64 { fn get_size(&self) -> u64 {
fs::metadata(&self.path).unwrap().len() self.size
} }
} }

View file

@ -7,7 +7,7 @@ use std::{
}; };
pub use checked::FileChecked; pub use checked::FileChecked;
pub use uploading::{ChunkState, FileUploading, UploadError}; pub use uploading::FileUploading;
pub trait SharryFile<'t> { pub trait SharryFile<'t> {
/// extract the filename part of a `Path` reference /// extract the filename part of a `Path` reference
@ -21,8 +21,6 @@ pub trait SharryFile<'t> {
.expect("bad file name") .expect("bad file name")
} }
fn into_path(self) -> PathBuf;
fn get_name(&'t self) -> &'t str; fn get_name(&'t self) -> &'t str;
fn get_size(&self) -> u64; fn get_size(&self) -> u64;

View file

@ -1,50 +1,21 @@
use std::{ use std::{
ffi::OsStr,
fmt, fs, fmt, fs,
io::{self, Read, Seek, SeekFrom}, io::{self, Read, Seek, SeekFrom},
path::PathBuf, path::PathBuf,
}; };
use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ureq::http::{HeaderValue, StatusCode};
use super::SharryFile; use super::{FileChecked, SharryFile};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct FileUploading { pub struct FileUploading {
path: PathBuf, path: PathBuf,
size: u64, size: u64,
uri: String, patch_uri: String,
offset: u64, 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 { impl fmt::Display for FileUploading {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
@ -58,87 +29,55 @@ impl fmt::Display for FileUploading {
} }
impl FileUploading { impl FileUploading {
pub fn new(path: PathBuf, size: u64, uri: String) -> Self { pub fn new(file: FileChecked, patch_uri: String) -> Self {
Self { Self {
path, path: file.path,
size, size: file.size,
uri, patch_uri,
offset: 0, offset: 0,
} }
} }
pub fn get_patch_uri(&self) -> &str {
&self.patch_uri
}
pub fn get_offset(&self) -> u64 { pub fn get_offset(&self) -> u64 {
self.offset self.offset
} }
fn read_chunk(&self, chunk_size: usize) -> io::Result<Vec<u8>> { pub fn read(&mut self, buf: &mut [u8]) -> io::Result<u64> {
let mut f = fs::File::open(&self.path)?; let mut f = fs::File::open(&self.path)?;
f.seek(SeekFrom::Start(self.offset))?; f.seek(SeekFrom::Start(self.offset))?;
let read_len = f.read(buf)?;
let mut bytes = vec![0; chunk_size]; // convert into `u64`
let read_len = f.read(&mut bytes)?;
bytes.truncate(read_len);
Ok(bytes)
}
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);
}
let Some(Ok(Ok(res_offset))) = (res.headers().get("Upload-Offset"))
.map(HeaderValue::to_str)
.map(|v| v.map(str::parse::<u64>))
else {
return ChunkState::Err(self, UploadError::ResponseOffset);
};
// convert chunk.len() into an `u64`
// //
// BOOKMARK this might panic on platforms where `usize` > 64 bit. // BOOKMARK this might **panic** on platforms where `usize` has more than 64 bit.
// Also whoa, you've sent more than 2 EiB ... in ONE chunk. // Also, you're reading more than 2 EiB ... in ONE chunk.
// Maybe just chill? // Whoa! Maybe just chill?
let chunk_len = u64::try_from(chunk.len()) let read_len = u64::try_from(read_len)
.unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", chunk.len(), e)); .unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", read_len, e));
if self.offset + chunk_len != res_offset { self.offset += read_len;
return ChunkState::Err(self, UploadError::ResponseOffset);
Ok(read_len)
} }
self.offset = res_offset; pub fn check_eof(self) -> Result<Self, PathBuf> {
if self.offset < self.size {
if self.offset == self.size { Ok(self)
return ChunkState::Finished(self.path); } else {
Err(self.path)
} }
ChunkState::Ok(self)
} }
} }
impl<'t> SharryFile<'t> for FileUploading { impl<'t> SharryFile<'t> for FileUploading {
fn into_path(self) -> PathBuf { /// get a reference to the file's name
self.path ///
} /// Uses `SharryFile::extract_file_name`, which may **panic**!
fn get_name(&'t self) -> &'t str { fn get_name(&'t self) -> &'t str {
<Self as SharryFile>::extract_file_name(&self.path) <Self as SharryFile>::extract_file_name(&self.path)
} }

View file

@ -6,4 +6,4 @@ mod file;
pub use api::{NewShareRequest, Uri}; pub use api::{NewShareRequest, Uri};
pub use client::{Client, ClientError}; pub use client::{Client, ClientError};
pub use file::{ChunkState, FileChecked, FileUploading, SharryFile, UploadError}; pub use file::{FileChecked, FileUploading, SharryFile};