major refactoring

- `sharry::Client` fn signatures
- `sharry::ClientError` rework
- `file::Uploading` saves `file_id` instead of `patch_uri`
- `impl sharry::Client for ureq::Agent` into separate file
This commit is contained in:
Jörn-Michael Miehe 2025-06-18 13:09:34 +00:00
parent de07d556a2
commit 783346c888
12 changed files with 407 additions and 258 deletions

1
Cargo.lock generated
View file

@ -854,6 +854,7 @@ dependencies = [
"env_logger", "env_logger",
"indicatif", "indicatif",
"log", "log",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.12", "thiserror 2.0.12",

View file

@ -13,6 +13,7 @@ dirs-next = "2.0.0"
env_logger = "0.11.8" env_logger = "0.11.8"
indicatif = { version = "0.17.11", default-features = false } indicatif = { version = "0.17.11", default-features = false }
log = "0.4.27" log = "0.4.27"
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.140"
thiserror = "2.0.12" thiserror = "2.0.12"

View file

@ -56,11 +56,7 @@ impl AppState {
pub fn from_args(args: &Cli) -> sharry::Result<Self> { pub fn from_args(args: &Cli) -> sharry::Result<Self> {
let http = new_http(args.get_timeout()); let http = new_http(args.get_timeout());
let share_id = http.share_create( let share_id = http.share_create(&args.get_uri(), &args.alias, args.get_share_request())?;
&args.get_uri().endpoint("alias/upload/new"),
&args.alias,
args.get_share_request(),
)?;
Ok(Self::new(http, CacheFile::from_args(args, share_id))) Ok(Self::new(http, CacheFile::from_args(args, share_id)))
} }
@ -152,12 +148,7 @@ impl AppState {
return Ok(true); return Ok(true);
}; };
self.http.file_patch( self.inner.file_patch(&self.http, &chunk)?;
chunk.get_patch_uri(),
self.inner.alias_id(),
chunk.get_offset(),
chunk.get_data(),
)?;
Ok(self.is_done()) Ok(self.is_done())
} }

