Compare commits

..

2 commits

Author SHA1 Message Date
9f1e0cfc6c [wip] impl Client for ureq::Agent
- Client is now owned by `AppState`
- minor cleanups
2025-06-12 23:01:16 +00:00
b9a0e1eeb0 [wip] impl Client for ureq::Agent
- progress bar handling
- share notify timing
2025-06-12 22:29:01 +00:00
6 changed files with 97 additions and 51 deletions

View file

@ -1,4 +1,8 @@
use std::{fmt, io, time::Duration};
use std::{
cell::{Ref, RefCell},
fmt, io,
time::Duration,
};
use console::style;
use indicatif::{ProgressBar, ProgressStyle};
@ -7,14 +11,15 @@ use log::debug;
use super::{
cachefile::CacheFile,
cli::Cli,
file::FileTrait,
file::{self, FileTrait},
sharry::{self, Client, ClientError},
};
pub struct AppState {
progress: Option<ProgressBar>,
current_bar: RefCell<Option<ProgressBar>>,
buffer: Vec<u8>,
http: ureq::Agent,
inner: CacheFile,
}
@ -26,11 +31,19 @@ impl fmt::Debug for AppState {
}
}
fn new_http(timeout: Option<Duration>) -> ureq::Agent {
ureq::Agent::config_builder()
.timeout_global(timeout)
.build()
.into()
}
impl AppState {
fn new(chunk_size: usize, inner: CacheFile) -> Self {
fn new(chunk_size: usize, http: ureq::Agent, inner: CacheFile) -> Self {
Self {
progress: None,
current_bar: None.into(),
buffer: vec![0; chunk_size * 1024 * 1024],
http,
inner,
}
}
@ -40,10 +53,16 @@ impl AppState {
.inspect_err(|e| debug!("could not resume from hash {:?}: {e}", args.get_hash()))
.ok()?;
Some(Self::new(args.chunk_size, inner))
Some(Self::new(
args.chunk_size,
new_http(args.get_timeout()),
inner,
))
}
pub fn from_args(args: &Cli, http: &impl Client) -> sharry::Result<Self> {
pub fn from_args(args: &Cli) -> sharry::Result<Self> {
let http = new_http(args.get_timeout());
let share_id = http.share_create(
&args.get_uri().endpoint("alias/upload/new"),
&args.alias,
@ -52,20 +71,14 @@ impl AppState {
Ok(Self::new(
args.chunk_size,
http,
CacheFile::from_args(args, share_id),
))
}
pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result<Option<()>> {
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
let bar = &*self.progress.get_or_insert_with(|| {
// Create a new bar with style
fn get_or_create_progressbar(&self, uploading: &file::Uploading) -> Ref<'_, ProgressBar> {
let mut slot = self.current_bar.borrow_mut();
if slot.is_none() {
let bar = ProgressBar::new(uploading.get_size())
.with_style(
ProgressStyle::with_template(&format!(
@ -76,23 +89,44 @@ impl AppState {
),
style("/").magenta(),
))
.unwrap(),
.unwrap(), // safe as long as the style template is valid
)
.with_message(uploading.get_name().to_owned())
.with_position(uploading.get_offset());
.with_position(uploading.get_offset())
.with_message(uploading.get_name().to_owned());
bar.enable_steady_tick(Duration::from_millis(100));
bar
});
*slot = Some(bar);
}
drop(slot);
// unwrap is safe: We just made sure it's `Some`.
Ref::map(self.current_bar.borrow(), |opt| opt.as_ref().unwrap())
}
fn finish_bar(&self) {
let mut slot = self.current_bar.borrow_mut();
if let Some(bar) = &*slot {
bar.finish();
*slot = None;
}
}
pub fn upload_chunk(&mut self) -> sharry::Result<Option<()>> {
let Some(mut uploading) = self.inner.pop_file(&self.http) else {
self.inner.share_notify(&self.http).unwrap(); // HACK unwrap
return Ok(None);
};
self.get_or_create_progressbar(&uploading);
debug!("{uploading} chunk {}", self.buffer.len());
let chunk = uploading
.read(&mut self.buffer)
.map_err(ClientError::from)?;
if chunk.get_length() == 0 {
return Err(ClientError::req_err("wtf"));
}
http.file_patch(
self.http.file_patch(
chunk.get_patch_uri(),
self.inner.alias_id(),
chunk.get_offset(),
@ -101,16 +135,19 @@ impl AppState {
match uploading.check_eof() {
Ok(uploading) => {
let bar = self.get_or_create_progressbar(&uploading);
bar.set_position(uploading.get_offset());
// BUG in `indicatif` crate?
// `set_position` does not force immediate redraw, so we also call `inc_length` here
bar.inc_length(0);
drop(bar);
self.inner.push_file(uploading);
Ok(Some(()))
}
Err(path) => {
debug!("Finished {:?}!", path.display());
bar.finish();
self.progress = None;
self.inner.share_notify(http).unwrap(); // HACK unwrap
self.finish_bar();
Ok(self.inner.has_file().then_some(()))
}

View file

@ -1,4 +1,5 @@
use std::{
fmt,
hash::{DefaultHasher, Hash, Hasher},
time::Duration,
};
@ -10,7 +11,7 @@ use super::{
sharry::{NewShareRequest, Uri},
};
#[derive(Parser, Debug, Hash)]
#[derive(Parser, Hash)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// Timeout in seconds for HTTP actions (set 0 or invalid to disable)
@ -56,6 +57,20 @@ pub struct Cli {
pub files: Vec<Checked>,
}
impl fmt::Debug for Cli {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Cli")
.field("uri", &self.get_uri())
.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("hash", &self.get_hash())
.finish_non_exhaustive()
}
}
fn parse_seconds(data: &str) -> Result<Duration, String> {
data.parse().or(Ok(0)).map(Duration::from_secs)
}

View file

@ -48,6 +48,13 @@ impl Uploading {
f.seek(SeekFrom::Start(self.offset))?;
let read_len = f.read(buf)?;
if read_len == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("could not read from file {:?}", self.path.display()),
));
}
let chunk = Chunk::new(&buf[..read_len], &self.patch_uri, self.offset);
self.offset += chunk.get_length();

View file

@ -16,7 +16,6 @@ use clap::Parser;
use console::style;
use dialoguer::{Confirm, theme::ColorfulTheme};
use log::{error, info};
use ureq::Agent;
use appstate::AppState;
use cli::Cli;
@ -76,12 +75,6 @@ fn main() {
let args = Cli::parse();
info!("args: {args:?}");
info!("timeout: {:?}", args.get_timeout());
let agent: Agent = Agent::config_builder()
.timeout_global(args.get_timeout())
.build()
.into();
let mut state = AppState::try_resume(&args)
.and_then(|state| {
@ -94,7 +87,7 @@ fn main() {
.unwrap_or_else(|| {
check_ctrlc();
match AppState::from_args(&args, &agent) {
match AppState::from_args(&args) {
Ok(state) => {
state.save().unwrap(); // HACK unwrap
state
@ -115,7 +108,7 @@ fn main() {
);
loop {
match state.upload_chunk(&agent) {
match state.upload_chunk() {
Err(e) => error!("error: {e:?}"),
Ok(None) => {
info!("all uploads done");

View file

@ -31,7 +31,7 @@ impl fmt::Display for Uri {
}
}
#[derive(Serialize)]
#[derive(Serialize, Debug)]
#[allow(non_snake_case)]
pub struct NewShareRequest {
name: String,

View file

@ -21,7 +21,7 @@ pub trait Client {
file_size: u64,
) -> Result<String>;
fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>;
fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()>;
}
#[derive(Debug, Error)]
@ -86,8 +86,6 @@ impl Client for ureq::Agent {
alias_id: &str,
data: NewShareRequest,
) -> Result<String> {
// let endpoint = uri.get_endpoint("alias/upload/new");
let mut res = self
.post(endpoint)
.header("Sharry-Alias", alias_id)
@ -112,8 +110,6 @@ impl Client for ureq::Agent {
}
fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> {
// let endpoint = uri.get_endpoint(format!("alias/mail/notify/{}", share_id));
let mut res = self
.post(endpoint)
.header("Sharry-Alias", alias_id)
@ -140,8 +136,6 @@ impl Client for ureq::Agent {
file_name: &str,
file_size: u64,
) -> Result<String> {
// let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id));
let res = self
.post(endpoint)
.header("Sharry-Alias", alias_id)
@ -164,15 +158,15 @@ impl Client for ureq::Agent {
Ok(location)
}
fn file_patch(&self, patch_uri: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()> {
fn file_patch(&self, endpoint: &str, alias_id: &str, offset: u64, chunk: &[u8]) -> Result<()> {
let res = self
.patch(patch_uri)
.patch(endpoint)
.header("Sharry-Alias", alias_id)
.header("Upload-Offset", offset)
.send(chunk)
.map_err(ClientError::from)?;
trace!("{patch_uri:?} response: {res:?}");
trace!("{endpoint:?} response: {res:?}");
ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?;
let res_offset = (res.headers().get("Upload-Offset"))