Compare commits

..

No commits in common. "fb06725f0575e3a97809ea1116555819ef78895a" and "9b1f7f872cbfe1db799e84cdb2d2dc05fd9acd10" have entirely different histories.

4 changed files with 151 additions and 206 deletions

View file

@ -1,4 +1,10 @@
use std::{fmt, io, path::PathBuf, time::Duration};
use std::{
collections::VecDeque,
fmt, fs,
io::{self, Write},
path::{Path, PathBuf},
time::Duration,
};
use console::style;
use indicatif::{ProgressBar, ProgressStyle};
@ -7,29 +13,68 @@ use serde::{Deserialize, Serialize};
use super::{
cli::Cli,
file::FileTrait,
savedstate::SavedState,
sharry::{self, Client, ClientError},
file::{self, FileTrait},
sharry::{self, Client, ClientError, Uri},
};
#[derive(Serialize, Deserialize)]
pub struct AppState {
#[serde(skip)]
file_name: PathBuf,
#[serde(skip)]
progress: Option<ProgressBar>,
#[serde(skip)]
buffer: Vec<u8>,
inner: SavedState,
uri: Uri,
alias_id: String,
share_id: String,
files: VecDeque<FileState>,
}
impl fmt::Debug for AppState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AppState")
.field("inner", &self.inner)
.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)
.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<file::Uploading> {
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()
@ -47,55 +92,90 @@ impl AppState {
file_name
}
fn new(chunk_size: usize, inner: SavedState) -> Self {
fn new(
file_name: PathBuf,
chunk_size: usize,
uri: Uri,
alias_id: String,
share_id: String,
files: VecDeque<FileState>,
) -> Self {
Self {
file_name,
progress: None,
buffer: vec![0; chunk_size * 1024 * 1024],
inner,
uri,
alias_id,
share_id,
files,
}
}
fn load(file_name: &Path, chunk_size: usize) -> io::Result<Self> {
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<Self> {
let file_name = Self::cache_file(args);
let inner = SavedState::load(&file_name)
.inspect_err(|e| debug!("could not resume from {:?}: {e}", file_name.display()))
.ok()?;
Some(Self::new(args.chunk_size, inner))
Self::load(&file_name, args.chunk_size)
.inspect_err(|e| debug!("could not resume from {:?}: {e}", file_name.display()))
.ok()
}
pub fn from_args(args: &Cli, http: &impl Client) -> sharry::Result<Self> {
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"),
&args.alias,
&alias_id,
args.get_share_request(),
)?;
let files: VecDeque<_> = args.files.clone().into_iter().map(FileState::C).collect();
Ok(Self::new(
file_name,
args.chunk_size,
SavedState::new(
Self::cache_file(&args),
uri,
args.alias.clone(),
alias_id,
share_id,
&args.files,
),
files,
))
}
pub fn file_names(&self) -> Vec<&str> {
self.inner.file_names()
self.files.iter().map(FileState::file_name).collect()
}
pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result<Option<()>> {
let Some(mut uploading) = self.inner.pop_file(http) else {
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 {
return Ok(None);
};
debug!("{uploading} chunk {}", self.buffer.len());
// Initialize or fetch the existing ProgressBar
// 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())
@ -119,7 +199,7 @@ impl AppState {
let chunk = uploading
.read(&mut self.buffer)
.map_err(ClientError::from)?;
.map_err(ClientError::req_err)?;
if chunk.get_length() == 0 {
return Err(ClientError::req_err("wtf"));
}
@ -134,7 +214,7 @@ impl AppState {
match uploading.check_eof() {
Ok(uploading) => {
bar.set_position(uploading.get_offset());
self.inner.push_file(uploading);
self.files.push_front(FileState::U(uploading));
Ok(Some(()))
}
Err(path) => {
@ -147,16 +227,26 @@ impl AppState {
.endpoint(format!("alias/mail/notify/{}", self.share_id));
http.share_notify(&endpoint, &self.alias_id).unwrap(); // HACK unwrap
Ok(self.inner.has_file().then_some(()))
Ok(self.files.front().map(drop))
}
}
}
pub fn save(&self) -> io::Result<()> {
self.inner.save()
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<()> {
self.inner.clear()
fs::remove_file(&self.file_name)?;
trace!("removed {:?}", self.file_name.display());
Ok(())
}
}

View file

@ -1,11 +1,10 @@
mod appstate;
mod cli;
mod file;
mod savedstate;
mod sharry;
use std::{
process,
process::{self, exit},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
@ -22,32 +21,6 @@ use appstate::AppState;
use cli::Cli;
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: {}",
style("Error!").red().bold(),
style(cause).cyan(),
);
println!("{}", style(e.to_string()).yellow().italic());
} else {
// handle unknown error
error!("unknown error: {e} ({e:?})");
println!("{}", style("Unknown Error!").red().bold());
}
}
fn main() {
env_logger::init();
@ -59,8 +32,8 @@ fn main() {
let check_ctrlc = {
let stop = Arc::new(AtomicBool::new(false));
let stop_ctrlc = stop.clone();
let stop_ctrlc = stop.clone();
ctrlc::set_handler(move || {
stop_ctrlc.store(true, Ordering::SeqCst);
info!("stopping as soon as possible ...");
@ -69,7 +42,7 @@ fn main() {
move || {
if stop.load(Ordering::SeqCst) {
process::exit(255);
process::exit(1);
}
}
};
@ -100,20 +73,39 @@ fn main() {
state
}
Err(e) => {
print_error(&e);
process::exit(1);
if let Some(cause) = match e {
ClientError::ResponseStatus {
actual: 403,
expected: _,
} => Some("Alias ID"),
// ClientError::FileIO(_) => Some("URL"),
_ => None,
} {
info!("handling error: {e:?}");
println!(
"{} probably wrong: {} {:?}",
style("Error!").red().bold(),
style(cause).cyan(),
style(e.to_string()).yellow().italic()
);
} else {
error!("unknown error: {e} {e:?}");
println!("{}", style("Unknown Error!").red().bold());
}
exit(1);
}
}
});
info!("continuing with state: {state:?}");
println!(
"{} uploading: {}",
style("ShrUpl").yellow().bold(),
style(state.file_names().join(", ")).magenta(),
);
info!("continuing with state: {state:?}");
loop {
match state.upload_chunk(&agent) {
Err(e) => error!("error: {e:?}"),

View file

@ -1,133 +0,0 @@
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<file::Uploading> {
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<FileState>,
}
impl SavedState {
pub fn new(
file_name: PathBuf,
uri: Uri,
alias_id: String,
share_id: String,
files: &Vec<file::Checked>,
) -> 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<Self> {
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<file::Uploading> {
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(())
}
}

View file

@ -26,18 +26,15 @@ pub trait Client {
#[derive(Debug, Error)]
pub enum ClientError {
#[error(transparent)]
StdIo(#[from] std::io::Error),
#[error("network request failed: {0}")]
Request(String),
#[error("unexpected response status: {actual} (expected {expected})")]
ResponseStatus { actual: u16, expected: u16 },
#[error("response parsing failed: {0}")]
ResponseParsing(String),
#[error("unexpected response status: {actual} (expected {expected:?})")]
ResponseStatus { actual: u16, expected: Option<u16> },
#[error("unexpected response content: {0}")]
ResponseContent(String),
}
@ -60,7 +57,7 @@ impl ClientError {
} else {
Err(Self::ResponseStatus {
actual: actual.into(),
expected: expected.into(),
expected: Some(expected.into()),
})
}
}
@ -69,12 +66,11 @@ impl ClientError {
impl From<ureq::Error> for ClientError {
fn from(value: ureq::Error) -> Self {
match value {
ureq::Error::StatusCode(status) => Self::ResponseStatus {
ureq::Error::StatusCode(status) => ClientError::ResponseStatus {
actual: status,
expected: 200,
expected: None,
},
ureq::Error::Io(e) => e.into(),
error => Self::req_err(error),
error => Self::Request(error.to_string()),
}
}
}