View file

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
cli::Cli, cli::Cli,
file::{self, FileTrait}, file::{self, Chunk, FileTrait},
sharry::{self, Client, Uri}, sharry::{self, Client, Uri},
}; };
@ -30,12 +30,13 @@ impl FileState {
fn start_upload( fn start_upload(
self, self,
http: &impl Client, client: &impl sharry::Client,
endpoint: impl FnOnce() -> String, uri: &sharry::Uri,
alias_id: &str, alias_id: &str,
share_id: &str,
) -> sharry::Result<file::Uploading> { ) -> sharry::Result<file::Uploading> {
match self { 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), 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> { pub fn file_names(&self) -> Vec<&str> {
self.files.iter().map(FileState::file_name).collect() self.files.iter().map(FileState::file_name).collect()
} }
@ -103,14 +100,15 @@ impl CacheFile {
self.files.is_empty() self.files.is_empty()
} }
pub fn pop_file(&mut self, http: &impl Client) -> Option<file::Uploading> { pub fn pop_file(&mut self, client: &impl Client) -> Option<file::Uploading> {
if let Some(state) = self.files.pop_front() { if let Some(state) = self.files.pop_front() {
let endpoint = || { // HACK unwrap
self.uri
.endpoint(format!("alias/upload/{}/files/tus", self.share_id))
};
// TODO somehow retry // 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 { } else {
None None
} }
@ -120,12 +118,12 @@ impl CacheFile {
self.files.push_front(FileState::U(file)); self.files.push_front(FileState::U(file));
} }
pub fn share_notify(&self, http: &impl Client) -> sharry::Result<()> { pub fn share_notify(&self, client: &impl Client) -> sharry::Result<()> {
let endpoint = self client.share_notify(&self.uri, &self.alias_id, &self.share_id)
.uri }
.endpoint(format!("alias/mail/notify/{}", 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<()> { pub fn save(&self) -> io::Result<()> {

View file

@ -56,12 +56,13 @@ impl Checked {
pub fn start_upload( pub fn start_upload(
self, self,
client: &impl sharry::Client, client: &impl sharry::Client,
endpoint: &str, uri: &sharry::Uri,
alias_id: &str, alias_id: &str,
share_id: &str,
) -> sharry::Result<Uploading> { ) -> sharry::Result<Uploading> {
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))
} }
} }

View file

@ -1,15 +1,15 @@
use std::fmt; use std::fmt;
pub struct Chunk<'t> { pub struct Chunk<'t> {
data: &'t [u8], file_id: String,
patch_uri: String,
offset: u64, offset: u64,
data: &'t [u8],
} }
impl fmt::Debug for Chunk<'_> { impl fmt::Debug for Chunk<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Chunk") f.debug_struct("Chunk")
.field("patch_uri", &self.patch_uri) .field("file_id", &self.file_id)
.field("offset", &self.offset) .field("offset", &self.offset)
.field("data.len()", &self.data.len()) .field("data.len()", &self.data.len())
.finish_non_exhaustive() .finish_non_exhaustive()
@ -17,14 +17,22 @@ impl fmt::Debug for Chunk<'_> {
} }
impl<'t> Chunk<'t> { 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 { Self {
data, file_id,
patch_uri,
offset, 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] { pub fn get_data(&self) -> &[u8] {
self.data 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}")) u64::try_from(len).unwrap_or_else(|e| panic!("usize={len} did not fit into u64: {e}"))
} }
pub fn get_patch_uri(&self) -> &str { pub fn get_behind(&self) -> u64 {
&self.patch_uri self.offset + self.get_length()
}
pub fn get_offset(&self) -> u64 {
self.offset
} }
} }

View file

@ -1,5 +1,5 @@
use std::{ use std::{
fmt, fs, fs,
io::{self, Read, Seek, SeekFrom}, io::{self, Read, Seek, SeekFrom},
path::PathBuf, path::PathBuf,
}; };
@ -9,33 +9,22 @@ use serde::{Deserialize, Serialize};
use super::{Chunk, FileTrait}; use super::{Chunk, FileTrait};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug)]
pub struct Uploading { pub struct Uploading {
path: PathBuf, path: PathBuf,
size: u64, size: u64,
patch_uri: String, file_id: String,
#[serde(skip)]
last_offset: Option<u64>, last_offset: Option<u64>,
offset: u64, 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 { 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 { Self {
path, path,
size, size,
patch_uri, file_id,
last_offset: None, last_offset: None,
offset: 0, offset: 0,
} }
@ -46,11 +35,13 @@ impl Uploading {
} }
pub fn rewind(self) -> Option<Self> { pub fn rewind(self) -> Option<Self> {
if let Some(last_offset) = self.last_offset { Some(Self { if let Some(last_offset) = self.last_offset {
last_offset: None, Some(Self {
offset: last_offset, last_offset: None,
..self offset: last_offset,
}) } else { ..self
})
} else {
warn!("attempted to rewind twice"); warn!("attempted to rewind twice");
None 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.last_offset = Some(self.offset);
self.offset += chunk.get_length(); self.offset += chunk.get_length();

184
src/impl_ureq.rs Normal file
View file

@ -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<String> {
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::<sharry::NewShareResponse>()
.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::<sharry::NotifyShareResponse>()
.map_err(ClientError::response)?
};
debug!("{res:?}");
Ok(())
}
fn file_create(
&self,
uri: &Uri,
alias_id: &str,
share_id: &str,
file: &file::Checked,
) -> sharry::Result<String> {
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::<u64>()
.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()
)))
}
}
}

View file

