diff --git a/.vscode/tasks.json b/.vscode/tasks.json index db25719..ceeafb4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -32,6 +32,9 @@ "command": "clippy", "args": [ "--fix", + "--lib", + "--bin", + "shrupl", "--allow-dirty", "--allow-staged", "--", diff --git a/src/appstate.rs b/src/appstate.rs index e04e8d6..126059a 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -6,9 +6,10 @@ use log::{debug, info, warn}; use crate::{ cachefile::CacheFile, cli::Cli, + error, file::{Chunk, FileTrait}, output::new_progressbar, - sharry::{self, Client}, + sharry::Client, }; pub struct AppState { @@ -41,12 +42,12 @@ impl AppState { } } - pub fn try_resume(args: &Cli) -> sharry::Result { - fn check_hash<'a>(file: &'a impl FileTrait<'a>, bar: &ProgressBar) -> sharry::Result<()> { + pub fn try_resume(args: &Cli) -> error::Result { + fn check_hash<'a>(file: &'a impl FileTrait<'a>, bar: &ProgressBar) -> error::Result<()> { bar.set_message(format!("checking {:?}", file.get_name())); match file.check_hash(|bytes| bar.inc(bytes)) { Ok(true) => Ok(()), - Ok(false) => Err(sharry::ClientError::unknown(format!( + Ok(false) => Err(error::Error::unknown(format!( "Hash mismatch for file {:?}!", file.get_name() ))), @@ -90,7 +91,7 @@ impl AppState { Ok(Self::new(new_http(args.get_timeout()), inner)) } - pub fn from_args(args: &Cli) -> sharry::Result { + pub fn from_args(args: &Cli) -> error::Result { // TODO CLI switch begin let mut files = args.files.clone(); @@ -148,7 +149,7 @@ impl AppState { self.with_progressbar(f, true); } - fn next_chunk<'t>(&mut self, buffer: &'t mut [u8]) -> sharry::Result>> { + fn next_chunk<'t>(&mut self, buffer: &'t mut [u8]) -> error::Result>> { if self.inner.get_uploading(&self.http)?.is_none() { return Ok(None); } @@ -164,7 +165,7 @@ impl AppState { Ok(Some(chunk)) } - pub fn upload_chunk(&mut self, buffer: &mut [u8]) -> sharry::Result { + pub fn upload_chunk(&mut self, buffer: &mut [u8]) -> error::Result { let Some(chunk) = self.next_chunk(buffer)? else { self.inner .share_notify(&self.http) @@ -185,6 +186,7 @@ impl AppState { Ok(self.inner.peek_uploading().is_none() && self.inner.queue().is_empty()) } + #[must_use] pub fn rewind_chunk(mut self) -> Option { self.inner = self.inner.rewind_chunk()?; @@ -196,7 +198,7 @@ impl AppState { self.drop_progressbar(ProgressBar::abandon); } - pub fn rebuild_share(self, args: &Cli) -> sharry::Result { + pub fn rebuild_share(self, args: &Cli) -> error::Result { let share_id = self.http .share_create(&args.get_uri(), &args.alias, args.get_share_request())?; @@ -215,4 +217,8 @@ impl AppState { pub fn discard(self) -> io::Result<()> { self.inner.discard() } + + pub fn clear_any(args: &Cli) { + CacheFile::clear_any(args); + } } diff --git a/src/main.rs b/src/bin/shrupl.rs similarity index 85% rename from src/main.rs rename to src/bin/shrupl.rs index e1b8655..214aaea 100644 --- a/src/main.rs +++ b/src/bin/shrupl.rs @@ -1,11 +1,3 @@ -mod appstate; -mod cachefile; -mod cli; -mod file; -mod impl_ureq; -mod output; -mod sharry; - use std::{ process, sync::{ @@ -16,12 +8,12 @@ use std::{ use clap::Parser; use console::{StyledObject, style}; -use log::{info, trace}; +use log::{debug, info, trace}; -use appstate::AppState; -use cli::Cli; -use output::{Log, SHRUPL}; -use sharry::{ClientError, Parameter}; +use shrupl::{ + AppState, Cli, error, + output::{self, Log, SHRUPL}, +}; fn main() { let args = Cli::parse(); @@ -56,7 +48,7 @@ fn main() { let mut state = resumed .inspect_err(|e| { - cachefile::CacheFile::clear_any(&args); + AppState::clear_any(&args); Log::handle(e); info!("could not resume from hash {:?}: {e}", args.get_hash()); }) @@ -94,18 +86,18 @@ fn main() { Err(e) => { Log::handle(&e); - if let ClientError::InvalidParameter(p) = e { + if let error::Error::InvalidParameter(p) = e { match p { // Error 404 (File not found) - Parameter::FileID(fid) => { - trace!("requeueing file {fid:?}"); + error::Parameter::FileID(fid) => { + debug!("requeueing file {fid:?}"); state.abort_upload(); } // Error 404 (Share not found) - Parameter::ShareID(sid) => { + error::Parameter::ShareID(sid) => { // TODO ask - trace!("rebuilding share {sid:?}"); + debug!("rebuilding share {sid:?}"); // rebuild share let Ok(s) = state.rebuild_share(&args) else { @@ -122,7 +114,7 @@ fn main() { }; tries += 1; - trace!("State rewound, retrying last chunk (tries: {tries})"); + debug!("State rewound, retrying last chunk (tries: {tries})"); state = s; } } diff --git a/src/cachefile.rs b/src/cachefile.rs index b91eb88..3860fe1 100644 --- a/src/cachefile.rs +++ b/src/cachefile.rs @@ -10,8 +10,9 @@ use serde::{Deserialize, Serialize}; use crate::{ cli::Cli, + error, file::{self, Chunk}, - sharry::{self, Client, Uri}, + sharry::{Client, Uri}, }; #[derive(Serialize, Deserialize, Debug)] @@ -74,7 +75,7 @@ impl CacheFile { pub fn get_uploading( &mut self, client: &impl Client, - ) -> sharry::Result> { + ) -> error::Result> { if self.uploading.is_some() { Ok(self.uploading.as_mut()) } else if let Some(chk) = self.files.pop_front() { @@ -128,11 +129,11 @@ impl CacheFile { ); } - pub fn share_notify(&self, client: &impl Client) -> sharry::Result<()> { + pub fn share_notify(&self, client: &impl Client) -> error::Result<()> { client.share_notify(&self.uri, &self.alias_id, &self.share_id) } - pub fn file_patch(&self, client: &impl Client, chunk: &Chunk) -> sharry::Result<()> { + pub fn file_patch(&self, client: &impl Client, chunk: &Chunk) -> error::Result<()> { client.file_patch(&self.uri, &self.alias_id, &self.share_id, chunk) } diff --git a/src/cli.rs b/src/cli.rs index 82d0bd0..ad96bb5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -107,14 +107,17 @@ where } impl Cli { + #[must_use] pub fn get_timeout(&self) -> Option { (!self.timeout.is_zero()).then_some(self.timeout) } + #[must_use] pub fn get_uri(&self) -> Uri { Uri::new(&self.protocol, &self.url) } + #[must_use] pub fn may_retry(&self, tries: u32) -> bool { match self.retry_limit { 0 => true, @@ -122,10 +125,12 @@ impl Cli { } } + #[must_use] pub fn get_share_request(&self) -> NewShareRequest { NewShareRequest::new(&self.name, self.description.as_ref(), self.max_views) } + #[must_use] pub fn get_level_filter(&self) -> LevelFilter { match self.verbose { 0 => LevelFilter::Error, @@ -140,6 +145,7 @@ impl Cli { self.files.iter().map(FileTrait::get_name).collect() } + #[must_use] pub fn get_hash(&self) -> String { let mut hasher = Blake2b::new().hash_length(16).to_state(); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..515f567 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,76 @@ +use std::fmt; + +#[derive(Debug, thiserror::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 { + matches!(self, Self::Uri(_) | Self::AliasID(_)) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + StdIo(#[from] std::io::Error), + + #[error("response error: {0}")] + Response(String), + + #[error("Invalid {0}")] + InvalidParameter(Parameter), + + #[error("Unknown error: {0}")] + Unknown(String), +} + +#[allow(clippy::needless_pass_by_value)] +fn into_string(val: impl ToString) -> String { + val.to_string() +} + +impl Error { + pub fn res_status_check(actual: T, expected: T) -> Result<()> + where + T: PartialEq + fmt::Display + Copy, + { + if actual == expected { + Ok(()) + } else { + Err(Self::Response(format!( + "unexpected status: {actual} (expected {expected})" + ))) + } + } + + pub fn response(e: impl ToString) -> Self { + Self::Response(into_string(e)) + } + + pub fn unknown(e: impl ToString) -> Self { + Self::Unknown(into_string(e)) + } + + #[must_use] + pub fn is_fatal(&self) -> bool { + match self { + Self::InvalidParameter(p) => p.is_fatal(), + Self::Unknown(_) => true, + _ => false, + } + } +} + +pub type Result = std::result::Result; diff --git a/src/file/checked.rs b/src/file/checked.rs index 689bdb3..07688f7 100644 --- a/src/file/checked.rs +++ b/src/file/checked.rs @@ -5,7 +5,7 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::sharry; +use crate::{error, sharry}; use super::{FileTrait, Uploading}; @@ -81,7 +81,7 @@ impl Checked { uri: &sharry::Uri, alias_id: &str, share_id: &str, - ) -> sharry::Result { + ) -> error::Result { let file_id = client.file_create(uri, alias_id, share_id, &self)?; Ok(Uploading::new(self.path, self.size, self.hash, file_id)) diff --git a/src/impl_ureq.rs b/src/impl_ureq.rs index ccccfb2..1ffd188 100644 --- a/src/impl_ureq.rs +++ b/src/impl_ureq.rs @@ -1,8 +1,9 @@ use log::{debug, trace}; use crate::{ + error, file::{self, FileTrait}, - sharry::{self, ClientError, Uri}, + sharry::{self, Uri}, }; fn find_cause( @@ -10,22 +11,22 @@ fn find_cause( alias_id: &str, share_id: Option<&str>, file_id: Option<&str>, -) -> impl FnOnce(ureq::Error) -> ClientError { +) -> impl FnOnce(ureq::Error) -> error::Error { move |error| match error { ureq::Error::StatusCode(403) => { trace!("HTTP Error 403: Alias not found!"); - ClientError::InvalidParameter(sharry::Parameter::AliasID(alias_id.to_owned())) + error::Error::InvalidParameter(error::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())) + error::Error::InvalidParameter(error::Parameter::FileID(file_id.to_owned())) } else if let Some(share_id) = share_id { - ClientError::InvalidParameter(sharry::Parameter::ShareID(share_id.to_owned())) + error::Error::InvalidParameter(error::Parameter::ShareID(share_id.to_owned())) } else { - ClientError::unknown(error) + error::Error::InvalidParameter(error::Parameter::Uri(uri.to_string())) } } ureq::Error::Io(error) => { @@ -33,7 +34,7 @@ fn find_cause( if let Some(msg) = error.get_ref().map(ToString::to_string) { if msg.starts_with("failed to lookup address information") { - ClientError::InvalidParameter(sharry::Parameter::Uri(uri.to_string())) + error::Error::InvalidParameter(error::Parameter::Uri(uri.to_string())) } else { error.into() } @@ -41,7 +42,7 @@ fn find_cause( error.into() } } - error => ClientError::unknown(error), + error => error::Error::unknown(error), } } @@ -51,7 +52,7 @@ impl sharry::Client for ureq::Agent { uri: &Uri, alias_id: &str, data: sharry::NewShareRequest, - ) -> sharry::Result { + ) -> error::Result { let res = { let endpoint = uri.share_create(); @@ -62,11 +63,11 @@ impl sharry::Client for ureq::Agent { .map_err(find_cause(uri, alias_id, None, None))?; trace!("{endpoint:?} response: {res:?}"); - ClientError::res_status_check(res.status(), ureq::http::StatusCode::OK)?; + error::Error::res_status_check(res.status(), ureq::http::StatusCode::OK)?; res.body_mut() .read_json::() - .map_err(ClientError::response)? + .map_err(error::Error::response)? }; debug!("{res:?}"); @@ -76,11 +77,11 @@ impl sharry::Client for ureq::Agent { Ok(res.id) } else { - Err(ClientError::response(format!("{res:?}"))) + Err(error::Error::response(format!("{res:?}"))) } } - fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> sharry::Result<()> { + fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> error::Result<()> { let res = { let endpoint = uri.share_notify(share_id); @@ -91,11 +92,11 @@ impl sharry::Client for ureq::Agent { .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)?; + error::Error::res_status_check(res.status(), ureq::http::StatusCode::OK)?; res.body_mut() .read_json::() - .map_err(ClientError::response)? + .map_err(error::Error::response)? }; debug!("{res:?}"); @@ -109,7 +110,7 @@ impl sharry::Client for ureq::Agent { alias_id: &str, share_id: &str, file: &file::Checked, - ) -> sharry::Result { + ) -> error::Result { let res = { let endpoint = uri.file_create(share_id); @@ -122,14 +123,14 @@ impl sharry::Client for ureq::Agent { .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)?; + error::Error::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"))? + .ok_or_else(|| error::Error::response("Location header not found"))? .to_str() - .map_err(ClientError::response)? + .map_err(error::Error::response)? .to_string(); let file_id = Self::get_file_id(&location)?; @@ -145,7 +146,7 @@ impl sharry::Client for ureq::Agent { alias_id: &str, share_id: &str, chunk: &file::Chunk, - ) -> sharry::Result<()> { + ) -> error::Result<()> { let res = { let endpoint = uri.file_patch(share_id, chunk.get_file_id()); @@ -162,21 +163,21 @@ impl sharry::Client for ureq::Agent { ))?; trace!("{endpoint:?} response: {res:?}"); - ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?; + error::Error::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"))? + .ok_or_else(|| error::Error::response("Upload-Offset header not found"))? .to_str() - .map_err(ClientError::response)? + .map_err(error::Error::response)? .parse::() - .map_err(ClientError::response)?; + .map_err(error::Error::response)?; if chunk.get_behind() == res_offset { Ok(()) } else { - Err(ClientError::response(format!( + Err(error::Error::response(format!( "Unexpected Upload-Offset: {} (expected {})", res_offset, chunk.get_behind() diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d28d9f2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +// TODO fix with documentation +#![allow(clippy::missing_errors_doc)] + +mod appstate; +mod cachefile; +mod cli; +pub mod error; +mod file; +mod impl_ureq; +pub mod output; +mod sharry; + +pub use appstate::AppState; +pub use cli::Cli; diff --git a/src/output.rs b/src/output.rs index 73bd80c..7e52946 100644 --- a/src/output.rs +++ b/src/output.rs @@ -5,12 +5,11 @@ use dialoguer::{Select, theme::ColorfulTheme}; use indicatif::{ProgressBar, ProgressStyle}; use log::{error, info}; -use crate::sharry; - type StaticStyled<'t> = LazyLock>; pub static SHRUPL: StaticStyled = LazyLock::new(|| style("ShrUpl").yellow().bold()); +#[must_use] pub fn prompt_continue() -> bool { let prompt = format!( "This operation has previously been stopped. {}", @@ -44,6 +43,8 @@ where strs.iter().map(|&s| f(style(s)).to_string()).collect() } +#[must_use] +#[allow(clippy::missing_panics_doc)] pub fn new_progressbar() -> ProgressBar { ProgressBar::hidden().with_style( ProgressStyle::with_template(&format!( @@ -74,7 +75,7 @@ impl Log { process::exit(1); } - pub fn handle(e: &sharry::ClientError) { + pub fn handle(e: &crate::error::Error) { if e.is_fatal() { // react to fatal error error!("fatal error: {e:?}"); diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 6f87916..517a40a 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -1,15 +1,14 @@ -use std::{fmt, sync::LazyLock}; +use std::sync::LazyLock; use log::trace; use regex::Regex; -use thiserror::Error; -use crate::file; +use crate::{error, file}; use super::api::{NewShareRequest, Uri}; pub trait Client { - fn get_file_id(uri: &str) -> super::Result<&str> { + fn get_file_id(uri: &str) -> error::Result<&str> { /// Pattern breakdown: /// - `^([^:/?#]+)://` – scheme (anything but `:/?#`) + `"://"` /// - `([^/?#]+)` – authority/host (anything but `/?#`) @@ -33,7 +32,7 @@ pub trait Client { { Ok(fid) } else { - Err(super::ClientError::unknown(format!( + Err(error::Error::unknown(format!( "Could not extract File ID from {uri:?}" ))) } @@ -44,9 +43,9 @@ pub trait Client { uri: &Uri, alias_id: &str, data: NewShareRequest, - ) -> super::Result; + ) -> error::Result; - fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> super::Result<()>; + fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> error::Result<()>; fn file_create( &self, @@ -54,7 +53,7 @@ pub trait Client { alias_id: &str, share_id: &str, file: &file::Checked, - ) -> super::Result; + ) -> error::Result; fn file_patch( &self, @@ -62,7 +61,7 @@ pub trait Client { alias_id: &str, share_id: &str, chunk: &file::Chunk, - ) -> super::Result<()>; + ) -> error::Result<()>; } // TODO move into tests subdir @@ -85,75 +84,3 @@ pub trait Client { // 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 { - matches!(self, Self::Uri(_) | Self::AliasID(_)) - } -} - -#[derive(Debug, Error)] -pub enum ClientError { - #[error(transparent)] - StdIo(#[from] std::io::Error), - - #[error("response error: {0}")] - Response(String), - - #[error("Invalid {0}")] - InvalidParameter(Parameter), - - #[error("{0}")] - Unknown(String), -} - -#[allow(clippy::needless_pass_by_value)] -fn into_string(val: impl ToString) -> String { - val.to_string() -} - -impl ClientError { - pub fn res_status_check(actual: T, expected: T) -> super::Result<()> - where - T: PartialEq + fmt::Display + Copy, - { - if actual == expected { - Ok(()) - } else { - Err(Self::Response(format!( - "unexpected status: {actual} (expected {expected})" - ))) - } - } - - pub fn response(e: impl ToString) -> Self { - Self::Response(into_string(e)) - } - - pub fn unknown(e: impl ToString) -> Self { - Self::Unknown(into_string(e)) - } - - pub fn is_fatal(&self) -> bool { - match self { - Self::InvalidParameter(p) => p.is_fatal(), - Self::Unknown(_) => true, - _ => false, - } - } -} diff --git a/src/sharry/mod.rs b/src/sharry/mod.rs index 87a65f5..5da85d2 100644 --- a/src/sharry/mod.rs +++ b/src/sharry/mod.rs @@ -2,6 +2,4 @@ mod api; mod client; pub use api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri}; -pub use client::{Client, ClientError, Parameter}; - -pub type Result = std::result::Result; +pub use client::Client;