Compare commits

..

3 commits

8 changed files with 215 additions and 13 deletions

27
Cargo.lock generated
View file

@ -236,7 +236,7 @@ checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
dependencies = [
"console",
"shell-words",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@ -702,7 +702,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@ -856,6 +856,7 @@ dependencies = [
"log",
"serde",
"serde_json",
"thiserror 2.0.12",
"ureq",
]
@ -911,7 +912,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl 2.0.12",
]
[[package]]
@ -925,6 +935,17 @@ dependencies = [
"syn",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.41"

View file

@ -15,6 +15,7 @@ indicatif = { version = "0.17.11", default-features = false }
log = "0.4.27"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
thiserror = "2.0.12"
ureq = { version = "3.0.11", features = ["json"] }
[profile.release]

View file

@ -1,5 +1,6 @@
use std::fmt::Display;
use std::fmt;
use log::debug;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Hash)]
@ -9,6 +10,13 @@ pub struct Uri {
}
impl Uri {
pub(super) fn get_endpoint(&self, endpoint: impl fmt::Display + fmt::Debug) -> String {
let uri = format!("{}/{}", self, endpoint);
debug!("endpoint uri: {uri:?}");
uri
}
pub fn with_protocol(protocol: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
protocol: protocol.into(),
@ -17,8 +25,8 @@ impl Uri {
}
}
impl Display for Uri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for Uri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}://{}/api/v2", self.protocol, self.base_url)
}
}

145
src/sharry/client.rs Normal file
View file

@ -0,0 +1,145 @@
use std::{error::Error, io};
use log::debug;
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<String, ClientError>;
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: FileChecked,
) -> Result<FileUploading, ClientError>;
// fn sharry_file_patch(&self);
}
#[derive(Debug, thiserror::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),
//
// #[error("could not parse offset header")]
// ResponseOffset,
}
impl Client for ureq::Agent {
fn sharry_share_create(
&self,
uri: &Uri,
alias_id: &str,
data: NewShareRequest,
) -> Result<String, ClientError> {
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::Request(e.to_string()))?
.body_mut()
.read_json::<NewShareResponse>()
.map_err(|e| ClientError::ResponseParsing(e.to_string()))?
};
debug!("response: {res:?}");
if res.success && (res.message == "Share created.") {
Ok(res.id)
} else {
Err(ClientError::ResponseContent(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::Request(e.to_string()))?
.body_mut()
.read_json::<NotifyShareResponse>()
.map_err(|e| ClientError::ResponseParsing(e.to_string()))?
};
debug!("response: {res:?}");
Ok(())
}
fn sharry_file_create(
&self,
uri: &Uri,
alias_id: &str,
share_id: &str,
file: FileChecked,
) -> Result<FileUploading, ClientError> {
let size = file.get_size();
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.get_name())
.header("Upload-Length", size)
.send_empty()
.map_err(|e| ClientError::Request(e.to_string()))?
};
if res.status() != ureq::http::StatusCode::CREATED {
return Err(ClientError::ResponseStatus {
actual: res.status().as_u16(),
expected: ureq::http::StatusCode::CREATED.as_u16(),
});
}
let location = (res.headers().get("Location"))
.ok_or_else(|| ClientError::ResponseParsing("Location header not found".to_owned()))?
.to_str()
.map_err(|_| ClientError::ResponseParsing("Location header invalid".to_owned()))?
.to_string();
debug!("patch uri: {location}");
Ok(FileUploading::new(file.into_path(), size, location))
}
}

View file

@ -66,6 +66,10 @@ impl FileChecked {
}
impl<'t> SharryFile<'t> for FileChecked {
fn into_path(self) -> PathBuf {
self.path
}
/// get a reference to the file's name
///
/// Uses `SharryFile::extract_file_name`, which may **panic**!

View file

@ -1,7 +1,10 @@
mod checked;
mod uploading;
use std::{ffi::OsStr, path::Path};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
pub use checked::FileChecked;
pub use uploading::{ChunkState, FileUploading, UploadError};
@ -20,6 +23,8 @@ pub trait SharryFile<'t> {
.expect("bad file name")
}
fn into_path(self) -> PathBuf;
fn get_name(&'t self) -> &'t str;
fn get_size(&self) -> u64;

View file

@ -1,6 +1,6 @@
use std::{
ffi::OsStr,
fs,
fmt, fs,
io::{self, Read, Seek, SeekFrom},
path::PathBuf,
};
@ -19,12 +19,24 @@ pub struct FileUploading {
offset: u64,
}
#[derive(Debug)]
#[derive(Debug, thiserror::Error)]
pub enum UploadError {
FileIO(io::Error),
#[error("file I/O error: {0}")]
FileIO(#[from] io::Error),
#[error("network request failed")]
Request,
#[error("unexpected response status")]
ResponseStatus,
#[error("could not parse offset header")]
ResponseOffset,
// #[error("chunk length conversion failed: {0}")]
// InvalidChunkLength(String),
// #[error("offset mismatch")]
// ResponseOffsetMismatch,
}
pub enum ChunkState {
@ -33,8 +45,8 @@ pub enum ChunkState {
Finished(PathBuf),
}
impl std::fmt::Display for FileUploading {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl fmt::Display for FileUploading {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Uploading {:?} ({}/{})",
@ -46,7 +58,7 @@ impl std::fmt::Display for FileUploading {
}
impl FileUploading {
pub(super) fn new(path: PathBuf, size: u64, uri: String) -> Self {
pub fn new(path: PathBuf, size: u64, uri: String) -> Self {
Self {
path,
size,
@ -123,6 +135,10 @@ impl FileUploading {
}
impl<'t> SharryFile<'t> for FileUploading {
fn into_path(self) -> PathBuf {
self.path
}
fn get_name(&'t self) -> &'t str {
<Self as SharryFile>::extract_file_name(&self.path)
}

View file

@ -2,10 +2,12 @@
mod alias;
mod api;
mod client;
mod file;
mod share;
pub use alias::Alias;
pub use api::{NewShareRequest, Uri};
// pub use client::SharryClient;
pub use file::{ChunkState, FileChecked, FileUploading, SharryFile, UploadError};
pub use share::Share;