Compare commits

..

No commits in common. "d3148ca9fe940067a1dd9bca748c7ae5d9d6ceb0" and "6ca3e6c9dd06c02ef2341f8df86795955f366066" have entirely different histories.

14 changed files with 267 additions and 414 deletions

1
Cargo.lock generated
View file

@ -854,7 +854,6 @@ dependencies = [
"env_logger",
"indicatif",
"log",
"regex",
"serde",
"serde_json",
"thiserror 2.0.12",

View file

@ -13,7 +13,6 @@ dirs-next = "2.0.0"
env_logger = "0.11.8"
indicatif = { version = "0.17.11", default-features = false }
log = "0.4.27"
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
thiserror = "2.0.12"

View file

@ -55,7 +55,6 @@
- client error rework
- current variants are too "low level"
- use variants like `InvalidEndpoint`, `InvalidAlias` etc.
- `Uploading::abort() -> Checked`
- hashing
- store file hashes with all `file::*` variants

View file

@ -8,7 +8,7 @@ use console::style;
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, warn};
use crate::{
use super::{
cachefile::CacheFile,
cli::Cli,
file::{self, Chunk, FileTrait},
@ -56,7 +56,11 @@ impl AppState {
pub fn from_args(args: &Cli) -> sharry::Result<Self> {
let http = new_http(args.get_timeout());
let share_id = http.share_create(&args.get_uri(), &args.alias, args.get_share_request())?;
let share_id = http.share_create(
&args.get_uri().endpoint("alias/upload/new"),
&args.alias,
args.get_share_request(),
)?;
Ok(Self::new(http, CacheFile::from_args(args, share_id)))
}
@ -148,7 +152,12 @@ impl AppState {
return Ok(true);
};
self.inner.file_patch(&self.http, &chunk)?;
self.http.file_patch(
chunk.get_patch_uri(),
self.inner.alias_id(),
chunk.get_offset(),
chunk.get_data(),
)?;
Ok(self.is_done())
}

View file

@ -8,9 +8,9 @@ use std::{
use log::trace;
use serde::{Deserialize, Serialize};
use crate::{
use super::{
cli::Cli,
file::{self, Chunk, FileTrait},
file::{self, FileTrait},
sharry::{self, Client, Uri},
};
@ -30,13 +30,12 @@ impl FileState {
fn start_upload(
self,
client: &impl sharry::Client,
uri: &sharry::Uri,
http: &impl Client,
endpoint: impl FnOnce() -> String,
alias_id: &str,
share_id: &str,
) -> sharry::Result<file::Uploading> {
match self {
FileState::C(checked) => checked.start_upload(client, uri, alias_id, share_id),
FileState::C(checked) => checked.start_upload(http, &endpoint(), alias_id),
FileState::U(uploading) => Ok(uploading),
}
}
@ -92,6 +91,10 @@ impl CacheFile {
}
}
pub fn alias_id(&self) -> &str {
&self.alias_id
}
pub fn file_names(&self) -> Vec<&str> {
self.files.iter().map(FileState::file_name).collect()
}
@ -100,15 +103,14 @@ impl CacheFile {
self.files.is_empty()
}
pub fn pop_file(&mut self, client: &impl Client) -> Option<file::Uploading> {
pub fn pop_file(&mut self, http: &impl Client) -> Option<file::Uploading> {
if let Some(state) = self.files.pop_front() {
// HACK unwrap
let endpoint = || {
self.uri
.endpoint(format!("alias/upload/{}/files/tus", self.share_id))
};
// TODO somehow retry
Some(
state
.start_upload(client, &self.uri, &self.alias_id, &self.share_id)
.unwrap(),
)
Some(state.start_upload(http, endpoint, &self.alias_id).unwrap()) // HACK unwrap
} else {
None
}
@ -118,12 +120,12 @@ impl CacheFile {
self.files.push_front(FileState::U(file));
}
pub fn share_notify(&self, client: &impl Client) -> sharry::Result<()> {
client.share_notify(&self.uri, &self.alias_id, &self.share_id)
}
pub fn share_notify(&self, http: &impl Client) -> sharry::Result<()> {
let endpoint = self
.uri
.endpoint(format!("alias/mail/notify/{}", self.share_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)
http.share_notify(&endpoint, &self.alias_id)
}
pub fn save(&self) -> io::Result<()> {

View file

@ -1,8 +1,6 @@
use std::{
convert::Infallible,
fmt,
hash::{DefaultHasher, Hash, Hasher},
io,
time::Duration,
};
@ -12,7 +10,7 @@ use clap::{
value_parser,
};
use crate::{
use super::{
file::Checked,
sharry::{NewShareRequest, Uri},
};
@ -81,12 +79,12 @@ impl fmt::Debug for Cli {
}
}
fn parse_seconds(data: &str) -> Result<Duration, Infallible> {
fn parse_seconds(data: &str) -> Result<Duration, String> {
data.parse().or(Ok(0)).map(Duration::from_secs)
}
fn parse_sharry_file(data: &str) -> io::Result<Checked> {
Checked::new(data)
fn parse_sharry_file(data: &str) -> Result<Checked, String> {
Checked::new(data).map_err(|e| e.to_string())
}
impl Cli {
@ -95,7 +93,7 @@ impl Cli {
}
pub fn get_uri(&self) -> Uri {
Uri::new(&self.protocol, &self.url)
Uri::with_protocol(&self.protocol, &self.url)
}
pub fn get_share_request(&self) -> NewShareRequest {

View file

@ -56,13 +56,12 @@ impl Checked {
pub fn start_upload(
self,
client: &impl sharry::Client,
uri: &sharry::Uri,
endpoint: &str,
alias_id: &str,
share_id: &str,
) -> sharry::Result<Uploading> {
let file_id = client.file_create(uri, alias_id, share_id, &self)?;
let patch_uri = client.file_create(endpoint, alias_id, self.get_name(), self.size)?;
Ok(Uploading::new(self.path, self.size, file_id))
Ok(Uploading::new(self.path, self.size, patch_uri))
}
}

View file

@ -1,15 +1,15 @@
use std::fmt;
pub struct Chunk<'t> {
file_id: String,
offset: u64,
data: &'t [u8],
patch_uri: String,
offset: u64,
}
impl fmt::Debug for Chunk<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Chunk")
.field("file_id", &self.file_id)
.field("patch_uri", &self.patch_uri)
.field("offset", &self.offset)
.field("data.len()", &self.data.len())
.finish_non_exhaustive()
@ -17,22 +17,14 @@ impl fmt::Debug for Chunk<'_> {
}
impl<'t> Chunk<'t> {
pub fn new(file_id: String, offset: u64, data: &'t [u8]) -> Self {
pub fn new(data: &'t [u8], patch_uri: String, offset: u64) -> Self {
Self {
file_id,
offset,
data,
patch_uri,
offset,
}
}
pub fn get_file_id(&self) -> &str {
&self.file_id
}
pub fn get_offset(&self) -> u64 {
self.offset
}
pub fn get_data(&self) -> &[u8] {
self.data
}
@ -46,7 +38,11 @@ impl<'t> Chunk<'t> {
u64::try_from(len).unwrap_or_else(|e| panic!("usize={len} did not fit into u64: {e}"))
}
pub fn get_behind(&self) -> u64 {
self.offset + self.get_length()
pub fn get_patch_uri(&self) -> &str {
&self.patch_uri
}
pub fn get_offset(&self) -> u64 {
self.offset
}
}

View file

@ -1,5 +1,5 @@
use std::{
fs,
fmt, fs,
io::{self, Read, Seek, SeekFrom},
path::PathBuf,
};
@ -9,22 +9,33 @@ use serde::{Deserialize, Serialize};
use super::{Chunk, FileTrait};
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize)]
pub struct Uploading {
path: PathBuf,
size: u64,
file_id: String,
#[serde(skip)]
patch_uri: String,
last_offset: Option<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 {
pub(super) fn new(path: PathBuf, size: u64, file_id: String) -> Self {
pub(super) fn new(path: PathBuf, size: u64, patch_uri: String) -> Self {
Self {
path,
size,
file_id,
patch_uri,
last_offset: None,
offset: 0,
}
@ -35,13 +46,11 @@ impl Uploading {
}
pub fn rewind(self) -> Option<Self> {
if let Some(last_offset) = self.last_offset {
Some(Self {
if let Some(last_offset) = self.last_offset { Some(Self {
last_offset: None,
offset: last_offset,
..self
})
} else {
}) } else {
warn!("attempted to rewind twice");
None
}
@ -60,7 +69,7 @@ impl Uploading {
));
}
let chunk = Chunk::new(self.file_id.clone(), self.offset, &buf[..read_len]);
let chunk = Chunk::new(&buf[..read_len], self.patch_uri.clone(), self.offset);
self.last_offset = Some(self.offset);
self.offset += chunk.get_length();

View file

@ -1,184 +0,0 @@
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()
)))
}
}
}

View file

@ -2,7 +2,6 @@ mod appstate;
mod cachefile;
mod cli;
mod file;
mod impl_ureq;
mod sharry;
use std::{
@ -48,20 +47,27 @@ fn prompt_continue() -> bool {
selection == 0
}
fn handle_error(e: &ClientError) {
if e.is_fatal() {
// react to fatal error
error!("fatal error: {e:?}");
eprintln!(
"{} {}",
fn print_error(e: &ClientError) {
if let Some(cause) = match e {
// known errors
ClientError::ResponseStatus { actual: 403, .. } => Some("Alias ID"),
ClientError::StdIo(_) => Some("URL"),
// unknown error
_ => None,
} {
// handle known error
info!("known error: {e:?}");
println!(
"{} probably wrong: {}",
style("Error!").red().bold(),
style(e.to_string()).cyan().italic(),
style(cause).cyan(),
);
process::exit(1);
println!("{}", style(e.to_string()).yellow().italic());
} else {
// handle unknown error
error!("unknown error: {e} ({e:?})");
println!("{}", style("Unknown Error!").red().bold());
}
// handle recoverable error
info!("recoverable error: {e:?}");
}
fn main() {
@ -110,7 +116,7 @@ fn main() {
state
}
Err(e) => {
handle_error(&e);
print_error(&e);
process::exit(1);
}
}
@ -137,10 +143,9 @@ fn main() {
Err(e) => {
// TODO better error handling (this will just retry endlessly)
// Error 404: Share might have been deleted
handle_error(&e);
error!("error: {e:?}");
if let Some(s) = state.rewind() {
trace!("State rewound, retrying last chunk");
state = s;
} else {
eprintln!("{} Failed to retry chunk!", style("Error:").red().bold());

View file

@ -9,40 +9,25 @@ pub struct Uri {
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 {
pub fn new(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 {
protocol: protocol.into(),
base_url: base_url.into(),
}
}
fn endpoint(&self, path: fmt::Arguments) -> String {
let uri = format!("{}://{}/api/v2/{path}", self.protocol, self.base_url);
pub fn endpoint(&self, endpoint: impl fmt::Display) -> String {
let uri = format!("{self}/{endpoint}");
trace!("endpoint: {uri:?}");
uri
}
}
pub fn share_create(&self) -> String {
self.endpoint(format_args!("alias/upload/new"))
}
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}"))
impl fmt::Display for Uri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}://{}/api/v2", self.protocol, self.base_url)
}
}
@ -73,7 +58,7 @@ impl NewShareRequest {
}
#[derive(Deserialize, Debug)]
pub struct NewShareResponse {
pub(super) struct NewShareResponse {
pub success: bool,
pub message: String,
pub id: String,
@ -81,7 +66,7 @@ pub struct NewShareResponse {
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct NotifyShareResponse {
pub(super) struct NotifyShareResponse {
pub success: bool,
pub message: String,
}

View file

@ -1,109 +1,27 @@
use std::{fmt, sync::LazyLock};
use std::fmt;
use regex::Regex;
use log::{debug, trace};
use thiserror::Error;
use crate::file;
use super::api::{NewShareRequest, NewShareResponse, NotifyShareResponse};
use super::api::{NewShareRequest, Uri};
/// 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(|| {
Regex::new(r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P<fid>[^/]+)$")
.expect("Regex compilation failed")
});
pub type Result<T> = std::result::Result<T, ClientError>;
pub trait Client {
fn get_file_id(uri: &str) -> super::Result<&str> {
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, endpoint: &str, alias_id: &str, data: NewShareRequest)
-> Result<String>;
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 share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()>;
fn file_create(
&self,
uri: &Uri,
endpoint: &str,
alias_id: &str,
share_id: &str,
file: &file::Checked,
) -> super::Result<String>;
file_name: &str,
file_size: u64,
) -> Result<String>;
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,
}
}
fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>;
}
#[derive(Debug, Error)]
@ -111,43 +29,164 @@ pub enum ClientError {
#[error(transparent)]
StdIo(#[from] std::io::Error),
#[error("response error: {0}")]
Response(String),
#[error("network request failed: {0}")]
Request(String),
#[error("Invalid {0}")]
InvalidParameter(Parameter),
#[error("unexpected response status: {actual} (expected {expected})")]
ResponseStatus { actual: u16, expected: u16 },
#[error("Unknown error: {0}")]
Unknown(String),
#[error("response parsing failed: {0}")]
ResponseParsing(String),
#[error("unexpected response content: {0}")]
ResponseContent(String),
}
impl ClientError {
pub fn res_status_check<T>(actual: T, expected: T) -> super::Result<()>
pub fn req_err(msg: impl fmt::Display) -> Self {
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
T: PartialEq + fmt::Display + Copy,
T: PartialEq + Into<u16> + Copy,
{
if actual == expected {
Ok(())
} else {
Err(Self::Response(format!(
"unexpected status: {actual} (expected {expected})"
)))
}
}
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,
Err(Self::ResponseStatus {
actual: actual.into(),
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
)))
}
}
}

View file

@ -1,7 +1,5 @@
mod api;
mod client;
pub use api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri};
pub use client::{Client, ClientError, Parameter};
pub type Result<T> = std::result::Result<T, ClientError>;
pub use api::{NewShareRequest, Uri};
pub use client::{Client, ClientError, Result};