Compare commits
3 commits
e208df9711
...
90cecd015e
| Author | SHA1 | Date | |
|---|---|---|---|
| 90cecd015e | |||
| 51ecab41bb | |||
| 5b6fa3eaf7 |
8 changed files with 215 additions and 13 deletions
27
Cargo.lock
generated
27
Cargo.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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
145
src/sharry/client.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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**!
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue