Compare commits
5 commits
783346c888
...
79bc8e67a7
| Author | SHA1 | Date | |
|---|---|---|---|
| 79bc8e67a7 | |||
| 4eb0627a5f | |||
| a4bef827d1 | |||
| a633f4e228 | |||
| 01bcf92d9c |
6 changed files with 140 additions and 100 deletions
2
notes.md
2
notes.md
|
|
@ -46,10 +46,8 @@
|
||||||
# Ideas
|
# Ideas
|
||||||
|
|
||||||
- cli functions
|
- cli functions
|
||||||
- max retries => stop
|
|
||||||
- "continue" and "new" flags to avoid user interaction
|
- "continue" and "new" flags to avoid user interaction
|
||||||
- "quiet" flag to disable output entirely
|
- "quiet" flag to disable output entirely
|
||||||
- "verbose" flag to adjust RUST_LOG for `shrupl` crate
|
|
||||||
- some switch to change log to "pretty-print"
|
- some switch to change log to "pretty-print"
|
||||||
|
|
||||||
- client error rework
|
- client error rework
|
||||||
|
|
|
||||||
28
src/cli.rs
28
src/cli.rs
|
|
@ -11,6 +11,7 @@ use clap::{
|
||||||
builder::{PossibleValuesParser, TypedValueParser},
|
builder::{PossibleValuesParser, TypedValueParser},
|
||||||
value_parser,
|
value_parser,
|
||||||
};
|
};
|
||||||
|
use log::LevelFilter;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
file::Checked,
|
file::Checked,
|
||||||
|
|
@ -36,6 +37,10 @@ pub struct Cli {
|
||||||
)]
|
)]
|
||||||
protocol: String,
|
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
|
/// Name of the new share
|
||||||
#[arg(short, long, default_value = "ShrUpl Upload", value_name = "TEXT")]
|
#[arg(short, long, default_value = "ShrUpl Upload", value_name = "TEXT")]
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -56,6 +61,10 @@ pub struct Cli {
|
||||||
)]
|
)]
|
||||||
pub chunk_size: usize,
|
pub chunk_size: usize,
|
||||||
|
|
||||||
|
/// Increase output verbosity
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||||
|
verbose: u8,
|
||||||
|
|
||||||
/// Base URL for Sharry Instance
|
/// Base URL for Sharry Instance
|
||||||
url: String,
|
url: String,
|
||||||
|
|
||||||
|
|
@ -71,11 +80,13 @@ impl fmt::Debug for Cli {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.debug_struct("Cli")
|
f.debug_struct("Cli")
|
||||||
.field("uri", &self.get_uri())
|
.field("uri", &self.get_uri())
|
||||||
|
.field("retry_limit", &self.retry_limit)
|
||||||
.field("alias", &self.alias)
|
.field("alias", &self.alias)
|
||||||
.field("timeout", &self.get_timeout())
|
.field("timeout", &self.get_timeout())
|
||||||
.field("chunk_size", &self.chunk_size)
|
.field("chunk_size", &self.chunk_size)
|
||||||
.field("share_request", &self.get_share_request())
|
.field("share_request", &self.get_share_request())
|
||||||
.field("files", &self.files)
|
.field("files", &self.files)
|
||||||
|
.field("level_filter", &self.get_level_filter())
|
||||||
.field("hash", &self.get_hash())
|
.field("hash", &self.get_hash())
|
||||||
.finish_non_exhaustive()
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
|
|
@ -98,10 +109,27 @@ impl Cli {
|
||||||
Uri::new(&self.protocol, &self.url)
|
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 {
|
pub fn get_share_request(&self) -> NewShareRequest {
|
||||||
NewShareRequest::new(&self.name, self.description.as_ref(), self.max_views)
|
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 {
|
pub fn get_hash(&self) -> String {
|
||||||
let file_refs = {
|
let file_refs = {
|
||||||
let mut refs: Vec<_> = self.files.iter().collect();
|
let mut refs: Vec<_> = self.files.iter().collect();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ fn find_cause(
|
||||||
|
|
||||||
if let Some(msg) = error.get_ref().map(ToString::to_string) {
|
if let Some(msg) = error.get_ref().map(ToString::to_string) {
|
||||||
if msg == "failed to lookup address information: Name does not resolve" {
|
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 {
|
} else {
|
||||||
error.into()
|
error.into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
117
src/main.rs
117
src/main.rs
|
|
@ -3,6 +3,7 @@ mod cachefile;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod file;
|
mod file;
|
||||||
mod impl_ureq;
|
mod impl_ureq;
|
||||||
|
mod output;
|
||||||
mod sharry;
|
mod sharry;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -15,58 +16,13 @@ use std::{
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use console::style;
|
use console::style;
|
||||||
use dialoguer::{Select, theme::ColorfulTheme};
|
use log::{info, trace};
|
||||||
use log::{error, info, trace};
|
|
||||||
|
|
||||||
use appstate::AppState;
|
use appstate::AppState;
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
use sharry::ClientError;
|
use output::{Log, SHRUPL, prompt_continue};
|
||||||
|
|
||||||
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() {
|
fn main() {
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
let check_ctrlc = {
|
let check_ctrlc = {
|
||||||
let stop = Arc::new(AtomicBool::new(false));
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
let stop_ctrlc = stop.clone();
|
let stop_ctrlc = stop.clone();
|
||||||
|
|
@ -85,13 +41,15 @@ fn main() {
|
||||||
};
|
};
|
||||||
|
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
|
|
||||||
|
env_logger::Builder::new()
|
||||||
|
.filter_module("shrupl", args.get_level_filter())
|
||||||
|
.parse_default_env()
|
||||||
|
.init();
|
||||||
|
|
||||||
info!("args: {args:#?}");
|
info!("args: {args:#?}");
|
||||||
|
|
||||||
println!(
|
println!("{} to {}!", style("Welcome").magenta().bold(), *SHRUPL);
|
||||||
"{} to {}!",
|
|
||||||
style("Welcome").magenta().bold(),
|
|
||||||
style("ShrUpl").yellow().bold(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut state = AppState::try_resume(&args)
|
let mut state = AppState::try_resume(&args)
|
||||||
.and_then(|state| prompt_continue().then_some(state))
|
.and_then(|state| prompt_continue().then_some(state))
|
||||||
|
|
@ -101,54 +59,49 @@ fn main() {
|
||||||
match AppState::from_args(&args) {
|
match AppState::from_args(&args) {
|
||||||
Ok(state) => {
|
Ok(state) => {
|
||||||
state.save().unwrap_or_else(|e| {
|
state.save().unwrap_or_else(|e| {
|
||||||
eprintln!(
|
Log::warning(format_args!("Failed to save state: {e}"));
|
||||||
"{} Failed to save {} state: {e}",
|
|
||||||
style("Warning:").red().bold(),
|
|
||||||
style("ShrUpl").yellow().bold(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
handle_error(&e);
|
Log::handle(&e);
|
||||||
process::exit(1);
|
Log::error(format_args!("Failed to create state: {e}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("continuing with state: {state:#?}");
|
info!("continuing with state: {state:#?}");
|
||||||
|
|
||||||
let fns_magenta = state
|
let fns_magenta = output::style_all(&state.file_names(), |s| style(s).magenta()).join(", ");
|
||||||
.file_names()
|
|
||||||
.iter()
|
|
||||||
.map(|&n| style(n).magenta().to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
println!(
|
println!("{} is uploading: {fns_magenta}", *SHRUPL);
|
||||||
"{} is uploading: {fns_magenta}",
|
|
||||||
style("ShrUpl").yellow().bold(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut buffer = vec![0; args.chunk_size * 1024 * 1024];
|
let mut buffer = vec![0; args.chunk_size * 1024 * 1024];
|
||||||
|
let mut tries = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
if !args.may_retry(tries) {
|
||||||
|
Log::error("Retry limit reached!");
|
||||||
|
}
|
||||||
|
|
||||||
match state.upload_chunk(&mut buffer) {
|
match state.upload_chunk(&mut buffer) {
|
||||||
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
|
||||||
handle_error(&e);
|
Log::handle(&e);
|
||||||
|
|
||||||
if let Some(s) = state.rewind() {
|
if let Some(s) = state.rewind() {
|
||||||
trace!("State rewound, retrying last chunk");
|
tries += 1;
|
||||||
|
trace!("State rewound, retrying last chunk (tried: {tries})");
|
||||||
|
|
||||||
state = s;
|
state = s;
|
||||||
} else {
|
} else {
|
||||||
eprintln!("{} Failed to retry chunk!", style("Error:").red().bold());
|
Log::error("Failed to retry chunk!");
|
||||||
process::exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
trace!("chunk uploaded");
|
trace!("chunk uploaded");
|
||||||
|
tries = 0;
|
||||||
}
|
}
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
info!("all uploads done");
|
info!("all uploads done");
|
||||||
|
|
@ -157,26 +110,14 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.save().unwrap_or_else(|e| {
|
state.save().unwrap_or_else(|e| {
|
||||||
eprintln!(
|
Log::warning(format_args!("Failed to save state: {e}"));
|
||||||
"{} Failed to save {} state: {e}",
|
|
||||||
style("Warning:").red().bold(),
|
|
||||||
style("ShrUpl").yellow().bold(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
check_ctrlc();
|
check_ctrlc();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.clear().unwrap_or_else(|e| {
|
state.clear().unwrap_or_else(|e| {
|
||||||
eprintln!(
|
Log::warning(format_args!("Failed to remove state: {e}"));
|
||||||
"{} Failed to remove {} state: {e}",
|
|
||||||
style("Warning:").red().bold(),
|
|
||||||
style("ShrUpl").yellow().bold(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
println!(
|
println!("{} finished {}", *SHRUPL, style("successfully!").green());
|
||||||
"{} finished {}",
|
|
||||||
style("ShrUpl").yellow().bold(),
|
|
||||||
style("successfully!").green()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
72
src/output.rs
Normal file
72
src/output.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,8 +34,7 @@ pub trait Client {
|
||||||
Ok(fid)
|
Ok(fid)
|
||||||
} else {
|
} else {
|
||||||
Err(super::ClientError::unknown(format!(
|
Err(super::ClientError::unknown(format!(
|
||||||
"Could not extract File ID from {:?}",
|
"Could not extract File ID from {uri:?}"
|
||||||
uri
|
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +89,7 @@ pub trait Client {
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Parameter {
|
pub enum Parameter {
|
||||||
#[error("given URI {0:?}")]
|
#[error("given URI {0:?}")]
|
||||||
URI(String),
|
Uri(String),
|
||||||
|
|
||||||
#[error("given Alias ID {0:?}")]
|
#[error("given Alias ID {0:?}")]
|
||||||
AliasID(String),
|
AliasID(String),
|
||||||
|
|
@ -104,10 +103,7 @@ pub enum Parameter {
|
||||||
|
|
||||||
impl Parameter {
|
impl Parameter {
|
||||||
fn is_fatal(&self) -> bool {
|
fn is_fatal(&self) -> bool {
|
||||||
match self {
|
matches!(self, Self::Uri(_) | Self::AliasID(_))
|
||||||
Self::URI(_) | Self::AliasID(_) => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,6 +122,11 @@ pub enum ClientError {
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
fn into_string(val: impl ToString) -> String {
|
||||||
|
val.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
impl ClientError {
|
impl ClientError {
|
||||||
pub fn res_status_check<T>(actual: T, expected: T) -> super::Result<()>
|
pub fn res_status_check<T>(actual: T, expected: T) -> super::Result<()>
|
||||||
where
|
where
|
||||||
|
|
@ -141,11 +142,11 @@ impl ClientError {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn response(e: impl ToString) -> Self {
|
pub fn response(e: impl ToString) -> Self {
|
||||||
Self::Response(e.to_string())
|
Self::Response(into_string(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unknown(e: impl ToString) -> Self {
|
pub fn unknown(e: impl ToString) -> Self {
|
||||||
Self::Unknown(e.to_string())
|
Self::Unknown(into_string(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_fatal(&self) -> bool {
|
pub fn is_fatal(&self) -> bool {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue