use std::{error::Error, fmt::Display, io}; use log::debug; use thiserror::Error; use super::{ api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri}, file::{FileChecked, FileUploading, SharryFile}, }; pub trait Client { fn sharry_share_create( &self, uri: &Uri, alias_id: &str, data: NewShareRequest, ) -> Result; fn sharry_share_notify( &self, uri: &Uri, alias_id: &str, share_id: &str, ) -> Result<(), ClientError>; fn sharry_file_create( &self, uri: &Uri, alias_id: &str, share_id: &str, file_name: &str, file_size: u64, ) -> Result; fn sharry_file_patch( &self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8], ) -> Result; } #[derive(Debug, Error)] pub enum ClientError { #[error("file I/O error: {0}")] FileIO(#[from] io::Error), #[error("network request failed: {0}")] Request(String), #[error("response parsing failed: {0}")] ResponseParsing(String), #[error("unexpected response status: {actual} (expected {expected})")] ResponseStatus { actual: u16, expected: u16 }, #[error("unexpected response content: {0}")] ResponseContent(String), } impl ClientError { fn req_err(msg: impl Display) -> Self { Self::Request(msg.to_string()) } fn res_parse_err(msg: impl Display) -> Self { Self::ResponseParsing(msg.to_string()) } fn res_content_err(msg: impl Display) -> Self { Self::ResponseContent(msg.to_string()) } fn res_check_status(actual: T, expected: T) -> Result<(), Self> where T: Into + Eq, { if actual == expected { Ok(()) } else { Err(Self::ResponseStatus { actual: actual.into(), expected: expected.into(), }) } } } impl Client for ureq::Agent { fn sharry_share_create( &self, uri: &Uri, alias_id: &str, data: NewShareRequest, ) -> Result { let res = { let endpoint = uri.get_endpoint("alias/upload/new"); self.post(endpoint) .header("Sharry-Alias", alias_id) .send_json(data) .map_err(|e| ClientError::req_err(e))? .body_mut() .read_json::() .map_err(|e| ClientError::res_parse_err(e))? }; debug!("response: {res:?}"); if res.success && (res.message == "Share created.") { Ok(res.id) } else { Err(ClientError::res_content_err(format!("{res:?}"))) } } fn sharry_share_notify( &self, 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) .header("Sharry-Alias", alias_id) .send_empty() .map_err(|e| ClientError::req_err(e))? .body_mut() .read_json::() .map_err(|e| ClientError::res_parse_err(e))? }; debug!("response: {res:?}"); Ok(()) } fn sharry_file_create( &self, uri: &Uri, alias_id: &str, share_id: &str, file_name: &str, file_size: u64, ) -> Result { let res = { let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id)); self.post(endpoint) .header("Sharry-Alias", alias_id) .header("Sharry-File-Name", file_name) .header("Upload-Length", file_size) .send_empty() .map_err(ClientError::req_err)? }; ClientError::res_check_status(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!("patch uri: {location}"); Ok(location) } fn sharry_file_patch( &self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8], ) -> Result { 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::() .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 ))) } } }