[wip] impl Client for ureq::Agent

- factored out `file` module
- renamed some stuff
- decoupled `sharry::client` from `file`
This commit is contained in:
Jörn-Michael Miehe 2025-06-10 18:20:52 +00:00
parent 69bef4e994
commit d607380659
9 changed files with 134 additions and 138 deletions

View file

@ -13,7 +13,8 @@ use serde::{Deserialize, Serialize};
use super::{ use super::{
cli::Cli, cli::Cli,
sharry::{Client, ClientError, FileChecked, FileUploading, SharryFile, Uri}, file::{self, FileTrait},
sharry::{self, Client, Uri},
}; };
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -31,8 +32,8 @@ pub struct AppState {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
enum FileState { enum FileState {
C(FileChecked), C(file::Checked),
U(FileUploading), U(file::Uploading),
} }
impl FileState { impl FileState {
@ -49,9 +50,12 @@ impl FileState {
uri: &Uri, uri: &Uri,
alias_id: &str, alias_id: &str,
share_id: &str, share_id: &str,
) -> Result<FileUploading, ClientError> { ) -> sharry::Result<file::Uploading> {
match self { 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), FileState::U(uploading) => Ok(uploading),
} }
} }
@ -99,12 +103,16 @@ impl AppState {
.ok() .ok()
} }
pub fn from_args(args: &Cli, http: &impl Client) -> Result<Self, ClientError> { pub fn from_args(args: &Cli, http: &impl Client) -> sharry::Result<Self> {
let file_name = Self::cache_file(args); let file_name = Self::cache_file(args);
let uri = args.get_uri(); let uri = args.get_uri();
let alias_id = args.alias.clone(); 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(); let files: VecDeque<_> = args.files.clone().into_iter().map(FileState::C).collect();
@ -126,11 +134,9 @@ impl AppState {
&mut self, &mut self,
http: &ureq::Agent, http: &ureq::Agent,
chunk_size: usize, chunk_size: usize,
) -> Result<Option<()>, UploadError> { ) -> sharry::Result<Option<()>> {
let uploading = if let Some(state) = self.files.pop_front() { let uploading = if let Some(state) = self.files.pop_front() {
state state.start_upload(http, &self.uri, &self.alias_id, &self.share_id)?
.start_upload(http, &self.uri, &self.alias_id, &self.share_id)
.unwrap() // HACK unwrap
} else { } else {
return Ok(None); return Ok(None);
}; };

View file

@ -5,7 +5,10 @@ use std::{
use clap::{Parser, builder::PossibleValuesParser}; use clap::{Parser, builder::PossibleValuesParser};
use super::sharry::{FileChecked, NewShareRequest, Uri}; use super::{
file::Checked,
sharry::{NewShareRequest, Uri},
};
#[derive(Parser, Debug, Hash)] #[derive(Parser, Debug, Hash)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@ -50,15 +53,15 @@ pub struct Cli {
/// Files to upload to the new share /// Files to upload to the new share
#[arg(value_name = "FILE", required = true, value_parser = parse_sharry_file)] #[arg(value_name = "FILE", required = true, value_parser = parse_sharry_file)]
pub files: Vec<FileChecked>, pub files: Vec<Checked>,
} }
fn parse_seconds(data: &str) -> Result<Duration, String> { fn parse_seconds(data: &str) -> Result<Duration, String> {
data.parse().or(Ok(0)).map(Duration::from_secs) data.parse().or(Ok(0)).map(Duration::from_secs)
} }
fn parse_sharry_file(data: &str) -> Result<FileChecked, String> { fn parse_sharry_file(data: &str) -> Result<Checked, String> {
FileChecked::new(data).map_err(|e| e.to_string()) Checked::new(data).map_err(|e| e.to_string())
} }
impl Cli { impl Cli {

View file

@ -5,15 +5,17 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::SharryFile; use crate::sharry;
use super::{FileTrait, Uploading};
#[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 Checked {
pub(super) path: PathBuf, path: PathBuf,
pub(super) size: u64, size: u64,
} }
impl FileChecked { impl Checked {
pub fn new(value: impl AsRef<Path>) -> io::Result<Self> { pub fn new(value: impl AsRef<Path>) -> io::Result<Self> {
let meta = fs::metadata(&value)?; let meta = fs::metadata(&value)?;
if meta.is_file() { 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<Uploading> {
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 /// get a reference to the file's name
/// ///
/// Uses `SharryFile::extract_file_name`, which may **panic**! /// 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 FileTrait>::extract_file_name(&self.path)
} }
fn get_size(&self) -> u64 { fn get_size(&self) -> u64 {

View file

@ -1,15 +1,12 @@
mod checked; mod checked;
mod uploading; mod uploading;
use std::{ use std::{ffi::OsStr, path::Path};
ffi::OsStr,
path::{Path, PathBuf},
};
pub use checked::FileChecked; pub use checked::Checked;
pub use uploading::FileUploading; pub use uploading::Uploading;
pub trait SharryFile<'t> { pub trait FileTrait<'t> {
/// extract the filename part of a `Path` reference /// extract the filename part of a `Path` reference
/// ///
/// # Panics /// # Panics

View file

@ -6,17 +6,17 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{FileChecked, SharryFile}; use super::FileTrait;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct FileUploading { pub struct Uploading {
path: PathBuf, path: PathBuf,
size: u64, size: u64,
patch_uri: String, patch_uri: String,
offset: u64, offset: u64,
} }
impl fmt::Display for FileUploading { impl fmt::Display for Uploading {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
@ -28,11 +28,11 @@ impl fmt::Display for FileUploading {
} }
} }
impl FileUploading { impl Uploading {
pub fn new(file: FileChecked, patch_uri: String) -> Self { pub(super) fn new(path: PathBuf, size: u64, patch_uri: String) -> Self {
Self { Self {
path: file.path, path,
size: file.size, size,
patch_uri, patch_uri,
offset: 0, 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 /// get a reference to the file's name
/// ///
/// Uses `SharryFile::extract_file_name`, which may **panic**! /// 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 FileTrait>::extract_file_name(&self.path)
} }
fn get_size(&self) -> u64 { fn get_size(&self) -> u64 {

View file

@ -1,5 +1,6 @@
mod appstate; mod appstate;
mod cli; mod cli;
mod file;
mod sharry; mod sharry;
use std::{ use std::{
@ -69,7 +70,7 @@ fn main() {
actual: _, actual: _,
expected: 403, expected: 403,
} => Some("Alias ID"), } => Some("Alias ID"),
ClientError::FileIO(_) => Some("URL"), // ClientError::FileIO(_) => Some("URL"),
_ => None, _ => None,
} { } {
info!("handling error: {e:?}"); info!("handling error: {e:?}");

View file

@ -10,19 +10,19 @@ pub struct Uri {
} }
impl 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<String>, base_url: impl Into<String>) -> Self { pub fn with_protocol(protocol: impl Into<String>, base_url: impl Into<String>) -> Self {
Self { Self {
protocol: protocol.into(), protocol: protocol.into(),
base_url: base_url.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 { impl fmt::Display for Uri {

View file

@ -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 thiserror::Error;
use super::{ use super::api::{NewShareRequest, NewShareResponse, NotifyShareResponse};
api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri},
file::{FileChecked, FileUploading, SharryFile}, pub type Result<T> = std::result::Result<T, ClientError>;
};
pub trait Client { pub trait Client {
fn sharry_share_create( fn share_create(&self, endpoint: &str, alias_id: &str, data: NewShareRequest)
&self, -> Result<String>;
uri: &Uri,
alias_id: &str,
data: NewShareRequest,
) -> Result<String, ClientError>;
fn sharry_share_notify( fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()>;
&self,
uri: &Uri,
alias_id: &str,
share_id: &str,
) -> Result<(), ClientError>;
fn sharry_file_create( fn file_create(
&self, &self,
uri: &Uri, endpoint: &str,
alias_id: &str, alias_id: &str,
share_id: &str,
file_name: &str, file_name: &str,
file_size: u64, file_size: u64,
) -> Result<String, ClientError>; ) -> Result<String>;
fn sharry_file_patch( fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8])
&self, -> Result<u64>;
patch_uri: &str,
alias_id: &str,
offset: u64,
chunk: &[u8],
) -> Result<u64, ClientError>;
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ClientError { pub enum ClientError {
#[error("file I/O error: {0}")]
FileIO(#[from] io::Error),
#[error("network request failed: {0}")] #[error("network request failed: {0}")]
Request(String), Request(String),
@ -60,19 +41,15 @@ pub enum ClientError {
} }
impl ClientError { impl ClientError {
fn req_err(msg: impl Display) -> Self { fn req_err(msg: impl fmt::Display) -> Self {
Self::Request(msg.to_string()) 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()) Self::ResponseParsing(msg.to_string())
} }
fn res_content_err(msg: impl Display) -> Self { fn res_check_status<T>(actual: T, expected: T) -> Result<()>
Self::ResponseContent(msg.to_string())
}
fn res_check_status<T>(actual: T, expected: T) -> Result<(), Self>
where where
T: Into<u16> + Eq, T: Into<u16> + Eq,
{ {
@ -88,75 +65,77 @@ impl ClientError {
} }
impl Client for ureq::Agent { impl Client for ureq::Agent {
fn sharry_share_create( fn share_create(
&self, &self,
uri: &Uri, endpoint: &str,
alias_id: &str, alias_id: &str,
data: NewShareRequest, data: NewShareRequest,
) -> Result<String, ClientError> { ) -> Result<String> {
let res = { // let endpoint = uri.get_endpoint("alias/upload/new");
let endpoint = uri.get_endpoint("alias/upload/new");
self.post(endpoint) let mut res = self
.header("Sharry-Alias", alias_id) .post(endpoint)
.send_json(data) .header("Sharry-Alias", alias_id)
.map_err(|e| ClientError::req_err(e))? .send_json(data)
.body_mut() .map_err(ClientError::req_err)?;
.read_json::<NewShareResponse>()
.map_err(|e| ClientError::res_parse_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::<NewShareResponse>()
.map_err(ClientError::res_parse_err)?;
debug!("{res:?}");
if res.success && (res.message == "Share created.") { if res.success && (res.message == "Share created.") {
Ok(res.id) Ok(res.id)
} else { } else {
Err(ClientError::res_content_err(format!("{res:?}"))) Err(ClientError::ResponseContent(format!("{res:?}")))
} }
} }
fn sharry_share_notify( fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> {
&self, // let endpoint = uri.get_endpoint(format!("alias/mail/notify/{}", share_id));
uri: &Uri,
alias_id: &str,
share_id: &str,
) -> Result<(), ClientError> {
let res = {
let endpoint = uri.get_endpoint(format!("alias/mail/notify/{}", share_id));
self.post(endpoint) let mut res = self
.header("Sharry-Alias", alias_id) .post(endpoint)
.send_empty() .header("Sharry-Alias", alias_id)
.map_err(|e| ClientError::req_err(e))? .send_empty()
.body_mut() .map_err(|e| ClientError::req_err(e))?;
.read_json::<NotifyShareResponse>()
.map_err(|e| ClientError::res_parse_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::<NotifyShareResponse>()
.map_err(|e| ClientError::res_parse_err(e))?;
debug!("{res:?}");
Ok(()) Ok(())
} }
fn sharry_file_create( fn file_create(
&self, &self,
uri: &Uri, endpoint: &str,
alias_id: &str, alias_id: &str,
share_id: &str,
file_name: &str, file_name: &str,
file_size: u64, file_size: u64,
) -> Result<String, ClientError> { ) -> Result<String> {
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) let res = self
.header("Sharry-Alias", alias_id) .post(endpoint)
.header("Sharry-File-Name", file_name) .header("Sharry-Alias", alias_id)
.header("Upload-Length", file_size) .header("Sharry-File-Name", file_name)
.send_empty() .header("Upload-Length", file_size)
.map_err(ClientError::req_err)? .send_empty()
}; .map_err(ClientError::req_err)?;
trace!("{endpoint:?} response: {res:?}");
ClientError::res_check_status(res.status(), ureq::http::StatusCode::CREATED)?; ClientError::res_check_status(res.status(), ureq::http::StatusCode::CREATED)?;
let location = (res.headers().get("Location")) let location = (res.headers().get("Location"))
@ -165,18 +144,18 @@ impl Client for ureq::Agent {
.map_err(ClientError::res_parse_err)? .map_err(ClientError::res_parse_err)?
.to_string(); .to_string();
debug!("patch uri: {location}"); debug!("{location:?}");
Ok(location) Ok(location)
} }
fn sharry_file_patch( fn file_patch(
&self, &self,
patch_uri: &str, patch_uri: &str,
alias_id: &str, alias_id: &str,
offset: u64, offset: u64,
chunk: &[u8], chunk: &[u8],
) -> Result<u64, ClientError> { ) -> Result<u64> {
let res = self let res = self
.patch(patch_uri) .patch(patch_uri)
.header("Sharry-Alias", alias_id) .header("Sharry-Alias", alias_id)
@ -184,6 +163,7 @@ impl Client for ureq::Agent {
.send(chunk) .send(chunk)
.map_err(ClientError::req_err)?; .map_err(ClientError::req_err)?;
trace!("{patch_uri:?} response: {res:?}");
ClientError::res_check_status(res.status(), ureq::http::StatusCode::NO_CONTENT)?; ClientError::res_check_status(res.status(), ureq::http::StatusCode::NO_CONTENT)?;
let res_offset = (res.headers().get("Upload-Offset")) let res_offset = (res.headers().get("Upload-Offset"))

View file

@ -1,9 +1,5 @@
#![allow(unused_imports)]
mod api; mod api;
mod client; mod client;
mod file;
pub use api::{NewShareRequest, Uri}; pub use api::{NewShareRequest, Uri};
pub use client::{Client, ClientError}; pub use client::{Client, ClientError, Result};
pub use file::{FileChecked, FileUploading, SharryFile};