Compare commits
2 commits
2edc690331
...
9f1e0cfc6c
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f1e0cfc6c | |||
| b9a0e1eeb0 |
6 changed files with 97 additions and 51 deletions
|
|
@ -1,4 +1,8 @@
|
||||||
use std::{fmt, io, time::Duration};
|
use std::{
|
||||||
|
cell::{Ref, RefCell},
|
||||||
|
fmt, io,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use console::style;
|
use console::style;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
|
@ -7,14 +11,15 @@ use log::debug;
|
||||||
use super::{
|
use super::{
|
||||||
cachefile::CacheFile,
|
cachefile::CacheFile,
|
||||||
cli::Cli,
|
cli::Cli,
|
||||||
file::FileTrait,
|
file::{self, FileTrait},
|
||||||
sharry::{self, Client, ClientError},
|
sharry::{self, Client, ClientError},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
progress: Option<ProgressBar>,
|
current_bar: RefCell<Option<ProgressBar>>,
|
||||||
buffer: Vec<u8>,
|
buffer: Vec<u8>,
|
||||||
|
|
||||||
|
http: ureq::Agent,
|
||||||
inner: CacheFile,
|
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 {
|
impl AppState {
|
||||||
fn new(chunk_size: usize, inner: CacheFile) -> Self {
|
fn new(chunk_size: usize, http: ureq::Agent, inner: CacheFile) -> Self {
|
||||||
Self {
|
Self {
|
||||||
progress: None,
|
current_bar: None.into(),
|
||||||
buffer: vec![0; chunk_size * 1024 * 1024],
|
buffer: vec![0; chunk_size * 1024 * 1024],
|
||||||
|
http,
|
||||||
inner,
|
inner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,10 +53,16 @@ impl AppState {
|
||||||
.inspect_err(|e| debug!("could not resume from hash {:?}: {e}", args.get_hash()))
|
.inspect_err(|e| debug!("could not resume from hash {:?}: {e}", args.get_hash()))
|
||||||
.ok()?;
|
.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(
|
let share_id = http.share_create(
|
||||||
&args.get_uri().endpoint("alias/upload/new"),
|
&args.get_uri().endpoint("alias/upload/new"),
|
||||||
&args.alias,
|
&args.alias,
|
||||||
|
|
@ -52,20 +71,14 @@ impl AppState {
|
||||||
|
|
||||||
Ok(Self::new(
|
Ok(Self::new(
|
||||||
args.chunk_size,
|
args.chunk_size,
|
||||||
|
http,
|
||||||
CacheFile::from_args(args, share_id),
|
CacheFile::from_args(args, share_id),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_chunk(&mut self, http: &impl Client) -> sharry::Result<Option<()>> {
|
fn get_or_create_progressbar(&self, uploading: &file::Uploading) -> Ref<'_, ProgressBar> {
|
||||||
let Some(mut uploading) = self.inner.pop_file(http) else {
|
let mut slot = self.current_bar.borrow_mut();
|
||||||
return Ok(None);
|
if slot.is_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
|
|
||||||
let bar = ProgressBar::new(uploading.get_size())
|
let bar = ProgressBar::new(uploading.get_size())
|
||||||
.with_style(
|
.with_style(
|
||||||
ProgressStyle::with_template(&format!(
|
ProgressStyle::with_template(&format!(
|
||||||
|
|
@ -76,23 +89,44 @@ impl AppState {
|
||||||
),
|
),
|
||||||
style("/").magenta(),
|
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.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
|
let chunk = uploading
|
||||||
.read(&mut self.buffer)
|
.read(&mut self.buffer)
|
||||||
.map_err(ClientError::from)?;
|
.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(),
|
chunk.get_patch_uri(),
|
||||||
self.inner.alias_id(),
|
self.inner.alias_id(),
|
||||||
chunk.get_offset(),
|
chunk.get_offset(),
|
||||||
|
|
@ -101,16 +135,19 @@ impl AppState {
|
||||||
|
|
||||||
match uploading.check_eof() {
|
match uploading.check_eof() {
|
||||||
Ok(uploading) => {
|
Ok(uploading) => {
|
||||||
|
let bar = self.get_or_create_progressbar(&uploading);
|
||||||
bar.set_position(uploading.get_offset());
|
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);
|
self.inner.push_file(uploading);
|
||||||
Ok(Some(()))
|
Ok(Some(()))
|
||||||
}
|
}
|
||||||
Err(path) => {
|
Err(path) => {
|
||||||
debug!("Finished {:?}!", path.display());
|
debug!("Finished {:?}!", path.display());
|
||||||
bar.finish();
|
self.finish_bar();
|
||||||
self.progress = None;
|
|
||||||
|
|
||||||
self.inner.share_notify(http).unwrap(); // HACK unwrap
|
|
||||||
|
|
||||||
Ok(self.inner.has_file().then_some(()))
|
Ok(self.inner.has_file().then_some(()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
src/cli.rs
17
src/cli.rs
|
|
@ -1,4 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
fmt,
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
@ -10,7 +11,7 @@ use super::{
|
||||||
sharry::{NewShareRequest, Uri},
|
sharry::{NewShareRequest, Uri},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Parser, Debug, Hash)]
|
#[derive(Parser, Hash)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Timeout in seconds for HTTP actions (set 0 or invalid to disable)
|
/// Timeout in seconds for HTTP actions (set 0 or invalid to disable)
|
||||||
|
|
@ -56,6 +57,20 @@ pub struct Cli {
|
||||||
pub files: Vec<Checked>,
|
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> {
|
fn parse_seconds(data: &str) -> Result<Duration, String> {
|
||||||
data.parse().or(Ok(0)).map(Duration::from_secs)
|
data.parse().or(Ok(0)).map(Duration::from_secs)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,13 @@ impl Uploading {
|
||||||
f.seek(SeekFrom::Start(self.offset))?;
|
f.seek(SeekFrom::Start(self.offset))?;
|
||||||
let read_len = f.read(buf)?;
|
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);
|
let chunk = Chunk::new(&buf[..read_len], &self.patch_uri, self.offset);
|
||||||
self.offset += chunk.get_length();
|
self.offset += chunk.get_length();
|
||||||
|
|
||||||
|
|
|
||||||
11
src/main.rs
11
src/main.rs
|
|
@ -16,7 +16,6 @@ use clap::Parser;
|
||||||
use console::style;
|
use console::style;
|
||||||
use dialoguer::{Confirm, theme::ColorfulTheme};
|
use dialoguer::{Confirm, theme::ColorfulTheme};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use ureq::Agent;
|
|
||||||
|
|
||||||
use appstate::AppState;
|
use appstate::AppState;
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
|
|
@ -76,12 +75,6 @@ fn main() {
|
||||||
|
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
info!("args: {args:?}");
|
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)
|
let mut state = AppState::try_resume(&args)
|
||||||
.and_then(|state| {
|
.and_then(|state| {
|
||||||
|
|
@ -94,7 +87,7 @@ fn main() {
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
check_ctrlc();
|
check_ctrlc();
|
||||||
|
|
||||||
match AppState::from_args(&args, &agent) {
|
match AppState::from_args(&args) {
|
||||||
Ok(state) => {
|
Ok(state) => {
|
||||||
state.save().unwrap(); // HACK unwrap
|
state.save().unwrap(); // HACK unwrap
|
||||||
state
|
state
|
||||||
|
|
@ -115,7 +108,7 @@ fn main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match state.upload_chunk(&agent) {
|
match state.upload_chunk() {
|
||||||
Err(e) => error!("error: {e:?}"),
|
Err(e) => error!("error: {e:?}"),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
info!("all uploads done");
|
info!("all uploads done");
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ impl fmt::Display for Uri {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub struct NewShareRequest {
|
pub struct NewShareRequest {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub trait Client {
|
||||||
file_size: u64,
|
file_size: u64,
|
||||||
) -> Result<String>;
|
) -> 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)]
|
#[derive(Debug, Error)]
|
||||||
|
|
@ -86,8 +86,6 @@ impl Client for ureq::Agent {
|
||||||
alias_id: &str,
|
alias_id: &str,
|
||||||
data: NewShareRequest,
|
data: NewShareRequest,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// let endpoint = uri.get_endpoint("alias/upload/new");
|
|
||||||
|
|
||||||
let mut res = self
|
let mut res = self
|
||||||
.post(endpoint)
|
.post(endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id)
|
||||||
|
|
@ -112,8 +110,6 @@ impl Client for ureq::Agent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn share_notify(&self, endpoint: &str, alias_id: &str) -> Result<()> {
|
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
|
let mut res = self
|
||||||
.post(endpoint)
|
.post(endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id)
|
||||||
|
|
@ -140,8 +136,6 @@ impl Client for ureq::Agent {
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
file_size: u64,
|
file_size: u64,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
// let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id));
|
|
||||||
|
|
||||||
let res = self
|
let res = self
|
||||||
.post(endpoint)
|
.post(endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id)
|
||||||
|
|
@ -164,15 +158,15 @@ impl Client for ureq::Agent {
|
||||||
Ok(location)
|
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
|
let res = self
|
||||||
.patch(patch_uri)
|
.patch(endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id)
|
||||||
.header("Upload-Offset", offset)
|
.header("Upload-Offset", offset)
|
||||||
.send(chunk)
|
.send(chunk)
|
||||||
.map_err(ClientError::from)?;
|
.map_err(ClientError::from)?;
|
||||||
|
|
||||||
trace!("{patch_uri:?} response: {res:?}");
|
trace!("{endpoint:?} response: {res:?}");
|
||||||
ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?;
|
ClientError::res_status_check(res.status(), ureq::http::StatusCode::NO_CONTENT)?;
|
||||||
|
|
||||||
let res_offset = (res.headers().get("Upload-Offset"))
|
let res_offset = (res.headers().get("Upload-Offset"))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue