use std::{ collections::VecDeque, fs, io::{self, Write}, path::{Path, PathBuf}, time::Duration, }; use console::style; use indicatif::{ProgressBar, ProgressStyle}; use log::{debug, trace}; use serde::{Deserialize, Serialize}; use super::{ cli::Cli, sharry::{Alias, ChunkState, FileChecked, FileUploading, Share, SharryFile, UploadError}, }; #[derive(Serialize, Deserialize, Debug)] pub struct AppState { #[serde(skip)] file_name: PathBuf, #[serde(skip)] progress: Option, alias: Alias, share: Share, files: VecDeque, } #[derive(Serialize, Deserialize, Debug)] enum FileState { C(FileChecked), U(FileUploading), } 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: &ureq::Agent, alias: &Alias, share: &Share, ) -> io::Result { match self { FileState::C(checked) => checked.start_upload(http, alias, share), FileState::U(uploading) => Ok(uploading), } } } impl AppState { fn cache_dir() -> PathBuf { let dir_name = dirs_next::cache_dir() .expect("could not determine cache directory") .join("shrupl"); trace!("cachedir: {:?}", dir_name.display()); dir_name } fn cache_file(args: &Cli) -> PathBuf { let file_name = Self::cache_dir().join(format!("{}.json", args.get_hash())); trace!("cachefile: {:?}", file_name.display()); file_name } fn load(file_name: impl AsRef) -> io::Result { let content = fs::read_to_string(file_name)?; serde_json::from_str(&content).map_err(io::Error::other) } pub fn try_resume(args: &Cli) -> Option { let file_name = Self::cache_file(args); Self::load(&file_name) .inspect_err(|e| debug!("could not resume from {:?}: {e}", file_name.display())) .map(|state| { debug!("successfully loaded AppState"); Self { file_name, progress: None, alias: state.alias, share: state.share, files: state.files, } }) .ok() } pub fn from_args(args: &Cli, http: &ureq::Agent) -> Result { let file_name = Self::cache_file(args); let alias = args.get_alias(); let share = Share::create(http, &alias, args.get_share_request())?; let files: VecDeque<_> = args.files.clone().into_iter().map(FileState::C).collect(); Ok(Self { file_name, progress: None, alias, share, files, }) } pub fn file_names(&self) -> Vec<&str> { self.files.iter().map(FileState::file_name).collect() } pub fn upload_chunk( &mut self, http: &ureq::Agent, chunk_size: usize, ) -> Result, UploadError> { let uploading = if let Some(state) = self.files.pop_front() { state.start_upload(http, &self.alias, &self.share).unwrap() // HACK unwrap } else { return Ok(None); }; debug!("{uploading} chunk {chunk_size}"); // Initialize or fetch the existing ProgressBar in one call: let bar = &*self.progress.get_or_insert_with(|| { // Create a new bar with style let bar = ProgressBar::new(uploading.get_size()) .with_style( ProgressStyle::with_template(&format!( concat!( "{{msg:.yellow}}: {{bar:50.cyan/blue}} ", "{{binary_bytes:.magenta}}{}{{binary_total_bytes:.magenta}} ", "({{eta}})", ), style("/").magenta(), )) .unwrap(), ) .with_message(uploading.get_name().to_owned()) .with_position(uploading.get_offset()); bar.enable_steady_tick(Duration::from_millis(100)); bar }); match uploading.upload_chunk(http, &self.alias, chunk_size) { ChunkState::Ok(upl) => { bar.set_position(upl.get_offset()); self.files.push_front(FileState::U(upl)); Ok(Some(())) } ChunkState::Err(upl, e) => { self.files.push_front(FileState::U(upl)); Err(e) } ChunkState::Finished(path) => { debug!("Finished {:?}!", path.display()); bar.finish(); self.progress = None; self.share.notify(http, &self.alias).unwrap(); // HACK unwrap Ok(self.files.front().map(drop)) } } } 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(()) } pub fn clear(self) -> io::Result<()> { fs::remove_file(&self.file_name)?; trace!("removed {:?}", self.file_name.display()); Ok(()) } }