2025-06-10 18:20:52 +00:00
|
|
|
use std::fmt;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
use log::{debug, trace};
|
2025-06-08 21:31:50 +00:00
|
|
|
use thiserror::Error;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
use super::api::{NewShareRequest, NewShareResponse, NotifyShareResponse};
|
|
|
|
|
|
|
|
|
|
pub type Result<T> = std::result::Result<T, ClientError>;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
|
|
|
pub trait Client {
|
2025-06-10 18:20:52 +00:00
|
|
|
fn share_create(&self, endpoint: &str, alias_id: &str, data: NewShareRequest)
|
|
|
|
|
-> Result<String>;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()>;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
fn file_create(
|
2025-06-08 17:13:01 +00:00
|
|
|
&self,
|
2025-06-10 18:20:52 +00:00
|
|
|
endpoint: &str,
|
2025-06-08 17:13:01 +00:00
|
|
|
alias_id: &str,
|
2025-06-10 01:17:17 +00:00
|
|
|
file_name: &str,
|
|
|
|
|
file_size: u64,
|
2025-06-10 18:20:52 +00:00
|
|
|
) -> Result<String>;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
2025-06-12 23:01:16 +00:00
|
|
|
fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>;
|
2025-06-08 01:20:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-08 21:31:50 +00:00
|
|
|
#[derive(Debug, Error)]
|
2025-06-08 01:20:41 +00:00
|
|
|
pub enum ClientError {
|
2025-06-12 00:54:26 +00:00
|
|
|
#[error(transparent)]
|
|
|
|
|
StdIo(#[from] std::io::Error),
|
|
|
|
|
|
2025-06-08 01:20:41 +00:00
|
|
|
#[error("network request failed: {0}")]
|
|
|
|
|
Request(String),
|
|
|
|
|
|
2025-06-12 00:54:26 +00:00
|
|
|
#[error("unexpected response status: {actual} (expected {expected})")]
|
|
|
|
|
ResponseStatus { actual: u16, expected: u16 },
|
|
|
|
|
|
2025-06-08 01:20:41 +00:00
|
|
|
#[error("response parsing failed: {0}")]
|
|
|
|
|
ResponseParsing(String),
|
2025-06-08 17:13:01 +00:00
|
|
|
|
2025-06-08 01:20:41 +00:00
|
|
|
#[error("unexpected response content: {0}")]
|
|
|
|
|
ResponseContent(String),
|
2025-06-08 21:31:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ClientError {
|
2025-06-10 23:39:08 +00:00
|
|
|
pub fn req_err(msg: impl fmt::Display) -> Self {
|
2025-06-10 01:17:17 +00:00
|
|
|
Self::Request(msg.to_string())
|
2025-06-08 21:31:50 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-10 23:39:08 +00:00
|
|
|
pub fn res_parse_err(msg: impl fmt::Display) -> Self {
|
2025-06-10 01:17:17 +00:00
|
|
|
Self::ResponseParsing(msg.to_string())
|
2025-06-08 21:31:50 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-11 18:17:16 +00:00
|
|
|
pub fn res_status_check<T>(actual: T, expected: T) -> Result<()>
|
2025-06-10 01:17:17 +00:00
|
|
|
where
|
2025-06-11 18:17:16 +00:00
|
|
|
T: PartialEq + Into<u16> + Copy,
|
2025-06-10 01:17:17 +00:00
|
|
|
{
|
|
|
|
|
if actual == expected {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(Self::ResponseStatus {
|
|
|
|
|
actual: actual.into(),
|
2025-06-12 00:54:26 +00:00
|
|
|
expected: expected.into(),
|
2025-06-10 01:17:17 +00:00
|
|
|
})
|
|
|
|
|
}
|
2025-06-08 21:31:50 +00:00
|
|
|
}
|
2025-06-08 01:20:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-11 18:17:16 +00:00
|
|
|
impl From<ureq::Error> for ClientError {
|
|
|
|
|
fn from(value: ureq::Error) -> Self {
|
|
|
|
|
match value {
|
2025-06-12 00:54:26 +00:00
|
|
|
ureq::Error::StatusCode(status) => Self::ResponseStatus {
|
2025-06-11 18:17:16 +00:00
|
|
|
actual: status,
|
2025-06-12 00:54:26 +00:00
|
|
|
expected: 200,
|
2025-06-11 18:17:16 +00:00
|
|
|
},
|
2025-06-12 00:54:26 +00:00
|
|
|
ureq::Error::Io(e) => e.into(),
|
|
|
|
|
error => Self::req_err(error),
|
2025-06-11 18:17:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-08 01:20:41 +00:00
|
|
|
impl Client for ureq::Agent {
|
2025-06-10 18:20:52 +00:00
|
|
|
fn share_create(
|
2025-06-08 01:20:41 +00:00
|
|
|
&self,
|
2025-06-10 18:20:52 +00:00
|
|
|
endpoint: &str,
|
2025-06-08 01:20:41 +00:00
|
|
|
alias_id: &str,
|
|
|
|
|
data: NewShareRequest,
|
2025-06-10 18:20:52 +00:00
|
|
|
) -> Result<String> {
|
|
|
|
|
let mut res = self
|
|
|
|
|
.post(endpoint)
|
|
|
|
|
.header("Sharry-Alias", alias_id)
|
|
|
|
|
.send_json(data)
|
2025-06-11 18:17:16 +00:00
|
|
|
.map_err(ClientError::from)?;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
trace!("{endpoint:?} response: {res:?}");
|
2025-06-11 18:17:16 +00:00
|
|
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
2025-06-10 18:20:52 +00:00
|
|
|
|
|
|
|
|
let res = res
|
|
|
|
|
.body_mut()
|
|
|
|
|
.read_json::<NewShareResponse>()
|
|
|
|
|
.map_err(ClientError::res_parse_err)?;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
debug!("{res:?}");
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
|
|
|
if res.success && (res.message == "Share created.") {
|
|
|
|
|
Ok(res.id)
|
|
|
|
|
} else {
|
2025-06-10 18:20:52 +00:00
|
|
|
Err(ClientError::ResponseContent(format!("{res:?}")))
|
2025-06-08 01:20:41 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> {
|
|
|
|
|
let mut res = self
|
|
|
|
|
.post(endpoint)
|
|
|
|
|
.header("Sharry-Alias", alias_id)
|
|
|
|
|
.send_empty()
|
2025-06-11 18:17:16 +00:00
|
|
|
.map_err(ClientError::from)?;
|
2025-06-10 18:20:52 +00:00
|
|
|
|
|
|
|
|
trace!("{endpoint:?} response: {res:?}");
|
2025-06-11 18:17:16 +00:00
|
|
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
2025-06-10 18:20:52 +00:00
|
|
|
|
|
|
|
|
let res = res
|
|
|
|
|
.body_mut()
|
|
|
|
|
.read_json::<NotifyShareResponse>()
|
2025-06-11 00:06:08 +00:00
|
|
|
.map_err(ClientError::res_parse_err)?;
|
2025-06-10 18:20:52 +00:00
|
|
|
|
|
|
|
|
debug!("{res:?}");
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-06-08 17:13:01 +00:00
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
fn file_create(
|
2025-06-08 17:13:01 +00:00
|
|
|
&self,
|
2025-06-10 18:20:52 +00:00
|
|
|
endpoint: &str,
|
2025-06-08 17:13:01 +00:00
|
|
|
alias_id: &str,
|
2025-06-10 01:17:17 +00:00
|
|
|
file_name: &str,
|
|
|
|
|
file_size: u64,
|
2025-06-10 18:20:52 +00:00
|
|
|
) -> 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()
|
2025-06-11 18:17:16 +00:00
|
|
|
.map_err(ClientError::from)?;
|
2025-06-08 17:13:01 +00:00
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
trace!("{endpoint:?} response: {res:?}");
|
2025-06-11 18:17:16 +00:00
|
|
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::CREATED)?;
|
2025-06-08 17:13:01 +00:00
|
|
|
|
|
|
|
|
let location = (res.headers().get("Location"))
|
2025-06-08 21:31:50 +00:00
|
|
|
.ok_or_else(|| ClientError::res_parse_err("Location header not found"))?
|
2025-06-08 17:13:01 +00:00
|
|
|
.to_str()
|
2025-06-10 01:17:17 +00:00
|
|
|
.map_err(ClientError::res_parse_err)?
|
2025-06-08 17:13:01 +00:00
|
|
|
.to_string();
|
|
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
debug!("{location:?}");
|
2025-06-08 17:13:01 +00:00
|
|
|
|
2025-06-10 01:17:17 +00:00
|
|
|
Ok(location)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-12 23:01:16 +00:00
|
|
|
fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()> {
|
2025-06-10 01:17:17 +00:00
|
|
|
let res = self
|
2025-06-12 23:01:16 +00:00
|
|
|
.patch(endpoint)
|
2025-06-10 01:17:17 +00:00
|
|
|
.header("Sharry-Alias", alias_id)
|
|
|
|
|
.header("Upload-Offset", offset)
|
|
|
|
|
.send(chunk)
|
2025-06-11 18:17:16 +00:00
|
|
|
.map_err(ClientError::from)?;
|
2025-06-10 01:17:17 +00:00
|
|
|
|
2025-06-12 23:01:16 +00:00
|
|
|
trace!("{endpoint:?} response: {res:?}");
|
2025-06-11 18:17:16 +00:00
|
|
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?;
|
2025-06-10 01:17:17 +00:00
|
|
|
|
|
|
|
|
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 {
|
2025-06-10 23:39:08 +00:00
|
|
|
Ok(())
|
2025-06-10 01:17:17 +00:00
|
|
|
} else {
|
|
|
|
|
Err(ClientError::ResponseContent(format!(
|
|
|
|
|
"Unexpected Upload-Offset: {} (expected {})",
|
|
|
|
|
res_offset,
|
|
|
|
|
offset + chunk_len
|
|
|
|
|
)))
|
|
|
|
|
}
|
2025-06-08 17:13:01 +00:00
|
|
|
}
|
2025-06-08 01:20:41 +00:00
|
|
|
}
|