Compare commits

..

No commits in common. "79bc8e67a71a2de40503837f611166f79db29f7d" and "783346c888d8551df9c4c3f796fecaa7d5584959" have entirely different histories.

6 changed files with 100 additions and 140 deletions

View file

@ -46,8 +46,10 @@
# Ideas
- cli functions
- max retries => stop
- "continue" and "new" flags to avoid user interaction
- "quiet" flag to disable output entirely
- "verbose" flag to adjust RUST_LOG for `shrupl` crate
- some switch to change log to "pretty-print"
- client error rework

View file

@ -11,7 +11,6 @@ use clap::{
builder::{PossibleValuesParser, TypedValueParser},
value_parser,
};
use log::LevelFilter;
use crate::{
file::Checked,
@ -37,10 +36,6 @@ pub struct Cli {
)]
protocol: String,
/// Number of times actions are retried
#[arg(short, long, default_value_t = 5, value_name = "N")]
retry_limit: u32,
/// Name of the new share
#[arg(short, long, default_value = "ShrUpl Upload", value_name = "TEXT")]
name: String,
@ -61,10 +56,6 @@ pub struct Cli {
)]
pub chunk_size: usize,
/// Increase output verbosity
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
/// Base URL for Sharry Instance
url: String,
@ -80,13 +71,11 @@ impl fmt::Debug for Cli {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Cli")
.field("uri", &self.get_uri())
.field("retry_limit", &self.retry_limit)
.field("alias", &self.alias)
.field("timeout", &self.get_timeout())
.field("chunk_size", &self.chunk_size)
.field("share_request", &self.get_share_request())
.field("files", &self.files)
.field("level_filter", &self.get_level_filter())
.field("hash", &self.get_hash())
.finish_non_exhaustive()
}
@ -109,27 +98,10 @@ impl Cli {
Uri::new(&self.protocol, &self.url)
}
pub fn may_retry(&self, tries: u32) -> bool {
match self.retry_limit {
0 => true,
limit => tries < limit,
}
}
pub fn get_share_request(&self) -> NewShareRequest {
NewShareRequest::new(&self.name, self.description.as_ref(), self.max_views)
}
pub fn get_level_filter(&self) -> LevelFilter {
match self.verbose {
0 => LevelFilter::Error,
1 => LevelFilter::Warn,
2 => LevelFilter::Info,
3 => LevelFilter::Debug,
_ => LevelFilter::Trace,
}
}
pub fn get_hash(&self) -> String {
let file_refs = {
let mut refs: Vec<_> = self.files.iter().collect();

View file

@ -33,7 +33,7 @@ fn find_cause(
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()))
ClientError::InvalidParameter(sharry::Parameter::URI(uri.to_string()))
} else {
error.into()
}

View file

