diff --git a/src/appstate.rs b/src/appstate.rs index db8c64b..1a585ab 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -1,10 +1,4 @@ -use std::{ - collections::VecDeque, - fmt, fs, - io::{self, Write}, - path::{Path, PathBuf}, - time::Duration, -}; +use std::{fmt, io, path::PathBuf, time::Duration}; use console::style; use indicatif::{ProgressBar, ProgressStyle}; @@ -13,68 +7,29 @@ use serde::{Deserialize, Serialize}; use super::{ cli::Cli, - file::{self, FileTrait}, - sharry::{self, Client, ClientError, Uri}, + file::FileTrait, + savedstate::SavedState, + sharry::{self, Client, ClientError}, }; #[derive(Serialize, Deserialize)] pub struct AppState { - #[serde(skip)] - file_name: PathBuf, #[serde(skip)] progress: Option, #[serde(skip)] buffer: Vec, - uri: Uri, - alias_id: String, - share_id: String, - files: VecDeque, + inner: SavedState, } impl fmt::Debug for AppState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AppState") - .field("file_name", &self.file_name) - .field("uri", &self.uri) - .field("alias_id", &self.alias_id) - .field("share_id", &self.share_id) - .field("files", &self.files) + .field("inner", &self.inner) .finish() } } -#[derive(Serialize, Deserialize, Debug)] -enum FileState { - C(file::Checked), - U(file::Uploading), -} - -impl FileState { - fn file_name(&self) -> &str { - match self { - FileState::C(checked) => checked.get_name(), - FileState::U(uploading) => uploading.get_name(), - } - } - - fn start_upload( - self, - http: &impl Client, - uri: &Uri, - alias_id: &str, - share_id: &str, - ) -> sharry::Result { - match self { - FileState::C(checked) => { - let endpoint = &uri.endpoint(format!("alias/upload/{share_id}/files/tus")); - checked.start_upload(http, endpoint, alias_id) - } - FileState::U(uploading) => Ok(uploading), - } - } -} - impl AppState { fn cache_dir() -> PathBuf { let dir_name = dirs_next::cache_dir() @@ -92,90 +47,55 @@ impl AppState { file_name } - fn new( - file_name: PathBuf, - chunk_size: usize, - uri: Uri, - alias_id: String, - share_id: String, - files: VecDeque, - ) -> Self { + fn new(chunk_size: usize, inner: SavedState) -> Self { Self { - file_name, progress: None, buffer: vec![0; chunk_size * 1024 * 1024], - uri, - alias_id, - share_id, - files, + inner, } } - fn load(file_name: &Path, chunk_size: usize) -> io::Result { - let file = fs::File::open(file_name)?; - serde_json::from_reader(io::BufReader::new(file)) - .map_err(io::Error::other) - .map(|state: Self| { - debug!("successfully loaded AppState"); - - Self::new( - file_name.to_owned(), - chunk_size, - state.uri, - state.alias_id, - state.share_id, - state.files, - ) - }) - } - pub fn try_resume(args: &Cli) -> Option { let file_name = Self::cache_file(args); - - Self::load(&file_name, args.chunk_size) + let inner = SavedState::load(&file_name) .inspect_err(|e| debug!("could not resume from {:?}: {e}", file_name.display())) - .ok() + .ok()?; + + Some(Self::new(args.chunk_size, inner)) } pub fn from_args(args: &Cli, http: &impl Client) -> sharry::Result { - let file_name = Self::cache_file(args); - let uri = args.get_uri(); - let alias_id = args.alias.clone(); let share_id = http.share_create( &uri.endpoint("alias/upload/new"), - &alias_id, + &args.alias, args.get_share_request(), )?; - let files: VecDeque<_> = args.files.clone().into_iter().map(FileState::C).collect(); - Ok(Self::new( - file_name, args.chunk_size, - uri, - alias_id, - share_id, - files, + SavedState::new( + Self::cache_file(&args), + uri, + args.alias.clone(), + share_id, + &args.files, + ), )) } pub fn file_names(&self) -> Vec<&str> { - self.files.iter().map(FileState::file_name).collect() + self.inner.file_names() } pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result> { - let mut uploading = if let Some(state) = self.files.pop_front() { - state - .start_upload(http, &self.uri, &self.alias_id, &self.share_id) - .unwrap() // HACK unwrap - } else { + let Some(mut uploading) = self.inner.pop_file(http) else { return Ok(None); }; debug!("{uploading} chunk {}", self.buffer.len()); - // Initialize or fetch the existing ProgressBar in one call: + // Initialize or fetch the existing ProgressBar let bar = &*self.progress.get_or_insert_with(|| { // Create a new bar with style let bar = ProgressBar::new(uploading.get_size()) @@ -199,7 +119,7 @@ impl AppState { let chunk = uploading .read(&mut self.buffer) - .map_err(ClientError::req_err)?; + .map_err(ClientError::from)?; if chunk.get_length() == 0 { return Err(ClientError::req_err("wtf")); } @@ -214,7 +134,7 @@ impl AppState { match uploading.check_eof() { Ok(uploading) => { bar.set_position(uploading.get_offset()); - self.files.push_front(FileState::U(uploading)); + self.inner.push_file(uploading); Ok(Some(())) } Err(path) => { @@ -227,26 +147,16 @@ impl AppState { .endpoint(format!("alias/mail/notify/{}", self.share_id)); http.share_notify(&endpoint, &self.alias_id).unwrap(); // HACK unwrap - Ok(self.files.front().map(drop)) + Ok(self.inner.has_file().then_some(())) } } } pub fn save(&self) -> io::Result<()> { - fs::create_dir_all(Self::cache_dir())?; - - let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?; - let mut file = fs::File::create(&self.file_name)?; - file.write_all(json.as_bytes())?; - - trace!("updated {:?}", self.file_name.display()); - Ok(()) + self.inner.save() } pub fn clear(self) -> io::Result<()> { - fs::remove_file(&self.file_name)?; - - trace!("removed {:?}", self.file_name.display()); - Ok(()) + self.inner.clear() } } diff --git a/src/main.rs b/src/main.rs index e9925b1..7b5e8a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ mod appstate; mod cli; mod file; +mod savedstate; mod sharry; use std::{ - process::{self, exit}, + process, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -23,13 +24,16 @@ use sharry::ClientError; fn print_error(e: &ClientError) { if let Some(cause) = match e { + // known errors ClientError::ResponseStatus { actual: 403, expected: _, } => Some("Alias ID"), ClientError::StdIo(_) => Some("URL"), + // unknown error _ => None, } { + // handle known error info!("known error: {e:?}"); println!( "{} probably wrong: {}", @@ -38,6 +42,7 @@ fn print_error(e: &ClientError) { ); println!("{}", style(e.to_string()).yellow().italic()); } else { + // handle unknown error error!("unknown error: {e} ({e:?})"); println!("{}", style("Unknown Error!").red().bold()); } @@ -64,7 +69,7 @@ fn main() { move || { if stop.load(Ordering::SeqCst) { - process::exit(1); + process::exit(255); } } }; @@ -96,7 +101,7 @@ fn main() { } Err(e) => { print_error(&e); - exit(1); + process::exit(1); } } }); diff --git a/src/savedstate.rs b/src/savedstate.rs new file mode 100644 index 0000000..cb392c1 --- /dev/null +++ b/src/savedstate.rs @@ -0,0 +1,133 @@ +use std::{ + collections::VecDeque, + fs, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use log::trace; +use serde::{Deserialize, Serialize}; + +use super::{ + file::{self, FileTrait}, + sharry::{self, Client, Uri}, +}; + +#[derive(Serialize, Deserialize, Debug)] +enum FileState { + C(file::Checked), + U(file::Uploading), +} + +impl FileState { + fn file_name(&self) -> &str { + match self { + FileState::C(c) => c.get_name(), + FileState::U(u) => u.get_name(), + } + } + + fn start_upload( + self, + http: &impl Client, + uri: &Uri, + alias_id: &str, + share_id: &str, + ) -> sharry::Result { + match self { + FileState::C(checked) => { + let endpoint = &uri.endpoint(format!("alias/upload/{share_id}/files/tus")); + checked.start_upload(http, endpoint, alias_id) + } + FileState::U(uploading) => Ok(uploading), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SavedState { + #[serde(skip)] + file_name: PathBuf, + + uri: Uri, + alias_id: String, + share_id: String, + files: VecDeque, +} + +impl SavedState { + pub fn new( + file_name: PathBuf, + uri: Uri, + alias_id: String, + share_id: String, + files: &Vec, + ) -> Self { + Self { + file_name, + uri, + alias_id, + share_id, + files: files.clone().into_iter().map(FileState::C).collect(), + } + } + + pub fn load(file_name: &Path) -> io::Result { + let file = fs::File::open(file_name)?; + let state: Self = + serde_json::from_reader(io::BufReader::new(file)).map_err(io::Error::other)?; + + Ok(Self { + file_name: file_name.to_owned(), + uri: state.uri, + alias_id: state.alias_id, + share_id: state.share_id, + files: state.files, + }) + } + + pub fn file_names(&self) -> Vec<&str> { + self.files.iter().map(FileState::file_name).collect() + } + + pub fn has_file(&self) -> bool { + !self.files.is_empty() + } + + pub fn pop_file(&mut self, http: &impl Client) -> Option { + if let Some(state) = self.files.pop_front() { + Some( + state + .start_upload(http, &self.uri, &self.alias_id, &self.share_id) + .unwrap(), + ) // HACK unwrap + } else { + None + } + } + + pub fn push_file(&mut self, file: file::Uploading) { + self.files.push_front(FileState::U(file)); + } + + pub fn save(&self) -> io::Result<()> { + let cache_dir = self.file_name.parent().ok_or_else(|| { + io::Error::other(format!("orphan file {:?}", self.file_name.display())) + })?; + fs::create_dir_all(cache_dir)?; + + let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?; + let mut file = fs::File::create(&self.file_name)?; + file.write_all(json.as_bytes())?; + + trace!("updated {:?}", self.file_name.display()); + Ok(()) + } + + pub fn clear(self) -> io::Result<()> { + fs::remove_file(&self.file_name)?; + + trace!("removed {:?}", self.file_name.display()); + Ok(()) + } +}