shrupl/src/sharry/client.rs

190 lines
5.3 KiB
Rust
Raw Normal View History

use std::fmt;
2025-06-08 01:20:41 +00:00
use log::{debug, trace};
use thiserror::Error;
2025-06-08 01:20:41 +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 {
fn share_create(&self, endpoint: &str, alias_id: &str, data: NewShareRequest)
-> Result<String>;
2025-06-08 01:20:41 +00:00
fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()>;
2025-06-08 01:20:41 +00:00
fn file_create(
2025-06-08 17:13:01 +00:00
&self,
endpoint: &str,
2025-06-08 17:13:01 +00:00
alias_id: &str,
file_name: &str,
file_size: u64,
) -> Result<String>;
2025-06-08 01:20:41 +00:00
fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8])
-> Result<u64>;
2025-06-08 01:20:41 +00:00
}
#[derive(Debug, Error)]
2025-06-08 01:20:41 +00:00
pub enum ClientError {
#[error("network request failed: {0}")]
Request(String),
#[error("response parsing failed: {0}")]
ResponseParsing(String),
2025-06-08 17:13:01 +00:00
#[error("unexpected response status: {actual} (expected {expected})")]
ResponseStatus { actual: u16, expected: u16 },
2025-06-08 01:20:41 +00:00
#[error("unexpected response content: {0}")]
ResponseContent(String),
}
impl ClientError {
fn req_err(msg: impl fmt::Display) -> Self {
Self::Request(msg.to_string())
}
fn res_parse_err(msg: impl fmt::Display) -> Self {
Self::ResponseParsing(msg.to_string())
}
fn res_check_status<T>(actual: T, expected: T) -> Result<()>
where
T: Into<u16> + Eq,
{
if actual == expected {
Ok(())
} else {
Err(Self::ResponseStatus {
actual: actual.into(),
expected: expected.into(),
})
}
}
2025-06-08 01:20:41 +00:00
}
impl Client for ureq::Agent {
fn share_create(
2025-06-08 01:20:41 +00:00
&self,
endpoint: &str,
2025-06-08 01:20:41 +00:00
alias_id: &str,
data: NewShareRequest,
) -> Result<String> {
// let endpoint = uri.get_endpoint("alias/upload/new");
let mut res = self
.post(endpoint)
.header("Sharry-Alias", alias_id)
.send_json(data)
.map_err(ClientError::req_err)?;
2025-06-08 01:20:41 +00:00
trace!("{endpoint:?} response: {res:?}");
ClientError::res_check_status(res.status(), ureq::http::StatusCode::OK)?;
let res = res
.body_mut()
.read_json::<NewShareResponse>()
.map_err(ClientError::res_parse_err)?;
2025-06-08 01:20:41 +00:00
debug!("{res:?}");
2025-06-08 01:20:41 +00:00
if res.success && (res.message == "Share created.") {
Ok(res.id)
} else {
Err(ClientError::ResponseContent(format!("{res:?}")))
2025-06-08 01:20:41 +00:00
}
}
fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> {
// let endpoint = uri.get_endpoint(format!("alias/mail/notify/{}", share_id));
let mut res = self
.post(endpoint)
.header("Sharry-Alias", alias_id)
.send_empty()
.map_err(|e| ClientError::req_err(e))?;
trace!("{endpoint:?} response: {res:?}");
ClientError::res_check_status(res.status(), ureq::http::StatusCode::OK)?;
let res = res
.body_mut()
.read_json::<NotifyShareResponse>()
.map_err(|e| ClientError::res_parse_err(e))?;
debug!("{res:?}");
2025-06-08 01:20:41 +00:00
Ok(())
}
2025-06-08 17:13:01 +00:00
fn file_create(
2025-06-08 17:13:01 +00:00
&self,
endpoint: &str,
2025-06-08 17:13:01 +00:00
alias_id: &str,
file_name: &str,
file_size: u64,
) -> Result<String> {
// let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id));
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::req_err)?;
2025-06-08 17:13:01 +00:00
trace!("{endpoint:?} response: {res:?}");
ClientError::res_check_status(res.status(), ureq::http::StatusCode::CREATED)?;
2025-06-08 17:13:01 +00:00
let location = (res.headers().get("Location"))
.ok_or_else(|| ClientError::res_parse_err("Location header not found"))?
2025-06-08 17:13:01 +00:00
.to_str()
.map_err(ClientError::res_parse_err)?
2025-06-08 17:13:01 +00:00
.to_string();
debug!("{location:?}");
2025-06-08 17:13:01 +00:00
Ok(location)
}
fn file_patch(
&self,
patch_uri: &str,
alias_id: &str,
offset: u64,
chunk: &[u8],
) -> Result<u64> {
let res = self
.patch(patch_uri)
.header("Sharry-Alias", alias_id)
.header("Upload-Offset", offset)
.send(chunk)
.map_err(ClientError::req_err)?;
trace!("{patch_uri:?} response: {res:?}");
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::<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(res_offset)
} 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
}