Compare commits
6 commits
c9c21aa128
...
e0c5b5517f
| Author | SHA1 | Date | |
|---|---|---|---|
| e0c5b5517f | |||
| 087cef5d6f | |||
| b17f239801 | |||
| 9de8f948dc | |||
| 5adbf8cb38 | |||
| 3258b8fb74 |
11 changed files with 121 additions and 69 deletions
4
notes.md
4
notes.md
|
|
@ -37,10 +37,6 @@
|
||||||
|
|
||||||
- yvk repo: https://code.yavook.de/jmm/shrupl
|
- yvk repo: https://code.yavook.de/jmm/shrupl
|
||||||
- sharry issue: https://github.com/eikek/sharry/issues/1659
|
- sharry issue: https://github.com/eikek/sharry/issues/1659
|
||||||
- ureq: https://stackoverflow.com/questions/59586787/rust-how-to-do-http-put-of-large-files
|
|
||||||
- hashing: https://duckduckgo.com/?q=rust+get+file+hash&t=canonical&ia=web
|
|
||||||
- https://stackoverflow.com/q/69787906
|
|
||||||
- https://github.com/RustCrypto/hashes
|
|
||||||
|
|
||||||
|
|
||||||
# Ideas
|
# Ideas
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{fmt, io, time::Duration};
|
use std::{fmt, io, time::Duration};
|
||||||
|
|
||||||
use indicatif::{ProgressBar, ProgressDrawTarget};
|
use indicatif::ProgressBar;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -62,7 +62,6 @@ impl AppState {
|
||||||
|
|
||||||
if let Some(upl) = self.inner.peek_uploading() {
|
if let Some(upl) = self.inner.peek_uploading() {
|
||||||
if bar.length().is_none() {
|
if bar.length().is_none() {
|
||||||
bar.set_draw_target(ProgressDrawTarget::stderr());
|
|
||||||
bar.set_length(upl.get_size());
|
bar.set_length(upl.get_size());
|
||||||
bar.set_message(upl.get_name().to_owned());
|
bar.set_message(upl.get_name().to_owned());
|
||||||
bar.enable_steady_tick(Duration::from_millis(100));
|
bar.enable_steady_tick(Duration::from_millis(100));
|
||||||
|
|
@ -70,8 +69,7 @@ impl AppState {
|
||||||
|
|
||||||
bar.set_position(upl.get_offset());
|
bar.set_position(upl.get_offset());
|
||||||
// BUG in `indicatif` crate?
|
// BUG in `indicatif` crate?
|
||||||
// `set_position` does not force an immediate redraw, so we also call `inc_length` here
|
// `set_position` does not force an immediate redraw like e.g. `inc_length`
|
||||||
bar.inc_length(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f(bar);
|
f(bar);
|
||||||
|
|
|
||||||
|
|
@ -90,13 +90,13 @@ fn main() {
|
||||||
match p {
|
match p {
|
||||||
// Error 404 (File not found)
|
// Error 404 (File not found)
|
||||||
error::Parameter::FileID(fid) => {
|
error::Parameter::FileID(fid) => {
|
||||||
info!("requeueing file {fid:?}");
|
info!("retrying file {fid:?}");
|
||||||
|
|
||||||
state.abort_upload();
|
state.abort_upload();
|
||||||
}
|
}
|
||||||
// Error 404 (Share not found)
|
// Error 404 (Share not found)
|
||||||
error::Parameter::ShareID(sid) => {
|
error::Parameter::ShareID(sid) => {
|
||||||
// TODO ask
|
output::prompt_rebuild_share();
|
||||||
info!("rebuilding share {sid:?}");
|
info!("rebuilding share {sid:?}");
|
||||||
|
|
||||||
// rebuild share
|
// rebuild share
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use indicatif::{ProgressBar, ProgressDrawTarget};
|
use indicatif::ProgressBar;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -63,15 +63,7 @@ impl CacheFile {
|
||||||
bar: &ProgressBar,
|
bar: &ProgressBar,
|
||||||
) -> error::Result<()> {
|
) -> error::Result<()> {
|
||||||
bar.set_message(format!("checking {:?}", file.get_name()));
|
bar.set_message(format!("checking {:?}", file.get_name()));
|
||||||
|
file.check_hash(|bytes| bar.inc(bytes))
|
||||||
match file.check_hash(|bytes| bar.inc(bytes)) {
|
|
||||||
Ok(true) => Ok(()),
|
|
||||||
Ok(false) => Err(error::Error::unknown(format!(
|
|
||||||
"Hash mismatch for file {:?}!",
|
|
||||||
file.get_name()
|
|
||||||
))),
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("checking files in {state:?}");
|
info!("checking files in {state:?}");
|
||||||
|
|
@ -87,7 +79,6 @@ impl CacheFile {
|
||||||
};
|
};
|
||||||
|
|
||||||
let bar = new_progressbar();
|
let bar = new_progressbar();
|
||||||
bar.set_draw_target(ProgressDrawTarget::stderr());
|
|
||||||
bar.set_length(total_size);
|
bar.set_length(total_size);
|
||||||
bar.enable_steady_tick(Duration::from_millis(50));
|
bar.enable_steady_tick(Duration::from_millis(50));
|
||||||
|
|
||||||
|
|
@ -115,7 +106,6 @@ impl CacheFile {
|
||||||
info!("hashing files {files:?}");
|
info!("hashing files {files:?}");
|
||||||
|
|
||||||
let bar = new_progressbar();
|
let bar = new_progressbar();
|
||||||
bar.set_draw_target(ProgressDrawTarget::stderr());
|
|
||||||
// BOOKMARK assumption: total file size < 2 EiB
|
// BOOKMARK assumption: total file size < 2 EiB
|
||||||
bar.set_length(files.iter().map(FileTrait::get_size).sum());
|
bar.set_length(files.iter().map(FileTrait::get_size).sum());
|
||||||
bar.enable_steady_tick(Duration::from_millis(50));
|
bar.enable_steady_tick(Duration::from_millis(50));
|
||||||
|
|
|
||||||
13
src/error.rs
13
src/error.rs
|
|
@ -28,12 +28,15 @@ pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
StdIo(#[from] std::io::Error),
|
StdIo(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("response error: {0}")]
|
#[error("Response error: {0}")]
|
||||||
Response(String),
|
Response(String),
|
||||||
|
|
||||||
#[error("Invalid {0}")]
|
#[error("Invalid {0}")]
|
||||||
InvalidParameter(Parameter),
|
InvalidParameter(Parameter),
|
||||||
|
|
||||||
|
#[error("Mismatch, expected {expected:?} but got {actual:?}")]
|
||||||
|
Mismatch { expected: String, actual: String },
|
||||||
|
|
||||||
#[error("Unknown error: {0}")]
|
#[error("Unknown error: {0}")]
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
}
|
}
|
||||||
|
|
@ -61,14 +64,18 @@ impl Error {
|
||||||
Self::Response(into_string(e))
|
Self::Response(into_string(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unknown(e: impl ToString) -> Self {
|
pub fn mismatch(expected: impl ToString, actual: impl ToString) -> Self {
|
||||||
Self::Unknown(into_string(e))
|
Self::Mismatch {
|
||||||
|
expected: into_string(expected),
|
||||||
|
actual: into_string(actual),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_fatal(&self) -> bool {
|
pub fn is_fatal(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::InvalidParameter(p) => p.is_fatal(),
|
Self::InvalidParameter(p) => p.is_fatal(),
|
||||||
|
Self::Mismatch { .. } => true,
|
||||||
Self::Unknown(_) => true,
|
Self::Unknown(_) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,9 @@ impl Checked {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hash(&mut self, f: impl Fn(u64)) -> io::Result<()> {
|
pub fn hash(&mut self, f: impl Fn(u64)) -> error::Result<()> {
|
||||||
if self.hash.is_some() {
|
if self.hash.is_some() {
|
||||||
return Err(io::Error::other(format!(
|
return Err(error::Error::mismatch("unhashed file", self.path.display()));
|
||||||
"file {:?} is already hashed!",
|
|
||||||
self.path.display()
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hash = Some(super::compute_file_hash(&self.path, self.size, f)?);
|
self.hash = Some(super::compute_file_hash(&self.path, self.size, f)?);
|
||||||
|
|
@ -101,7 +98,7 @@ impl<'t> FileTrait<'t> for Checked {
|
||||||
self.size
|
self.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_hash(&self, on_progress: impl Fn(u64)) -> io::Result<bool> {
|
fn check_hash(&self, on_progress: impl Fn(u64)) -> error::Result<()> {
|
||||||
super::check_file_hash(&self.path, self.size, self.hash.as_ref(), on_progress)
|
super::check_file_hash(&self.path, self.size, self.hash.as_ref(), on_progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,19 @@ mod checked;
|
||||||
mod chunk;
|
mod chunk;
|
||||||
mod uploading;
|
mod uploading;
|
||||||
|
|
||||||
use std::{
|
use std::{ffi::OsStr, fs, io::Read, path::Path};
|
||||||
ffi::OsStr,
|
|
||||||
fs,
|
|
||||||
io::{self, Read},
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
use base64ct::{Base64, Encoding};
|
use base64ct::{Base64, Encoding};
|
||||||
use blake2b_simd::Params as Blake2b;
|
use blake2b_simd::Params as Blake2b;
|
||||||
|
|
||||||
pub use checked::Checked;
|
pub use checked::Checked;
|
||||||
pub use chunk::Chunk;
|
pub use chunk::Chunk;
|
||||||
use log::debug;
|
use log::{debug, warn};
|
||||||
pub use uploading::Uploading;
|
pub use uploading::Uploading;
|
||||||
|
|
||||||
fn compute_file_hash(path: &Path, size: u64, on_progress: impl Fn(u64)) -> io::Result<String> {
|
use crate::error;
|
||||||
|
|
||||||
|
fn compute_file_hash(path: &Path, size: u64, on_progress: impl Fn(u64)) -> error::Result<String> {
|
||||||
let mut file = fs::File::open(path)?;
|
let mut file = fs::File::open(path)?;
|
||||||
let mut hasher = Blake2b::new().hash_length(64).to_state();
|
let mut hasher = Blake2b::new().hash_length(64).to_state();
|
||||||
|
|
||||||
|
|
@ -36,9 +33,7 @@ fn compute_file_hash(path: &Path, size: u64, on_progress: impl Fn(u64)) -> io::R
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes_read != size {
|
if bytes_read != size {
|
||||||
return Err(io::Error::other(format!(
|
return Err(error::Error::mismatch(size, bytes_read));
|
||||||
"Hashed {bytes_read:?} bytes, known file size {size:?}!"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = Base64::encode_string(hasher.finalize().as_bytes());
|
let result = Base64::encode_string(hasher.finalize().as_bytes());
|
||||||
|
|
@ -51,15 +46,20 @@ fn check_file_hash(
|
||||||
size: u64,
|
size: u64,
|
||||||
hash: Option<&String>,
|
hash: Option<&String>,
|
||||||
on_progress: impl Fn(u64),
|
on_progress: impl Fn(u64),
|
||||||
) -> io::Result<bool> {
|
) -> error::Result<()> {
|
||||||
let Some(hash) = hash else {
|
let Some(expected) = hash else {
|
||||||
debug!("no hash to check for {:?}!", path.display());
|
return Err(error::Error::mismatch("hash", path.display()));
|
||||||
return Ok(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = *hash == compute_file_hash(path, size, on_progress)?;
|
let actual = &compute_file_hash(path, size, on_progress)?;
|
||||||
debug!("matches {:?}: {result:?}", *hash);
|
|
||||||
Ok(result)
|
if expected == actual {
|
||||||
|
debug!("hash matches {expected:?}");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
warn!("hash mismatch for file {:?}", path.display());
|
||||||
|
Err(error::Error::mismatch(expected, actual))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileTrait<'t> {
|
pub trait FileTrait<'t> {
|
||||||
|
|
@ -80,5 +80,5 @@ pub trait FileTrait<'t> {
|
||||||
/// get the file's size
|
/// get the file's size
|
||||||
fn get_size(&self) -> u64;
|
fn get_size(&self) -> u64;
|
||||||
|
|
||||||
fn check_hash(&self, on_progress: impl Fn(u64)) -> io::Result<bool>;
|
fn check_hash(&self, on_progress: impl Fn(u64)) -> error::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::sharry;
|
use crate::{error, sharry};
|
||||||
|
|
||||||
use super::{Checked, Chunk, FileTrait};
|
use super::{Checked, Chunk, FileTrait};
|
||||||
|
|
||||||
|
|
@ -108,7 +108,7 @@ impl<'t> FileTrait<'t> for Uploading {
|
||||||
self.size
|
self.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_hash(&self, on_progress: impl Fn(u64)) -> io::Result<bool> {
|
fn check_hash(&self, on_progress: impl Fn(u64)) -> error::Result<()> {
|
||||||
super::check_file_hash(&self.path, self.size, self.hash.as_ref(), on_progress)
|
super::check_file_hash(&self.path, self.size, self.hash.as_ref(), on_progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ fn find_cause(
|
||||||
error.into()
|
error.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error => error::Error::unknown(error),
|
error => error::Error::Unknown(error.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use std::{fmt, process, sync::LazyLock};
|
use std::{fmt, process, sync::LazyLock};
|
||||||
|
|
||||||
use console::{StyledObject, style};
|
use console::{StyledObject, style};
|
||||||
use dialoguer::{Select, theme::ColorfulTheme};
|
use dialoguer::{Confirm, Select, theme::ColorfulTheme};
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use log::{error, info};
|
use log::{info, warn};
|
||||||
|
|
||||||
type StaticStyled<'t> = LazyLock<StyledObject<&'t str>>;
|
type StaticStyled<'t> = LazyLock<StyledObject<&'t str>>;
|
||||||
|
|
||||||
|
|
@ -36,6 +36,23 @@ pub fn prompt_continue() -> bool {
|
||||||
selection == 0
|
selection == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prompt_rebuild_share() {
|
||||||
|
let prompt = format!(
|
||||||
|
"Target Share cannot be accessed. {}",
|
||||||
|
style("Completely restart upload?").cyan()
|
||||||
|
);
|
||||||
|
|
||||||
|
let selection = Confirm::with_theme(&ColorfulTheme::default())
|
||||||
|
.with_prompt(prompt)
|
||||||
|
.default(true)
|
||||||
|
.interact()
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if selection == false {
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn style_all<'t, F>(strs: &[&'t str], f: F) -> Vec<String>
|
pub fn style_all<'t, F>(strs: &[&'t str], f: F) -> Vec<String>
|
||||||
where
|
where
|
||||||
F: Fn(StyledObject<&'t str>) -> StyledObject<&'t str>,
|
F: Fn(StyledObject<&'t str>) -> StyledObject<&'t str>,
|
||||||
|
|
@ -46,7 +63,7 @@ where
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[allow(clippy::missing_panics_doc)]
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn new_progressbar() -> ProgressBar {
|
pub fn new_progressbar() -> ProgressBar {
|
||||||
ProgressBar::hidden().with_style(
|
ProgressBar::no_length().with_style(
|
||||||
ProgressStyle::with_template(&format!(
|
ProgressStyle::with_template(&format!(
|
||||||
concat!(
|
concat!(
|
||||||
"{{bar:50.cyan/blue}} {{msg:.magenta}}: ",
|
"{{bar:50.cyan/blue}} {{msg:.magenta}}: ",
|
||||||
|
|
@ -78,7 +95,7 @@ impl Log {
|
||||||
pub fn handle(e: &crate::error::Error) {
|
pub fn handle(e: &crate::error::Error) {
|
||||||
if e.is_fatal() {
|
if e.is_fatal() {
|
||||||
// react to fatal error
|
// react to fatal error
|
||||||
error!("fatal error: {e:?}");
|
warn!("fatal error: {e:?}");
|
||||||
Self::error(e);
|
Self::error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,10 @@ impl TryFrom<String> for FileID {
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
} else {
|
} else {
|
||||||
Err(error::Error::unknown(format!(
|
Err(error::Error::mismatch(
|
||||||
"Could not extract File ID from {value:?}"
|
"<proto>://<base>/api/v2/alias/upload/<share>/files/tus/<file>",
|
||||||
)))
|
value,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -68,16 +69,62 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fileid_tryfrom_string() {
|
fn valid_urls_produce_expected_file_id() {
|
||||||
let good = "https://example.com/api/v2/alias/upload/SID123/files/tus/FID456".to_owned();
|
// a handful of valid‐looking URLs
|
||||||
let good = FileID::try_from(good);
|
let cases = vec![
|
||||||
assert!(good.is_ok());
|
(
|
||||||
assert_eq!(good.unwrap().as_ref(), "FID456");
|
"http://example.com/api/v2/alias/upload/SID123/files/tus/FID456",
|
||||||
|
"FID456",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://my-host:8080/api/v2/alias/upload/another-SID/files/tus/some-file-id",
|
||||||
|
"some-file-id",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"custom+scheme://host/api/v2/alias/upload/x/files/tus/y",
|
||||||
|
"y",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456".to_owned(); // missing SID
|
for (good, expected_fid) in cases {
|
||||||
assert!(FileID::try_from(bad).is_err());
|
let s = good.to_string();
|
||||||
|
let file_id = FileID::try_from(s.clone()).expect("URL should parse successfully");
|
||||||
|
assert_eq!(
|
||||||
|
file_id.0, expected_fid,
|
||||||
|
"Expected `{}` → FileID({}), got {:?}",
|
||||||
|
good, expected_fid, file_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let bad = "https://example.com/api/v2/alias/upload/SID123/files/tus/".to_owned(); // missing FID
|
#[test]
|
||||||
assert!(FileID::try_from(bad).is_err());
|
fn invalid_urls_return_error() {
|
||||||
|
let bad_inputs = vec![
|
||||||
|
// missing /api/v2/alias/upload
|
||||||
|
"http://example.com/files/tus/FID",
|
||||||
|
// missing /files/tus
|
||||||
|
"http://example.com/api/v2/alias/upload/SID123/FID456",
|
||||||
|
// trailing slash (doesn't match `$`)
|
||||||
|
"http://example.com/api/v2/alias/upload/SID/files/tus/FID/",
|
||||||
|
// empty fid
|
||||||
|
"http://example.com/api/v2/alias/upload/SID/files/tus/",
|
||||||
|
// random string
|
||||||
|
"just-a-random-string",
|
||||||
|
];
|
||||||
|
|
||||||
|
for bad in bad_inputs {
|
||||||
|
let err = FileID::try_from(bad.to_string()).expect_err("URL should not parse");
|
||||||
|
// make sure it's the Mismatch variant, and that it contains the original input
|
||||||
|
match err {
|
||||||
|
error::Error::Mismatch { expected, actual } => {
|
||||||
|
assert_eq!(
|
||||||
|
expected, "<proto>://<base>/api/v2/alias/upload/<share>/files/tus/<file>",
|
||||||
|
"Error should output expected format"
|
||||||
|
);
|
||||||
|
assert_eq!(actual, bad.to_string(), "Error should echo back the input");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Error::Mismatch for input `{bad}` but got {err:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue