major refactoring
- `sharry::Client` fn signatures - `sharry::ClientError` rework - `file::Uploading` saves `file_id` instead of `patch_uri` - `impl sharry::Client for ureq::Agent` into separate file
This commit is contained in:
parent
de07d556a2
commit
783346c888
12 changed files with 407 additions and 258 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -854,6 +854,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"log",
|
"log",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ dirs-next = "2.0.0"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
indicatif = { version = "0.17.11", default-features = false }
|
indicatif = { version = "0.17.11", default-features = false }
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
|
regex = "1.11.1"
|
||||||
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"
|
thiserror = "2.0.12"
|
||||||
|
|
|
||||||
|
|
@ -56,11 +56,7 @@ impl AppState {
|
||||||
pub fn from_args(args: &Cli) -> sharry::Result<Self> {
|
pub fn from_args(args: &Cli) -> sharry::Result<Self> {
|
||||||
let http = new_http(args.get_timeout());
|
let http = new_http(args.get_timeout());
|
||||||
|
|
||||||
let share_id = http.share_create(
|
let share_id = http.share_create(&args.get_uri(), &args.alias, args.get_share_request())?;
|
||||||
&args.get_uri().endpoint("alias/upload/new"),
|
|
||||||
&args.alias,
|
|
||||||
args.get_share_request(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Self::new(http, CacheFile::from_args(args, share_id)))
|
Ok(Self::new(http, CacheFile::from_args(args, share_id)))
|
||||||
}
|
}
|
||||||
|
|
@ -152,12 +148,7 @@ impl AppState {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
self.http.file_patch(
|
self.inner.file_patch(&self.http, &chunk)?;
|
||||||
chunk.get_patch_uri(),
|
|
||||||
self.inner.alias_id(),
|
|
||||||
chunk.get_offset(),
|
|
||||||
chunk.get_data(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(self.is_done())
|
Ok(self.is_done())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::Cli,
|
cli::Cli,
|
||||||
file::{self, FileTrait},
|
file::{self, Chunk, FileTrait},
|
||||||
sharry::{self, Client, Uri},
|
sharry::{self, Client, Uri},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -30,12 +30,13 @@ impl FileState {
|
||||||
|
|
||||||
fn start_upload(
|
fn start_upload(
|
||||||
self,
|
self,
|
||||||
http: &impl Client,
|
client: &impl sharry::Client,
|
||||||
endpoint: impl FnOnce() -> String,
|
uri: &sharry::Uri,
|
||||||
alias_id: &str,
|
alias_id: &str,
|
||||||
|
share_id: &str,
|
||||||
) -> sharry::Result<file::Uploading> {
|
) -> sharry::Result<file::Uploading> {
|
||||||
match self {
|
match self {
|
||||||
FileState::C(checked) => checked.start_upload(http, &endpoint(), alias_id),
|
FileState::C(checked) => checked.start_upload(client, uri, alias_id, share_id),
|
||||||
FileState::U(uploading) => Ok(uploading),
|
FileState::U(uploading) => Ok(uploading),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,10 +92,6 @@ impl CacheFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn alias_id(&self) -> &str {
|
|
||||||
&self.alias_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn file_names(&self) -> Vec<&str> {
|
pub fn file_names(&self) -> Vec<&str> {
|
||||||
self.files.iter().map(FileState::file_name).collect()
|
self.files.iter().map(FileState::file_name).collect()
|
||||||
}
|
}
|
||||||
|
|
@ -103,14 +100,15 @@ impl CacheFile {
|
||||||
self.files.is_empty()
|
self.files.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pop_file(&mut self, http: &impl Client) -> Option<file::Uploading> {
|
pub fn pop_file(&mut self, client: &impl Client) -> Option<file::Uploading> {
|
||||||
if let Some(state) = self.files.pop_front() {
|
if let Some(state) = self.files.pop_front() {
|
||||||
let endpoint = || {
|
// HACK unwrap
|
||||||
self.uri
|
|
||||||
.endpoint(format!("alias/upload/{}/files/tus", self.share_id))
|
|
||||||
};
|
|
||||||
// TODO somehow retry
|
// TODO somehow retry
|
||||||
Some(state.start_upload(http, endpoint, &self.alias_id).unwrap()) // HACK unwrap
|
Some(
|
||||||
|
state
|
||||||
|
.start_upload(client, &self.uri, &self.alias_id, &self.share_id)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -120,12 +118,12 @@ impl CacheFile {
|
||||||
self.files.push_front(FileState::U(file));
|
self.files.push_front(FileState::U(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn share_notify(&self, http: &impl Client) -> sharry::Result<()> {
|
pub fn share_notify(&self, client: &impl Client) -> sharry::Result<()> {
|
||||||
let endpoint = self
|
client.share_notify(&self.uri, &self.alias_id, &self.share_id)
|
||||||
.uri
|
}
|
||||||
.endpoint(format!("alias/mail/notify/{}", self.share_id));
|
|
||||||
|
|
||||||
http.share_notify(&endpoint, &self.alias_id)
|
pub fn file_patch(&self, client: &impl Client, chunk: &Chunk) -> sharry::Result<()> {
|
||||||
|
client.file_patch(&self.uri, &self.alias_id, &self.share_id, chunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> io::Result<()> {
|
pub fn save(&self) -> io::Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -56,12 +56,13 @@ impl Checked {
|
||||||
pub fn start_upload(
|
pub fn start_upload(
|
||||||
self,
|
self,
|
||||||
client: &impl sharry::Client,
|
client: &impl sharry::Client,
|
||||||
endpoint: &str,
|
uri: &sharry::Uri,
|
||||||
alias_id: &str,
|
alias_id: &str,
|
||||||
|
share_id: &str,
|
||||||
) -> sharry::Result<Uploading> {
|
) -> sharry::Result<Uploading> {
|
||||||
let patch_uri = client.file_create(endpoint, alias_id, self.get_name(), self.size)?;
|
let file_id = client.file_create(uri, alias_id, share_id, &self)?;
|
||||||
|
|
||||||
Ok(Uploading::new(self.path, self.size, patch_uri))
|
Ok(Uploading::new(self.path, self.size, file_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
pub struct Chunk<'t> {
|
pub struct Chunk<'t> {
|
||||||
data: &'t [u8],
|
file_id: String,
|
||||||
patch_uri: String,
|
|
||||||
offset: u64,
|
offset: u64,
|
||||||
|
data: &'t [u8],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Chunk<'_> {
|
impl fmt::Debug for Chunk<'_> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("Chunk")
|
f.debug_struct("Chunk")
|
||||||
.field("patch_uri", &self.patch_uri)
|
.field("file_id", &self.file_id)
|
||||||
.field("offset", &self.offset)
|
.field("offset", &self.offset)
|
||||||
.field("data.len()", &self.data.len())
|
.field("data.len()", &self.data.len())
|
||||||
.finish_non_exhaustive()
|
.finish_non_exhaustive()
|
||||||
|
|
@ -17,14 +17,22 @@ impl fmt::Debug for Chunk<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'t> Chunk<'t> {
|
impl<'t> Chunk<'t> {
|
||||||
pub fn new(data: &'t [u8], patch_uri: String, offset: u64) -> Self {
|
pub fn new(file_id: String, offset: u64, data: &'t [u8]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
data,
|
file_id,
|
||||||
patch_uri,
|
|
||||||
offset,
|
offset,
|
||||||
|
data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_file_id(&self) -> &str {
|
||||||
|
&self.file_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_offset(&self) -> u64 {
|
||||||
|
self.offset
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_data(&self) -> &[u8] {
|
pub fn get_data(&self) -> &[u8] {
|
||||||
self.data
|
self.data
|
||||||
}
|
}
|
||||||
|
|
@ -38,11 +46,7 @@ impl<'t> Chunk<'t> {
|
||||||
u64::try_from(len).unwrap_or_else(|e| panic!("usize={len} did not fit into u64: {e}"))
|
u64::try_from(len).unwrap_or_else(|e| panic!("usize={len} did not fit into u64: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_patch_uri(&self) -> &str {
|
pub fn get_behind(&self) -> u64 {
|
||||||
&self.patch_uri
|
self.offset + self.get_length()
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_offset(&self) -> u64 {
|
|
||||||
self.offset
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
fmt, fs,
|
fs,
|
||||||
io::{self, Read, Seek, SeekFrom},
|
io::{self, Read, Seek, SeekFrom},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
@ -9,33 +9,22 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{Chunk, FileTrait};
|
use super::{Chunk, FileTrait};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Uploading {
|
pub struct Uploading {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
size: u64,
|
size: u64,
|
||||||
patch_uri: String,
|
file_id: String,
|
||||||
|
#[serde(skip)]
|
||||||
last_offset: Option<u64>,
|
last_offset: Option<u64>,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Uploading {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Uploading {:?} ({}/{})",
|
|
||||||
self.path.display(),
|
|
||||||
self.offset,
|
|
||||||
self.size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Uploading {
|
impl Uploading {
|
||||||
pub(super) fn new(path: PathBuf, size: u64, patch_uri: String) -> Self {
|
pub(super) fn new(path: PathBuf, size: u64, file_id: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
size,
|
size,
|
||||||
patch_uri,
|
file_id,
|
||||||
last_offset: None,
|
last_offset: None,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}
|
}
|
||||||
|
|
@ -46,11 +35,13 @@ impl Uploading {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rewind(self) -> Option<Self> {
|
pub fn rewind(self) -> Option<Self> {
|
||||||
if let Some(last_offset) = self.last_offset { Some(Self {
|
if let Some(last_offset) = self.last_offset {
|
||||||
last_offset: None,
|
Some(Self {
|
||||||
offset: last_offset,
|
last_offset: None,
|
||||||
..self
|
offset: last_offset,
|
||||||
}) } else {
|
..self
|
||||||
|
})
|
||||||
|
} else {
|
||||||
warn!("attempted to rewind twice");
|
warn!("attempted to rewind twice");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +60,7 @@ impl Uploading {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let chunk = Chunk::new(&buf[..read_len], self.patch_uri.clone(), self.offset);
|
let chunk = Chunk::new(self.file_id.clone(), self.offset, &buf[..read_len]);
|
||||||
self.last_offset = Some(self.offset);
|
self.last_offset = Some(self.offset);
|
||||||
self.offset += chunk.get_length();
|
self.offset += chunk.get_length();
|
||||||
|
|
||||||
|
|
|
||||||
184
src/impl_ureq.rs
Normal file
184
src/impl_ureq.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
use log::{debug, trace};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
file::{self, FileTrait},
|
||||||
|
sharry::{self, ClientError, Uri},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn find_cause(
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &str,
|
||||||
|
share_id: Option<&str>,
|
||||||
|
file_id: Option<&str>,
|
||||||
|
) -> impl FnOnce(ureq::Error) -> ClientError {
|
||||||
|
move |error| match error {
|
||||||
|
ureq::Error::StatusCode(403) => {
|
||||||
|
trace!("HTTP Error 403: Alias not found!");
|
||||||
|
|
||||||
|
ClientError::InvalidParameter(sharry::Parameter::AliasID(alias_id.to_owned()))
|
||||||
|
}
|
||||||
|
ureq::Error::StatusCode(404) => {
|
||||||
|
trace!("HTTP Error 404: Share and/or file may have been deleted!");
|
||||||
|
|
||||||
|
if let Some(file_id) = file_id {
|
||||||
|
ClientError::InvalidParameter(sharry::Parameter::FileID(file_id.to_owned()))
|
||||||
|
} else if let Some(share_id) = share_id {
|
||||||
|
ClientError::InvalidParameter(sharry::Parameter::ShareID(share_id.to_owned()))
|
||||||
|
} else {
|
||||||
|
ClientError::unknown(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ureq::Error::Io(error) => {
|
||||||
|
trace!("std::io::Error {error:?}");
|
||||||
|
|
||||||
|
if let Some(msg) = error.get_ref().map(ToString::to_string) {
|
||||||
|
if msg == "failed to lookup address information: Name does not resolve" {
|
||||||
|
ClientError::InvalidParameter(sharry::Parameter::URI(uri.to_string()))
|
||||||
|
} else {
|
||||||
|
error.into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error => ClientError::unknown(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sharry::Client for ureq::Agent {
|
||||||
|
fn share_create(
|
||||||
|
&self,
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &str,
|
||||||
|
data: sharry::NewShareRequest,
|
||||||
|
) -> sharry::Result<String> {
|
||||||
|
let res = {
|
||||||
|
let endpoint = uri.share_create();
|
||||||
|
|
||||||
|
let mut res = self
|
||||||
|
.post(&endpoint)
|
||||||
|
.header("Sharry-Alias", alias_id)
|
||||||
|
.send_json(data)
|
||||||
|
.map_err(find_cause(uri, alias_id, None, None))?;
|
||||||
|
|
||||||
|
trace!("{endpoint:?} response: {res:?}");
|
||||||
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
||||||
|
|
||||||
|
res.body_mut()
|
||||||
|
.read_json::<sharry::NewShareResponse>()
|
||||||
|
.map_err(ClientError::response)?
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("{res:?}");
|
||||||
|
|
||||||
|
if res.success && (res.message == "Share created.") {
|
||||||
|
Ok(res.id)
|
||||||
|
} else {
|
||||||
|
Err(ClientError::response(format!("{res:?}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> sharry::Result<()> {
|
||||||
|
let res = {
|
||||||
|
let endpoint = uri.share_notify(share_id);
|
||||||
|
|
||||||
|
let mut res = self
|
||||||
|
.post(&endpoint)
|
||||||
|
.header("Sharry-Alias", alias_id)
|
||||||
|
.send_empty()
|
||||||
|
.map_err(find_cause(uri, alias_id, Some(share_id), None))?;
|
||||||
|
|
||||||
|
trace!("{endpoint:?} response: {res:?}");
|
||||||
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
||||||
|
|
||||||
|
res.body_mut()
|
||||||
|
.read_json::<sharry::NotifyShareResponse>()
|
||||||
|
.map_err(ClientError::response)?
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("{res:?}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_create(
|
||||||
|
&self,
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &str,
|
||||||
|
share_id: &str,
|
||||||
|
file: &file::Checked,
|
||||||
|
) -> sharry::Result<String> {
|
||||||
|
let res = {
|
||||||
|
let endpoint = uri.file_create(share_id);
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.post(&endpoint)
|
||||||
|
.header("Sharry-Alias", alias_id)
|
||||||
|
.header("Sharry-File-Name", file.get_name())
|
||||||
|
.header("Upload-Length", file.get_size())
|
||||||
|
.send_empty()
|
||||||
|
.map_err(find_cause(uri, alias_id, Some(share_id), None))?;
|
||||||
|
|
||||||
|
trace!("{endpoint:?} response: {res:?}");
|
||||||
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::CREATED)?;
|
||||||
|
res
|
||||||
|
};
|
||||||
|
|
||||||
|
let location = (res.headers().get("Location"))
|
||||||
|
.ok_or_else(|| ClientError::response("Location header not found"))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(ClientError::response)?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let file_id = Self::get_file_id(&location)?;
|
||||||
|
|
||||||
|
debug!("location: {location:?}, file_id: {file_id:?}");
|
||||||
|
|
||||||
|
Ok(file_id.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_patch(
|
||||||
|
&self,
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &str,
|
||||||
|
share_id: &str,
|
||||||
|
chunk: &file::Chunk,
|
||||||
|
) -> sharry::Result<()> {
|
||||||
|
let res = {
|
||||||
|
let endpoint = uri.file_patch(share_id, chunk.get_file_id());
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.patch(&endpoint)
|
||||||
|
.header("Sharry-Alias", alias_id)
|
||||||
|
.header("Upload-Offset", chunk.get_offset())
|
||||||
|
.send(chunk.get_data())
|
||||||
|
.map_err(find_cause(
|
||||||
|
uri,
|
||||||
|
alias_id,
|
||||||
|
Some(share_id),
|
||||||
|
Some(chunk.get_file_id()),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
trace!("{endpoint:?} response: {res:?}");
|
||||||
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?;
|
||||||
|
res
|
||||||
|
};
|
||||||
|
|
||||||
|
let res_offset = (res.headers().get("Upload-Offset"))
|
||||||
|
.ok_or_else(|| ClientError::response("Upload-Offset header not found"))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(ClientError::response)?
|
||||||
|
.parse::<u64>()
|
||||||
|
.map_err(ClientError::response)?;
|
||||||
|
|
||||||
|
if chunk.get_behind() == res_offset {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ClientError::response(format!(
|
||||||
|
"Unexpected Upload-Offset: {} (expected {})",
|
||||||
|
res_offset,
|
||||||
|
chunk.get_behind()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main.rs
35
src/main.rs
|
|
@ -2,6 +2,7 @@ mod appstate;
|
||||||
mod cachefile;
|
mod cachefile;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod file;
|
mod file;
|
||||||
|
mod impl_ureq;
|
||||||
mod sharry;
|
mod sharry;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -47,27 +48,20 @@ fn prompt_continue() -> bool {
|
||||||
selection == 0
|
selection == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_error(e: &ClientError) {
|
fn handle_error(e: &ClientError) {
|
||||||
if let Some(cause) = match e {
|
if e.is_fatal() {
|
||||||
// known errors
|
// react to fatal error
|
||||||
ClientError::ResponseStatus { actual: 403, .. } => Some("Alias ID"),
|
error!("fatal error: {e:?}");
|
||||||
ClientError::StdIo(_) => Some("URL"),
|
eprintln!(
|
||||||
// unknown error
|
"{} {}",
|
||||||
_ => None,
|
|
||||||
} {
|
|
||||||
// handle known error
|
|
||||||
info!("known error: {e:?}");
|
|
||||||
println!(
|
|
||||||
"{} probably wrong: {}",
|
|
||||||
style("Error!").red().bold(),
|
style("Error!").red().bold(),
|
||||||
style(cause).cyan(),
|
style(e.to_string()).cyan().italic(),
|
||||||
);
|
);
|
||||||
println!("{}", style(e.to_string()).yellow().italic());
|
process::exit(1);
|
||||||
} else {
|
|
||||||
// handle unknown error
|
|
||||||
error!("unknown error: {e} ({e:?})");
|
|
||||||
println!("{}", style("Unknown Error!").red().bold());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle recoverable error
|
||||||
|
info!("recoverable error: {e:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -116,7 +110,7 @@ fn main() {
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
print_error(&e);
|
handle_error(&e);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,9 +137,10 @@ fn main() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// TODO better error handling (this will just retry endlessly)
|
// TODO better error handling (this will just retry endlessly)
|
||||||
// Error 404: Share might have been deleted
|
// Error 404: Share might have been deleted
|
||||||
error!("error: {e:?}");
|
handle_error(&e);
|
||||||
|
|
||||||
if let Some(s) = state.rewind() {
|
if let Some(s) = state.rewind() {
|
||||||
|
trace!("State rewound, retrying last chunk");
|
||||||
state = s;
|
state = s;
|
||||||
} else {
|
} else {
|
||||||
eprintln!("{} Failed to retry chunk!", style("Error:").red().bold());
|
eprintln!("{} Failed to retry chunk!", style("Error:").red().bold());
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@ pub struct Uri {
|
||||||
base_url: String,
|
base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Uri {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}://{}", self.protocol, self.base_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Uri {
|
impl Uri {
|
||||||
pub fn new(protocol: impl Into<String>, base_url: impl Into<String>) -> Self {
|
pub fn new(protocol: impl Into<String>, base_url: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -17,17 +23,26 @@ impl Uri {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn endpoint(&self, endpoint: impl fmt::Display) -> String {
|
fn endpoint(&self, path: fmt::Arguments) -> String {
|
||||||
let uri = format!("{self}/{endpoint}");
|
let uri = format!("{}://{}/api/v2/{path}", self.protocol, self.base_url);
|
||||||
trace!("endpoint: {uri:?}");
|
trace!("endpoint: {uri:?}");
|
||||||
|
|
||||||
uri
|
uri
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Uri {
|
pub fn share_create(&self) -> String {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
self.endpoint(format_args!("alias/upload/new"))
|
||||||
write!(f, "{}://{}/api/v2", self.protocol, self.base_url)
|
}
|
||||||
|
|
||||||
|
pub fn share_notify(&self, share_id: &str) -> String {
|
||||||
|
self.endpoint(format_args!("alias/mail/notify/{share_id}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_create(&self, share_id: &str) -> String {
|
||||||
|
self.endpoint(format_args!("alias/upload/{share_id}/files/tus"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_patch(&self, share_id: &str, file_id: &str) -> String {
|
||||||
|
self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +73,7 @@ impl NewShareRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub(super) struct NewShareResponse {
|
pub struct NewShareResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -66,7 +81,7 @@ pub(super) struct NewShareResponse {
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(super) struct NotifyShareResponse {
|
pub struct NotifyShareResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,114 @@
|
||||||
use std::fmt;
|
use std::{fmt, sync::LazyLock};
|
||||||
|
|
||||||
use log::{debug, trace};
|
use log::trace;
|
||||||
|
use regex::Regex;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use super::api::{NewShareRequest, NewShareResponse, NotifyShareResponse};
|
use crate::file;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, ClientError>;
|
use super::api::{NewShareRequest, Uri};
|
||||||
|
|
||||||
pub trait Client {
|
pub trait Client {
|
||||||
fn share_create(&self, endpoint: &str, alias_id: &str, data: NewShareRequest)
|
fn get_file_id(uri: &str) -> super::Result<&str> {
|
||||||
-> Result<String>;
|
/// Pattern breakdown:
|
||||||
|
/// - `^([^:/?#]+)://` – scheme (anything but `:/?#`) + `"://"`
|
||||||
|
/// - `([^/?#]+)` – authority/host (anything but `/?#`)
|
||||||
|
/// - `/api/v2/alias/upload/` – literal path segment
|
||||||
|
/// - `([^/]+)` – capture SID (one or more non-slash chars)
|
||||||
|
/// - `/files/tus/` – literal path segment
|
||||||
|
/// - `(?P<fid>[^/]+)` – capture FID (one or more non-slash chars)
|
||||||
|
/// - `$` – end of string
|
||||||
|
static UPLOAD_URL_RE: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
|
trace!("compiling UPLOAD_URL_RE");
|
||||||
|
|
||||||
fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()>;
|
Regex::new(
|
||||||
|
r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P<fid>[^/]+)$",
|
||||||
|
)
|
||||||
|
.expect("Regex compilation failed")
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(fid) = UPLOAD_URL_RE
|
||||||
|
.captures(uri)
|
||||||
|
.and_then(|caps| caps.name("fid").map(|m| m.as_str()))
|
||||||
|
{
|
||||||
|
Ok(fid)
|
||||||
|
} else {
|
||||||
|
Err(super::ClientError::unknown(format!(
|
||||||
|
"Could not extract File ID from {:?}",
|
||||||
|
uri
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn share_create(
|
||||||
|
&self,
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &str,
|
||||||
|
data: NewShareRequest,
|
||||||
|
) -> super::Result<String>;
|
||||||
|
|
||||||
|
fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> super::Result<()>;
|
||||||
|
|
||||||
fn file_create(
|
fn file_create(
|
||||||
&self,
|
&self,
|
||||||
endpoint: &str,
|
uri: &Uri,
|
||||||
alias_id: &str,
|
alias_id: &str,
|
||||||
file_name: &str,
|
share_id: &str,
|
||||||
file_size: u64,
|
file: &file::Checked,
|
||||||
) -> Result<String>;
|
) -> super::Result<String>;
|
||||||
|
|
||||||
fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>;
|
fn file_patch(
|
||||||
|
&self,
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &str,
|
||||||
|
share_id: &str,
|
||||||
|
chunk: &file::Chunk,
|
||||||
|
) -> super::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO move into tests subdir
|
||||||
|
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod tests {
|
||||||
|
// use super::*;
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn test_get_file_id() {
|
||||||
|
// let good = "https://example.com/api/v2/alias/upload/SID123/files/tus/FID456";
|
||||||
|
// let good = Client::get_file_id(good);
|
||||||
|
// assert!(good.is_ok());
|
||||||
|
// assert_eq!(good.unwrap(), "FID456");
|
||||||
|
|
||||||
|
// let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456"; // missing SID
|
||||||
|
// assert!(Client::get_file_id(bad).is_err());
|
||||||
|
|
||||||
|
// let bad: &'static str = "https://example.com/api/v2/alias/upload/SID123/files/tus/"; // missing FID
|
||||||
|
// assert!(Client::get_file_id(bad).is_err());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Parameter {
|
||||||
|
#[error("given URI {0:?}")]
|
||||||
|
URI(String),
|
||||||
|
|
||||||
|
#[error("given Alias ID {0:?}")]
|
||||||
|
AliasID(String),
|
||||||
|
|
||||||
|
#[error("stored Share ID {0:?}")]
|
||||||
|
ShareID(String),
|
||||||
|
|
||||||
|
#[error("stored File ID {0:?}")]
|
||||||
|
FileID(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter {
|
||||||
|
fn is_fatal(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::URI(_) | Self::AliasID(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
@ -29,164 +116,43 @@ pub enum ClientError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
StdIo(#[from] std::io::Error),
|
StdIo(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("network request failed: {0}")]
|
#[error("response error: {0}")]
|
||||||
Request(String),
|
Response(String),
|
||||||
|
|
||||||
#[error("unexpected response status: {actual} (expected {expected})")]
|
#[error("Invalid {0}")]
|
||||||
ResponseStatus { actual: u16, expected: u16 },
|
InvalidParameter(Parameter),
|
||||||
|
|
||||||
#[error("response parsing failed: {0}")]
|
#[error("Unknown error: {0}")]
|
||||||
ResponseParsing(String),
|
Unknown(String),
|
||||||
|
|
||||||
#[error("unexpected response content: {0}")]
|
|
||||||
ResponseContent(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientError {
|
impl ClientError {
|
||||||
pub fn req_err(msg: impl fmt::Display) -> Self {
|
pub fn res_status_check<T>(actual: T, expected: T) -> super::Result<()>
|
||||||
Self::Request(msg.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn res_parse_err(msg: impl fmt::Display) -> Self {
|
|
||||||
Self::ResponseParsing(msg.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn res_status_check<T>(actual: T, expected: T) -> Result<()>
|
|
||||||
where
|
where
|
||||||
T: PartialEq + Into<u16> + Copy,
|
T: PartialEq + fmt::Display + Copy,
|
||||||
{
|
{
|
||||||
if actual == expected {
|
if actual == expected {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(Self::ResponseStatus {
|
Err(Self::Response(format!(
|
||||||
actual: actual.into(),
|
"unexpected status: {actual} (expected {expected})"
|
||||||
expected: expected.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ureq::Error> for ClientError {
|
|
||||||
fn from(value: ureq::Error) -> Self {
|
|
||||||
match value {
|
|
||||||
ureq::Error::StatusCode(status) => Self::ResponseStatus {
|
|
||||||
actual: status,
|
|
||||||
expected: 200,
|
|
||||||
},
|
|
||||||
ureq::Error::Io(e) => e.into(),
|
|
||||||
error => Self::req_err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client for ureq::Agent {
|
|
||||||
fn share_create(
|
|
||||||
&self,
|
|
||||||
endpoint: &str,
|
|
||||||
alias_id: &str,
|
|
||||||
data: NewShareRequest,
|
|
||||||
) -> Result<String> {
|
|
||||||
let mut res = self
|
|
||||||
.post(endpoint)
|
|
||||||
.header("Sharry-Alias", alias_id)
|
|
||||||
.send_json(data)
|
|
||||||
.map_err(ClientError::from)?;
|
|
||||||
|
|
||||||
trace!("{endpoint:?} response: {res:?}");
|
|
||||||
ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
|
||||||
|
|
||||||
let res = res
|
|
||||||
.body_mut()
|
|
||||||
.read_json::<NewShareResponse>()
|
|
||||||
.map_err(ClientError::res_parse_err)?;
|
|
||||||
|
|
||||||
debug!("{res:?}");
|
|
||||||
|
|
||||||
if res.success && (res.message == "Share created.") {
|
|
||||||
Ok(res.id)
|
|
||||||
} else {
|
|
||||||
Err(ClientError::ResponseContent(format!("{res:?}")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> {
|
|
||||||
let mut res = self
|
|
||||||
.post(endpoint)
|
|
||||||
.header("Sharry-Alias", alias_id)
|
|
||||||
.send_empty()
|
|
||||||
.map_err(ClientError::from)?;
|
|
||||||
|
|
||||||
trace!("{endpoint:?} response: {res:?}");
|
|
||||||
ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
|
||||||
|
|
||||||
let res = res
|
|
||||||
.body_mut()
|
|
||||||
.read_json::<NotifyShareResponse>()
|
|
||||||
.map_err(ClientError::res_parse_err)?;
|
|
||||||
|
|
||||||
debug!("{res:?}");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_create(
|
|
||||||
&self,
|
|
||||||
endpoint: &str,
|
|
||||||
alias_id: &str,
|
|
||||||
file_name: &str,
|
|
||||||
file_size: u64,
|
|
||||||
) -> 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()
|
|
||||||
.map_err(ClientError::from)?;
|
|
||||||
|
|
||||||
trace!("{endpoint:?} response: {res:?}");
|
|
||||||
ClientError::res_status_check(res.status(), ureq::http::StatusCode::CREATED)?;
|
|
||||||
|
|
||||||
let location = (res.headers().get("Location"))
|
|
||||||
.ok_or_else(|| ClientError::res_parse_err("Location header not found"))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(ClientError::res_parse_err)?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
debug!("{location:?}");
|
|
||||||
|
|
||||||
Ok(location)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()> {
|
|
||||||
let res = self
|
|
||||||
.patch(endpoint)
|
|
||||||
.header("Sharry-Alias", alias_id)
|
|
||||||
.header("Upload-Offset", offset)
|
|
||||||
.send(chunk)
|
|
||||||
.map_err(ClientError::from)?;
|
|
||||||
|
|
||||||
trace!("{endpoint:?} response: {res:?}");
|
|
||||||
ClientError::res_status_check(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(())
|
|
||||||
} else {
|
|
||||||
Err(ClientError::ResponseContent(format!(
|
|
||||||
"Unexpected Upload-Offset: {} (expected {})",
|
|
||||||
res_offset,
|
|
||||||
offset + chunk_len
|
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn response(e: impl ToString) -> Self {
|
||||||
|
Self::Response(e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unknown(e: impl ToString) -> Self {
|
||||||
|
Self::Unknown(e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_fatal(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::InvalidParameter(p) => p.is_fatal(),
|
||||||
|
Self::Unknown(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
mod api;
|
mod api;
|
||||||
mod client;
|
mod client;
|
||||||
|
|
||||||
pub use api::{NewShareRequest, Uri};
|
pub use api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri};
|
||||||
pub use client::{Client, ClientError, Result};
|
pub use client::{Client, ClientError, Parameter};
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, ClientError>;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue