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 = [ dependencies = [
"console", "console",
"shell-words", "shell-words",
"thiserror", "thiserror 1.0.69",
] ]
[[package]] [[package]]
@ -702,7 +702,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"libredox", "libredox",
"thiserror", "thiserror 1.0.69",
] ]
[[package]] [[package]]
@ -856,6 +856,7 @@ dependencies = [
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.12",
"ureq", "ureq",
] ]
@ -911,7 +912,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [ 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]] [[package]]
@ -925,6 +935,17 @@ dependencies = [
"syn", "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]] [[package]]
name = "time" name = "time"
version = "0.3.41" version = "0.3.41"

View file

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

View file

@ -1,5 +1,6 @@
use std::fmt::Display; use std::fmt;
use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Hash)] #[derive(Serialize, Deserialize, Debug, Hash)]
@ -9,6 +10,13 @@ pub struct Uri {
} }
impl 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 { pub fn with_protocol(protocol: impl Into<String>, base_url: impl Into<String>) -> Self {
Self { Self {
protocol: protocol.into(), protocol: protocol.into(),
@ -17,8 +25,8 @@ impl Uri {
} }
} }
impl Display for Uri { impl fmt::Display for Uri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}://{}/api/v2", self.protocol, self.base_url) 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 { impl<'t> SharryFile<'t> for FileChecked {
fn into_path(self) -> PathBuf {
self.path
}
/// get a reference to the file's name /// get a reference to the file's name
/// ///
/// Uses `SharryFile::extract_file_name`, which may **panic**! /// Uses `SharryFile::extract_file_name`, which may **panic**!

View file

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

View file

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

View file

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