diff --git a/Cargo.lock b/Cargo.lock index 8794b6b..57f40d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -854,6 +854,7 @@ dependencies = [ "env_logger", "indicatif", "log", + "regex", "serde", "serde_json", "thiserror 2.0.12", diff --git a/Cargo.toml b/Cargo.toml index f1fec53..623bc0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ dirs-next = "2.0.0" env_logger = "0.11.8" indicatif = { version = "0.17.11", default-features = false } log = "0.4.27" +regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" thiserror = "2.0.12" diff --git a/src/appstate.rs b/src/appstate.rs index f91bef3..11016ff 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -56,11 +56,7 @@ impl AppState { 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, - args.get_share_request(), - )?; + let share_id = http.share_create(&args.get_uri(), &args.alias, args.get_share_request())?; Ok(Self::new(http, CacheFile::from_args(args, share_id))) } @@ -152,12 +148,7 @@ impl AppState { return Ok(true); }; - self.http.file_patch( - chunk.get_patch_uri(), - self.inner.alias_id(), - chunk.get_offset(), - chunk.get_data(), - )?; + self.inner.file_patch(&self.http, &chunk)?; Ok(self.is_done()) } diff --git a/src/cachefile.rs b/src/cachefile.rs index 3fc01c1..7640800 100644 --- a/src/cachefile.rs +++ b/src/cachefile.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use crate::{ cli::Cli, - file::{self, FileTrait}, + file::{self, Chunk, FileTrait}, sharry::{self, Client, Uri}, }; @@ -30,12 +30,13 @@ impl FileState { fn start_upload( self, - http: &impl Client, - endpoint: impl FnOnce() -> String, + client: &impl sharry::Client, + uri: &sharry::Uri, alias_id: &str, + share_id: &str, ) -> sharry::Result { match self { - FileState::C(checked) => checked.start_upload(http, &endpoint(), alias_id), + FileState::C(checked) => checked.start_upload(client, uri, alias_id, share_id), FileState::U(uploading) => Ok(uploading), } } @@ -91,10 +92,6 @@ impl CacheFile { } } - pub fn alias_id(&self) -> &str { - &self.alias_id - } - pub fn file_names(&self) -> Vec<&str> { self.files.iter().map(FileState::file_name).collect() } @@ -103,14 +100,15 @@ impl CacheFile { self.files.is_empty() } - pub fn pop_file(&mut self, http: &impl Client) -> Option { + pub fn pop_file(&mut self, client: &impl Client) -> Option { if let Some(state) = self.files.pop_front() { - let endpoint = || { - self.uri - .endpoint(format!("alias/upload/{}/files/tus", self.share_id)) - }; + // HACK unwrap // TODO somehow retry - Some(state.start_upload(http, endpoint, &self.alias_id).unwrap()) // HACK unwrap + Some( + state + .start_upload(client, &self.uri, &self.alias_id, &self.share_id) + .unwrap(), + ) } else { None } @@ -120,12 +118,12 @@ impl CacheFile { 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)); + pub fn share_notify(&self, client: &impl Client) -> sharry::Result<()> { + client.share_notify(&self.uri, &self.alias_id, &self.share_id) + } - http.share_notify(&endpoint, &self.alias_id) + pub fn file_patch(&self, client: &impl Client, chunk: &Chunk) -> sharry::Result<()> { + client.file_patch(&self.uri, &self.alias_id, &self.share_id, chunk) } pub fn save(&self) -> io::Result<()> { diff --git a/src/file/checked.rs b/src/file/checked.rs index c6957ff..69e4ba2 100644 --- a/src/file/checked.rs +++ b/src/file/checked.rs @@ -56,12 +56,13 @@ impl Checked { pub fn start_upload( self, client: &impl sharry::Client, - endpoint: &str, + uri: &sharry::Uri, alias_id: &str, + share_id: &str, ) -> sharry::Result { - let patch_uri = client.file_create(endpoint, alias_id, self.get_name(), self.size)?; + let file_id = client.file_create(uri, alias_id, share_id, &self)?; - Ok(Uploading::new(self.path, self.size, patch_uri)) + Ok(Uploading::new(self.path, self.size, file_id)) } } diff --git a/src/file/chunk.rs b/src/file/chunk.rs index f93f172..da46daf 100644 --- a/src/file/chunk.rs +++ b/src/file/chunk.rs @@ -1,15 +1,15 @@ use std::fmt; pub struct Chunk<'t> { - data: &'t [u8], - patch_uri: String, + file_id: String, offset: u64, + data: &'t [u8], } impl fmt::Debug for Chunk<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Chunk") - .field("patch_uri", &self.patch_uri) + .field("file_id", &self.file_id) .field("offset", &self.offset) .field("data.len()", &self.data.len()) .finish_non_exhaustive() @@ -17,14 +17,22 @@ impl fmt::Debug for Chunk<'_> { } impl<'t> Chunk<'t> { - pub fn new(data: &'t [u8], patch_uri: String, offset: u64) -> Self { + pub fn new(file_id: String, offset: u64, data: &'t [u8]) -> Self { Self { - data, - patch_uri, + file_id, offset, + data, } } + pub fn get_file_id(&self) -> &str { + &self.file_id + } + + pub fn get_offset(&self) -> u64 { + self.offset + } + pub fn get_data(&self) -> &[u8] { self.data } @@ -38,11 +46,7 @@ impl<'t> Chunk<'t> { u64::try_from(len).unwrap_or_else(|e| panic!("usize={len} did not fit into u64: {e}")) } - pub fn get_patch_uri(&self) -> &str { - &self.patch_uri - } - - pub fn get_offset(&self) -> u64 { - self.offset + pub fn get_behind(&self) -> u64 { + self.offset + self.get_length() } } diff --git a/src/file/uploading.rs b/src/file/uploading.rs index 0554527..587ded7 100644 --- a/src/file/uploading.rs +++ b/src/file/uploading.rs @@ -1,5 +1,5 @@ use std::{ - fmt, fs, + fs, io::{self, Read, Seek, SeekFrom}, path::PathBuf, }; @@ -9,33 +9,22 @@ use serde::{Deserialize, Serialize}; use super::{Chunk, FileTrait}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Uploading { path: PathBuf, size: u64, - patch_uri: String, + file_id: String, + #[serde(skip)] last_offset: Option, offset: u64, } -impl fmt::Debug for Uploading { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Uploading {:?} ({}/{})", - self.path.display(), - self.offset, - self.size - ) - } -} - impl Uploading { - pub(super) fn new(path: PathBuf, size: u64, patch_uri: String) -> Self { + pub(super) fn new(path: PathBuf, size: u64, file_id: String) -> Self { Self { path, size, - patch_uri, + file_id, last_offset: None, offset: 0, } @@ -46,11 +35,13 @@ impl Uploading { } pub fn rewind(self) -> Option { - if let Some(last_offset) = self.last_offset { Some(Self { - last_offset: None, - offset: last_offset, - ..self - }) } else { + if let Some(last_offset) = self.last_offset { + Some(Self { + last_offset: None, + offset: last_offset, + ..self + }) + } else { warn!("attempted to rewind twice"); None } @@ -69,7 +60,7 @@ impl Uploading { )); } - let chunk = Chunk::new(&buf[..read_len], self.patch_uri.clone(), self.offset); + let chunk = Chunk::new(self.file_id.clone(), self.offset, &buf[..read_len]); self.last_offset = Some(self.offset); self.offset += chunk.get_length(); diff --git a/src/impl_ureq.rs b/src/impl_ureq.rs new file mode 100644 index 0000000..2ec8ee2 --- /dev/null +++ b/src/impl_ureq.rs @@ -0,0 +1,184 @@ +use log::{debug, trace}; + +use crate::{ + file::{self, FileTrait}, + sharry::{self, ClientError, Uri}, +}; + +fn find_cause( + uri: &Uri, + alias_id: &str, + share_id: Option<&str>, + file_id: Option<&str>, +) -> impl FnOnce(ureq::Error) -> ClientError { + move |error| match error { + ureq::Error::StatusCode(403) => { + trace!("HTTP Error 403: Alias not found!"); + + ClientError::InvalidParameter(sharry::Parameter::AliasID(alias_id.to_owned())) + } + ureq::Error::StatusCode(404) => { + trace!("HTTP Error 404: Share and/or file may have been deleted!"); + + if let Some(file_id) = file_id { + ClientError::InvalidParameter(sharry::Parameter::FileID(file_id.to_owned())) + } else if let Some(share_id) = share_id { + ClientError::InvalidParameter(sharry::Parameter::ShareID(share_id.to_owned())) + } else { + ClientError::unknown(error) + } + } + ureq::Error::Io(error) => { + trace!("std::io::Error {error:?}"); + + if let Some(msg) = error.get_ref().map(ToString::to_string) { + if msg == "failed to lookup address information: Name does not resolve" { + ClientError::InvalidParameter(sharry::Parameter::URI(uri.to_string())) + } else { + error.into() + } + } else { + error.into() + } + } + error => ClientError::unknown(error), + } +} + +impl sharry::Client for ureq::Agent { + fn share_create( + &self, + uri: &Uri, + alias_id: &str, + data: sharry::NewShareRequest, + ) -> sharry::Result { + let res = { + let endpoint = uri.share_create(); + + let mut res = self + .post(&endpoint) + .header("Sharry-Alias", alias_id) + .send_json(data) + .map_err(find_cause(uri, alias_id, None, None))?; + + trace!("{endpoint:?} response: {res:?}"); + ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?; + + res.body_mut() + .read_json::() + .map_err(ClientError::response)? + }; + + debug!("{res:?}"); + + if res.success && (res.message == "Share created.") { + Ok(res.id) + } else { + Err(ClientError::response(format!("{res:?}"))) + } + } + + fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> sharry::Result<()> { + let res = { + let endpoint = uri.share_notify(share_id); + + let mut res = self + .post(&endpoint) + .header("Sharry-Alias", alias_id) + .send_empty() + .map_err(find_cause(uri, alias_id, Some(share_id), None))?; + + trace!("{endpoint:?} response: {res:?}"); + ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?; + + res.body_mut() + .read_json::() + .map_err(ClientError::response)? + }; + + debug!("{res:?}"); + + Ok(()) + } + + fn file_create( + &self, + uri: &Uri, + alias_id: &str, + share_id: &str, + file: &file::Checked, + ) -> sharry::Result { + let res = { + let endpoint = uri.file_create(share_id); + + let res = self + .post(&endpoint) + .header("Sharry-Alias", alias_id) + .header("Sharry-File-Name", file.get_name()) + .header("Upload-Length", file.get_size()) + .send_empty() + .map_err(find_cause(uri, alias_id, Some(share_id), None))?; + + trace!("{endpoint:?} response: {res:?}"); + ClientError::res_status_check(res.status(), ureq::http::StatusCode::CREATED)?; + res + }; + + let location = (res.headers().get("Location")) + .ok_or_else(|| ClientError::response("Location header not found"))? + .to_str() + .map_err(ClientError::response)? + .to_string(); + + let file_id = Self::get_file_id(&location)?; + + debug!("location: {location:?}, file_id: {file_id:?}"); + + Ok(file_id.to_owned()) + } + + fn file_patch( + &self, + uri: &Uri, + alias_id: &str, + share_id: &str, + chunk: &file::Chunk, + ) -> sharry::Result<()> { + let res = { + let endpoint = uri.file_patch(share_id, chunk.get_file_id()); + + let res = self + .patch(&endpoint) + .header("Sharry-Alias", alias_id) + .header("Upload-Offset", chunk.get_offset()) + .send(chunk.get_data()) + .map_err(find_cause( + uri, + alias_id, + Some(share_id), + Some(chunk.get_file_id()), + ))?; + + trace!("{endpoint:?} response: {res:?}"); + ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?; + res + }; + + let res_offset = (res.headers().get("Upload-Offset")) + .ok_or_else(|| ClientError::response("Upload-Offset header not found"))? + .to_str() + .map_err(ClientError::response)? + .parse::() + .map_err(ClientError::response)?; + + if chunk.get_behind() == res_offset { + Ok(()) + } else { + Err(ClientError::response(format!( + "Unexpected Upload-Offset: {} (expected {})", + res_offset, + chunk.get_behind() + ))) + } + } +} diff --git a/src/main.rs b/src/main.rs index 249977e..08fa7b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod appstate; mod cachefile; mod cli; mod file; +mod impl_ureq; mod sharry; use std::{ @@ -47,27 +48,20 @@ fn prompt_continue() -> bool { selection == 0 } -fn print_error(e: &ClientError) { - if let Some(cause) = match e { - // known errors - ClientError::ResponseStatus { actual: 403, .. } => Some("Alias ID"), - ClientError::StdIo(_) => Some("URL"), - // unknown error - _ => None, - } { - // handle known error - info!("known error: {e:?}"); - println!( - "{} probably wrong: {}", +fn handle_error(e: &ClientError) { + if e.is_fatal() { + // react to fatal error + error!("fatal error: {e:?}"); + eprintln!( + "{} {}", style("Error!").red().bold(), - style(cause).cyan(), + style(e.to_string()).cyan().italic(), ); - println!("{}", style(e.to_string()).yellow().italic()); - } else { - // handle unknown error - error!("unknown error: {e} ({e:?})"); - println!("{}", style("Unknown Error!").red().bold()); + process::exit(1); } + + // handle recoverable error + info!("recoverable error: {e:?}"); } fn main() { @@ -116,7 +110,7 @@ fn main() { state } Err(e) => { - print_error(&e); + handle_error(&e); process::exit(1); } } @@ -143,9 +137,10 @@ fn main() { Err(e) => { // TODO better error handling (this will just retry endlessly) // Error 404: Share might have been deleted - error!("error: {e:?}"); + handle_error(&e); if let Some(s) = state.rewind() { + trace!("State rewound, retrying last chunk"); state = s; } else { eprintln!("{} Failed to retry chunk!", style("Error:").red().bold()); diff --git a/src/sharry/api.rs b/src/sharry/api.rs index c1cb5f3..b762e4e 100644 --- a/src/sharry/api.rs +++ b/src/sharry/api.rs @@ -9,6 +9,12 @@ pub struct Uri { base_url: String, } +impl fmt::Display for Uri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}://{}", self.protocol, self.base_url) + } +} + impl Uri { pub fn new(protocol: impl Into, base_url: impl Into) -> Self { Self { @@ -17,17 +23,26 @@ impl Uri { } } - pub fn endpoint(&self, endpoint: impl fmt::Display) -> String { - let uri = format!("{self}/{endpoint}"); + fn endpoint(&self, path: fmt::Arguments) -> String { + let uri = format!("{}://{}/api/v2/{path}", self.protocol, self.base_url); trace!("endpoint: {uri:?}"); - uri } -} -impl fmt::Display for Uri { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}://{}/api/v2", self.protocol, self.base_url) + pub fn share_create(&self) -> String { + self.endpoint(format_args!("alias/upload/new")) + } + + pub fn share_notify(&self, share_id: &str) -> String { + self.endpoint(format_args!("alias/mail/notify/{share_id}")) + } + + pub fn file_create(&self, share_id: &str) -> String { + self.endpoint(format_args!("alias/upload/{share_id}/files/tus")) + } + + pub fn file_patch(&self, share_id: &str, file_id: &str) -> String { + self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}")) } } @@ -58,7 +73,7 @@ impl NewShareRequest { } #[derive(Deserialize, Debug)] -pub(super) struct NewShareResponse { +pub struct NewShareResponse { pub success: bool, pub message: String, pub id: String, @@ -66,7 +81,7 @@ pub(super) struct NewShareResponse { #[derive(Deserialize, Debug)] #[allow(dead_code)] -pub(super) struct NotifyShareResponse { +pub struct NotifyShareResponse { pub success: bool, pub message: String, } diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 68c6e49..8497ca6 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -1,27 +1,114 @@ -use std::fmt; +use std::{fmt, sync::LazyLock}; -use log::{debug, trace}; +use log::trace; +use regex::Regex; use thiserror::Error; -use super::api::{NewShareRequest, NewShareResponse, NotifyShareResponse}; +use crate::file; -pub type Result = std::result::Result; +use super::api::{NewShareRequest, Uri}; pub trait Client { - fn share_create(&self, endpoint: &str, alias_id: &str, data: NewShareRequest) - -> Result; + fn get_file_id(uri: &str) -> super::Result<&str> { + /// Pattern breakdown: + /// - `^([^:/?#]+)://` – scheme (anything but `:/?#`) + `"://"` + /// - `([^/?#]+)` – authority/host (anything but `/?#`) + /// - `/api/v2/alias/upload/` – literal path segment + /// - `([^/]+)` – capture SID (one or more non-slash chars) + /// - `/files/tus/` – literal path segment + /// - `(?P[^/]+)` – capture FID (one or more non-slash chars) + /// - `$` – end of string + static UPLOAD_URL_RE: LazyLock = LazyLock::new(|| { + trace!("compiling UPLOAD_URL_RE"); - fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()>; + Regex::new( + r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P[^/]+)$", + ) + .expect("Regex compilation failed") + }); + + if let Some(fid) = UPLOAD_URL_RE + .captures(uri) + .and_then(|caps| caps.name("fid").map(|m| m.as_str())) + { + Ok(fid) + } else { + Err(super::ClientError::unknown(format!( + "Could not extract File ID from {:?}", + uri + ))) + } + } + + fn share_create( + &self, + uri: &Uri, + alias_id: &str, + data: NewShareRequest, + ) -> super::Result; + + fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> super::Result<()>; fn file_create( &self, - endpoint: &str, + uri: &Uri, alias_id: &str, - file_name: &str, - file_size: u64, - ) -> Result; + share_id: &str, + file: &file::Checked, + ) -> super::Result; - fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>; + fn file_patch( + &self, + uri: &Uri, + alias_id: &str, + share_id: &str, + chunk: &file::Chunk, + ) -> super::Result<()>; +} + +// TODO move into tests subdir + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn test_get_file_id() { +// let good = "https://example.com/api/v2/alias/upload/SID123/files/tus/FID456"; +// let good = Client::get_file_id(good); +// assert!(good.is_ok()); +// assert_eq!(good.unwrap(), "FID456"); + +// let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456"; // missing SID +// assert!(Client::get_file_id(bad).is_err()); + +// let bad: &'static str = "https://example.com/api/v2/alias/upload/SID123/files/tus/"; // missing FID +// assert!(Client::get_file_id(bad).is_err()); +// } +// } + +#[derive(Debug, Error)] +pub enum Parameter { + #[error("given URI {0:?}")] + URI(String), + + #[error("given Alias ID {0:?}")] + AliasID(String), + + #[error("stored Share ID {0:?}")] + ShareID(String), + + #[error("stored File ID {0:?}")] + FileID(String), +} + +impl Parameter { + fn is_fatal(&self) -> bool { + match self { + Self::URI(_) | Self::AliasID(_) => true, + _ => false, + } + } } #[derive(Debug, Error)] @@ -29,164 +116,43 @@ pub enum ClientError { #[error(transparent)] StdIo(#[from] std::io::Error), - #[error("network request failed: {0}")] - Request(String), + #[error("response error: {0}")] + Response(String), - #[error("unexpected response status: {actual} (expected {expected})")] - ResponseStatus { actual: u16, expected: u16 }, + #[error("Invalid {0}")] + InvalidParameter(Parameter), - #[error("response parsing failed: {0}")] - ResponseParsing(String), - - #[error("unexpected response content: {0}")] - ResponseContent(String), + #[error("Unknown error: {0}")] + Unknown(String), } impl ClientError { - pub fn req_err(msg: impl fmt::Display) -> Self { - Self::Request(msg.to_string()) - } - - pub fn res_parse_err(msg: impl fmt::Display) -> Self { - Self::ResponseParsing(msg.to_string()) - } - - pub fn res_status_check(actual: T, expected: T) -> Result<()> + pub fn res_status_check(actual: T, expected: T) -> super::Result<()> where - T: PartialEq + Into + Copy, + T: PartialEq + fmt::Display + Copy, { if actual == expected { Ok(()) } else { - Err(Self::ResponseStatus { - actual: actual.into(), - expected: expected.into(), - }) - } - } -} - -impl From for ClientError { - fn from(value: ureq::Error) -> Self { - match value { - ureq::Error::StatusCode(status) => Self::ResponseStatus { - actual: status, - expected: 200, - }, - ureq::Error::Io(e) => e.into(), - error => Self::req_err(error), - } - } -} - -impl Client for ureq::Agent { - fn share_create( - &self, - endpoint: &str, - alias_id: &str, - data: NewShareRequest, - ) -> Result { - let mut res = self - .post(endpoint) - .header("Sharry-Alias", alias_id) - .send_json(data) - .map_err(ClientError::from)?; - - trace!("{endpoint:?} response: {res:?}"); - ClientError::res_status_check(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::ResponseContent(format!("{res:?}"))) - } - } - - fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> { - let mut res = self - .post(endpoint) - .header("Sharry-Alias", alias_id) - .send_empty() - .map_err(ClientError::from)?; - - trace!("{endpoint:?} response: {res:?}"); - ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?; - - let res = res - .body_mut() - .read_json::() - .map_err(ClientError::res_parse_err)?; - - debug!("{res:?}"); - - Ok(()) - } - - fn file_create( - &self, - endpoint: &str, - alias_id: &str, - file_name: &str, - file_size: u64, - ) -> Result { - 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::from)?; - - trace!("{endpoint:?} response: {res:?}"); - 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"))? - .to_str() - .map_err(ClientError::res_parse_err)? - .to_string(); - - debug!("{location:?}"); - - Ok(location) - } - - fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()> { - let res = self - .patch(endpoint) - .header("Sharry-Alias", alias_id) - .header("Upload-Offset", offset) - .send(chunk) - .map_err(ClientError::from)?; - - trace!("{endpoint:?} response: {res:?}"); - 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"))? - .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(()) - } else { - Err(ClientError::ResponseContent(format!( - "Unexpected Upload-Offset: {} (expected {})", - res_offset, - offset + chunk_len + Err(Self::Response(format!( + "unexpected status: {actual} (expected {expected})" ))) } } + + pub fn response(e: impl ToString) -> Self { + Self::Response(e.to_string()) + } + + pub fn unknown(e: impl ToString) -> Self { + Self::Unknown(e.to_string()) + } + + pub fn is_fatal(&self) -> bool { + match self { + Self::InvalidParameter(p) => p.is_fatal(), + Self::Unknown(_) => true, + _ => false, + } + } } diff --git a/src/sharry/mod.rs b/src/sharry/mod.rs index e230f9d..87a65f5 100644 --- a/src/sharry/mod.rs +++ b/src/sharry/mod.rs @@ -1,5 +1,7 @@ mod api; mod client; -pub use api::{NewShareRequest, Uri}; -pub use client::{Client, ClientError, Result}; +pub use api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri}; +pub use client::{Client, ClientError, Parameter}; + +pub type Result = std::result::Result;