@ -2,6 +2,7 @@ mod appstate;
mod cachefile; mod cachefile;
mod cli; mod cli;
mod file; mod file;
mod impl_ureq;
mod sharry; mod sharry;
use std::{ use std::{
@ -47,27 +48,20 @@ fn prompt_continue() -> bool {
selection == 0 selection == 0
} }
fn print_error(e: &ClientError) { fn handle_error(e: &ClientError) {
if let Some(cause) = match e { if e.is_fatal() {
// known errors // react to fatal error
ClientError::ResponseStatus { actual: 403, .. } => Some("Alias ID"), error!("fatal error: {e:?}");
ClientError::StdIo(_) => Some("URL"), eprintln!(
// unknown error "{} {}",
_ => None,
} {
// handle known error
info!("known error: {e:?}");
println!(
"{} probably wrong: {}",
style("Error!").red().bold(), style("Error!").red().bold(),
style(cause).cyan(), style(e.to_string()).cyan().italic(),
); );
println!("{}", style(e.to_string()).yellow().italic()); process::exit(1);
} else {
// handle unknown error
error!("unknown error: {e} ({e:?})");
println!("{}", style("Unknown Error!").red().bold());
} }
// handle recoverable error
info!("recoverable error: {e:?}");
} }
fn main() { fn main() {
@ -116,7 +110,7 @@ fn main() {
state state
} }
Err(e) => { Err(e) => {
print_error(&e); handle_error(&e);
process::exit(1); process::exit(1);
} }
} }
@ -143,9 +137,10 @@ fn main() {
Err(e) => { Err(e) => {
// TODO better error handling (this will just retry endlessly) // TODO better error handling (this will just retry endlessly)
// Error 404: Share might have been deleted // Error 404: Share might have been deleted
error!("error: {e:?}"); handle_error(&e);
if let Some(s) = state.rewind() { if let Some(s) = state.rewind() {
trace!("State rewound, retrying last chunk");
state = s; state = s;
} else { } else {
eprintln!("{} Failed to retry chunk!", style("Error:").red().bold()); eprintln!("{} Failed to retry chunk!", style("Error:").red().bold());

View file

@ -9,6 +9,12 @@ pub struct Uri {
base_url: String, 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 { impl Uri {
pub fn new(protocol: impl Into<String>, base_url: impl Into<String>) -> Self { pub fn new(protocol: impl Into<String>, base_url: impl Into<String>) -> Self {
Self { Self {
@ -17,17 +23,26 @@ impl Uri {
} }
} }
pub fn endpoint(&self, endpoint: impl fmt::Display) -> String { fn endpoint(&self, path: fmt::Arguments) -> String {
let uri = format!("{self}/{endpoint}"); let uri = format!("{}://{}/api/v2/{path}", self.protocol, self.base_url);
trace!("endpoint: {uri:?}"); trace!("endpoint: {uri:?}");
uri uri
} }
}
impl fmt::Display for Uri { pub fn share_create(&self) -> String {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.endpoint(format_args!("alias/upload/new"))
write!(f, "{}://{}/api/v2", self.protocol, self.base_url) }
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)] #[derive(Deserialize, Debug)]
pub(super) struct NewShareResponse { pub struct NewShareResponse {
pub success: bool, pub success: bool,
pub message: String, pub message: String,
pub id: String, pub id: String,
@ -66,7 +81,7 @@ pub(super) struct NewShareResponse {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub(super) struct NotifyShareResponse { pub struct NotifyShareResponse {
pub success: bool, pub success: bool,
pub message: String, pub message: String,
} }

View file

@ -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 thiserror::Error;
use super::api::{NewShareRequest, NewShareResponse, NotifyShareResponse}; use crate::file;
pub type Result<T> = std::result::Result<T, ClientError>; use super::api::{NewShareRequest, Uri};
pub trait Client { pub trait Client {
fn share_create(&self, endpoint: &str, alias_id: &str, data: NewShareRequest) fn get_file_id(uri: &str) -> super::Result<&str> {
-> Result<String>; /// 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<fid>[^/]+)` capture FID (one or more non-slash chars)
/// - `$` end of string
static UPLOAD_URL_RE: LazyLock<Regex> = 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<fid>[^/]+)$",
)
.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<String>;
fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> super::Result<()>;
fn file_create( fn file_create(
&self, &self,
endpoint: &str, uri: &Uri,
alias_id: &str, alias_id: &str,
file_name: &str, share_id: &str,
file_size: u64, file: &file::Checked,
) -> Result<String>; ) -> super::Result<String>;
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)] #[derive(Debug, Error)]
@ -29,164 +116,43 @@ pub enum ClientError {
#[error(transparent)] #[error(transparent)]
StdIo(#[from] std::io::Error), StdIo(#[from] std::io::Error),
#[error("network request failed: {0}")] #[error("response error: {0}")]
Request(String), Response(String),
#[error("unexpected response status: {actual} (expected {expected})")] #[error("Invalid {0}")]
ResponseStatus { actual: u16, expected: u16 }, InvalidParameter(Parameter),
#[error("response parsing failed: {0}")] #[error("Unknown error: {0}")]
ResponseParsing(String), Unknown(String),
#[error("unexpected response content: {0}")]
ResponseContent(String),
} }
impl ClientError { impl ClientError {
pub fn req_err(msg: impl fmt::Display) -> Self { pub fn res_status_check<T>(actual: T, expected: T) -> super::Result<()>
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<T>(actual: T, expected: T) -> Result<()>
where where
T: PartialEq + Into<u16> + Copy, T: PartialEq + fmt::Display + Copy,
{ {
if actual == expected { if actual == expected {
Ok(()) Ok(())
} else { } else {
Err(Self::ResponseStatus { Err(Self::Response(format!(
actual: actual.into(), "unexpected status: {actual} (expected {expected})"
expected: expected.into(),
})
}
}
}
impl From<ureq::Error> 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<String> {
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::<NewShareResponse>()
.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::<NotifyShareResponse>()
.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<String> {
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::<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(())
} else {
Err(ClientError::ResponseContent(format!(
"Unexpected Upload-Offset: {} (expected {})",
res_offset,
offset + chunk_len
))) )))
} }
} }
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,
}
}
} }

View file

@ -1,5 +1,7 @@
mod api; mod api;
mod client; mod client;
pub use api::{NewShareRequest, Uri}; pub use api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri};
pub use client::{Client, ClientError, Result}; pub use client::{Client, ClientError, Parameter};
pub type Result<T> = std::result::Result<T, ClientError>;