@ -3,7 +3,6 @@ mod cachefile;
mod cli;
mod file;
mod impl_ureq;
mod output;
mod sharry;
use std::{
@ -16,13 +15,58 @@ use std::{
use clap::Parser;
use console::style;
use log::{info, trace};
use dialoguer::{Select, theme::ColorfulTheme};
use log::{error, info, trace};
use appstate::AppState;
use cli::Cli;
use output::{Log, SHRUPL, prompt_continue};
use sharry::ClientError;
fn prompt_continue() -> bool {
let prompt = format!(
"This operation has previously been stopped. {}",
style("How to proceed?").cyan()
);
let choices = [
format!("Load and {}", style("continue operation").green().bold()),
format!("Start a {}", style("new operation").cyan().bold()),
format!("Quit {}", style("ShrUpl").yellow().bold()),
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt(prompt)
.default(0)
.items(&choices)
.interact()
.unwrap_or(2);
if selection == 2 {
process::exit(255);
}
selection == 0
}
fn handle_error(e: &ClientError) {
if e.is_fatal() {
// react to fatal error
error!("fatal error: {e:?}");
eprintln!(
"{} {}",
style("Error!").red().bold(),
style(e.to_string()).cyan().italic(),
);
process::exit(1);
}
// handle recoverable error
info!("recoverable error: {e:?}");
}
fn main() {
env_logger::init();
let check_ctrlc = {
let stop = Arc::new(AtomicBool::new(false));
let stop_ctrlc = stop.clone();
@ -41,15 +85,13 @@ fn main() {
};
let args = Cli::parse();
env_logger::Builder::new()
.filter_module("shrupl", args.get_level_filter())
.parse_default_env()
.init();
info!("args: {args:#?}");
println!("{} to {}!", style("Welcome").magenta().bold(), *SHRUPL);
println!(
"{} to {}!",
style("Welcome").magenta().bold(),
style("ShrUpl").yellow().bold(),
);
let mut state = AppState::try_resume(&args)
.and_then(|state| prompt_continue().then_some(state))
@ -59,49 +101,54 @@ fn main() {
match AppState::from_args(&args) {
Ok(state) => {
state.save().unwrap_or_else(|e| {
Log::warning(format_args!("Failed to save state: {e}"));
eprintln!(
"{} Failed to save {} state: {e}",
style("Warning:").red().bold(),
style("ShrUpl").yellow().bold(),
);
});
state
}
Err(e) => {
Log::handle(&e);
Log::error(format_args!("Failed to create state: {e}"));
handle_error(&e);
process::exit(1);
}
}
});
info!("continuing with state: {state:#?}");
let fns_magenta = output::style_all(&state.file_names(), |s| style(s).magenta()).join(", ");
let fns_magenta = state
.file_names()
.iter()
.map(|&n| style(n).magenta().to_string())
.collect::<Vec<_>>()
.join(", ");
println!("{} is uploading: {fns_magenta}", *SHRUPL);
println!(
"{} is uploading: {fns_magenta}",
style("ShrUpl").yellow().bold(),
);
let mut buffer = vec![0; args.chunk_size * 1024 * 1024];
let mut tries = 0;
loop {
if !args.may_retry(tries) {
Log::error("Retry limit reached!");
}
match state.upload_chunk(&mut buffer) {
Err(e) => {
// TODO better error handling (this will just retry endlessly)
// Error 404: Share might have been deleted
Log::handle(&e);
handle_error(&e);
if let Some(s) = state.rewind() {
tries += 1;
trace!("State rewound, retrying last chunk (tried: {tries})");
trace!("State rewound, retrying last chunk");
state = s;
} else {
Log::error("Failed to retry chunk!");
eprintln!("{} Failed to retry chunk!", style("Error:").red().bold());
process::exit(1);
}
}
Ok(false) => {
trace!("chunk uploaded");
tries = 0;
}
Ok(true) => {
info!("all uploads done");
@ -110,14 +157,26 @@ fn main() {
}
state.save().unwrap_or_else(|e| {
Log::warning(format_args!("Failed to save state: {e}"));
eprintln!(
"{} Failed to save {} state: {e}",
style("Warning:").red().bold(),
style("ShrUpl").yellow().bold(),
);
});
check_ctrlc();
}
state.clear().unwrap_or_else(|e| {
Log::warning(format_args!("Failed to remove state: {e}"));
eprintln!(
"{} Failed to remove {} state: {e}",
style("Warning:").red().bold(),
style("ShrUpl").yellow().bold(),
);
});
println!("{} finished {}", *SHRUPL, style("successfully!").green());
println!(
"{} finished {}",
style("ShrUpl").yellow().bold(),
style("successfully!").green()
);
}

View file

@ -1,72 +0,0 @@
use std::{fmt, process, sync::LazyLock};
use console::{StyledObject, style};
use dialoguer::{Select, theme::ColorfulTheme};
use log::{error, info};
use crate::sharry;
type StaticStyled<'t> = LazyLock<StyledObject<&'t str>>;
pub static SHRUPL: StaticStyled = LazyLock::new(|| style("ShrUpl").yellow().bold());
pub fn prompt_continue() -> bool {
let prompt = format!(
"This operation has previously been stopped. {}",
style("How to proceed?").cyan()
);
let choices = [
format!("Load and {}", style("continue operation").green().bold()),
format!("Start a {}", style("new operation").cyan().bold()),
format!("Quit {}", *SHRUPL),
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt(prompt)
.default(0)
.items(&choices)
.interact()
.unwrap_or(2);
if selection == 2 {
process::exit(0);
}
selection == 0
}
pub fn style_all<'t, F>(strs: &[&'t str], f: F) -> Vec<String>
where
F: Fn(&'t str) -> StyledObject<&'t str>,
{
strs.iter().map(|&s| f(s).to_string()).collect()
}
pub enum Log {}
impl Log {
fn eprintln(kind: impl fmt::Display, msg: impl fmt::Display) {
eprintln!("{} {}: {}", *SHRUPL, kind, style(msg).cyan().italic(),);
}
pub fn warning(msg: impl fmt::Display) {
Self::eprintln(style("Warning").magenta().bold(), msg);
}
pub fn error(msg: impl fmt::Display) -> ! {
Self::eprintln(style("Error").red().bold(), msg);
process::exit(1);
}
pub fn handle(e: &sharry::ClientError) {
if e.is_fatal() {
// react to fatal error
error!("fatal error: {e:?}");
Self::error(e);
}
// handle recoverable error
info!("recoverable error: {e:?}");
}
}

View file

@ -34,7 +34,8 @@ pub trait Client {
Ok(fid)
} else {
Err(super::ClientError::unknown(format!(
"Could not extract File ID from {uri:?}"
"Could not extract File ID from {:?}",
uri
)))
}
}
@ -89,7 +90,7 @@ pub trait Client {
#[derive(Debug, Error)]
pub enum Parameter {
#[error("given URI {0:?}")]
Uri(String),
URI(String),
#[error("given Alias ID {0:?}")]
AliasID(String),
@ -103,7 +104,10 @@ pub enum Parameter {
impl Parameter {
fn is_fatal(&self) -> bool {
matches!(self, Self::Uri(_) | Self::AliasID(_))
match self {
Self::URI(_) | Self::AliasID(_) => true,
_ => false,
}
}
}
@ -122,11 +126,6 @@ pub enum ClientError {
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<T>(actual: T, expected: T) -> super::Result<()>
where
@ -142,11 +141,11 @@ impl ClientError {
}
pub fn response(e: impl ToString) -> Self {
Self::Response(into_string(e))
Self::Response(e.to_string())
}
pub fn unknown(e: impl ToString) -> Self {
Self::Unknown(into_string(e))
Self::Unknown(e.to_string())
}
pub fn is_fatal(&self) -> bool {