use std::fmt; use log::{debug, trace}; use thiserror::Error; use super::api::{NewShareRequest, NewShareResponse, NotifyShareResponse}; pub type Result = std::result::Result; pub trait Client { fn share_create(&self, endpoint: &str, alias_id: &str, data: NewShareRequest) -> Result; fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()>; fn file_create( &self, endpoint: &str, alias_id: &str, file_name: &str, file_size: u64, ) -> Result; fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>; } #[derive(Debug, Error)] pub enum ClientError { #[error(transparent)] StdIo(#[from] std::io::Error), #[error("network request failed: {0}")] Request(String), #[error("unexpected response status: {actual} (expected {expected})")] ResponseStatus { actual: u16, expected: u16 }, #[error("response parsing failed: {0}")] ResponseParsing(String), #[error("unexpected response content: {0}")] ResponseContent(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<()> where T: PartialEq + Into + 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 ))) } } }