207 lines
5.9 KiB
Rust
207 lines
5.9 KiB
Rust
use std::{
|
|
fs, io,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::sharry;
|
|
|
|
use super::{FileTrait, Uploading};
|
|
|
|
/// Description of an existing, regular file
|
|
///
|
|
/// - impl `Clone` for `clap` compatibility
|
|
/// - impl `serde` for cachefile handling
|
|
/// - impl `PartialEq..Ord` to handle multiple files given
|
|
/// - impl `AsRef<[u8]>` for hashing with `blake2b_simd`
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct Checked {
|
|
/// canonical path to a regular file
|
|
path: PathBuf,
|
|
/// size of that file
|
|
size: u64,
|
|
/// hash of that file
|
|
hash: Option<String>,
|
|
}
|
|
|
|
impl AsRef<[u8]> for Checked {
|
|
fn as_ref(&self) -> &[u8] {
|
|
self.path.as_os_str().as_encoded_bytes()
|
|
}
|
|
}
|
|
|
|
impl Checked {
|
|
pub(super) fn new_direct(path: PathBuf, size: u64, hash: Option<String>) -> Self {
|
|
Self { path, size, hash }
|
|
}
|
|
|
|
/// create a new checked file from some path reference
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// - from `fs::metadata(path)` or `fs::canonicalize`
|
|
/// - given path does not correspond to a regular file
|
|
pub fn new(value: impl AsRef<Path>) -> io::Result<Self> {
|
|
let meta = fs::metadata(&value)?;
|
|
if meta.is_file() {
|
|
Ok(Self {
|
|
path: fs::canonicalize(&value)?,
|
|
size: meta.len(),
|
|
hash: None,
|
|
})
|
|
} else {
|
|
Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"Not a regular file",
|
|
))
|
|
}
|
|
}
|
|
|
|
/// calculate and store hash for this file
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// - from `file::compute_hash`
|
|
/// - Mismatch if file already hashed
|
|
///
|
|
/// TODO this could use an error variant like `IllegalInvocation`
|
|
pub fn hash(&mut self, on_progress: impl FnMut(u64)) -> crate::Result<()> {
|
|
if self.hash.is_some() {
|
|
return crate::Error::mismatch("unhashed file", self.path.display());
|
|
}
|
|
|
|
self.hash = Some(super::compute_hash(&self.path, self.size, on_progress)?);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// start uploading this file
|
|
///
|
|
/// - try to create a new file using the client
|
|
/// - consume `self` into a `file::Uploading` struct
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// - from `sharry::Client::file_create`
|
|
pub fn start_upload(
|
|
self,
|
|
client: &impl sharry::Client,
|
|
uri: &sharry::Uri,
|
|
alias_id: &sharry::AliasID,
|
|
share_id: &sharry::ShareID,
|
|
) -> crate::Result<Uploading> {
|
|
let file_id = client.file_create(uri, alias_id, share_id, &self)?;
|
|
|
|
Ok(Uploading::new_direct(
|
|
self.path, self.size, self.hash, file_id,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl FileTrait for Checked {
|
|
fn get_name(&self) -> &str {
|
|
<Self as FileTrait>::extract_file_name(&self.path)
|
|
}
|
|
|
|
fn get_size(&self) -> u64 {
|
|
self.size
|
|
}
|
|
|
|
fn check_hash(&self, on_progress: impl FnMut(u64)) -> crate::Result<()> {
|
|
super::check_hash(&self.path, self.size, self.hash.as_deref(), on_progress)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
// tests for `Checked::start_upload` omitted, as they require a `sharry::Client`
|
|
|
|
use tempfile::TempDir;
|
|
|
|
use crate::test_util::{
|
|
create_file,
|
|
data::{HASHES_STD_GOOD, cases, data},
|
|
};
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn new_on_existing_file_works() {
|
|
for (content, size) in cases() {
|
|
let file = create_file(content);
|
|
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
|
|
|
|
let path = file
|
|
.path()
|
|
.canonicalize()
|
|
.expect("the file should have a canonical path");
|
|
|
|
assert_eq!(chk.path, path);
|
|
assert_eq!(chk.size, size);
|
|
assert!(chk.hash.is_none());
|
|
|
|
// `FileTrait`
|
|
assert_eq!(
|
|
chk.get_name(),
|
|
file.path().file_name().expect("`file_name` should succeed")
|
|
);
|
|
assert_eq!(chk.get_size(), size);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn new_on_dir_errors() {
|
|
let tempdir = TempDir::new().expect("creating temp dir");
|
|
let fs_root = PathBuf::from("/");
|
|
|
|
let dirs = [tempdir.path(), fs_root.as_path()];
|
|
|
|
for p in dirs {
|
|
let err = Checked::new(p).expect_err("creating `Checked` should fail");
|
|
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
|
#[cfg(target_os = "linux")]
|
|
assert_eq!(err.to_string(), "Not a regular file");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn new_on_nex_errors() {
|
|
let tempdir = TempDir::new().expect("creating temp dir");
|
|
let nex_paths = [0, 1, 2, 3, 4].map(|i| tempdir.path().join(format!("nex_{i}.ext")));
|
|
|
|
for p in nex_paths {
|
|
let err = Checked::new(p).expect_err("creating `Checked` should fail");
|
|
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
|
#[cfg(target_os = "linux")]
|
|
assert_eq!(err.to_string(), "No such file or directory (os error 2)");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hashing_works() {
|
|
for (content, hash) in data().zip(HASHES_STD_GOOD) {
|
|
let file = create_file(content);
|
|
let mut chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
|
|
|
|
chk.hash(drop).expect("`hash` should succeed");
|
|
// `FileTrait`
|
|
chk.check_hash(drop).expect("`check_hash` should succeed");
|
|
|
|
assert_eq!(chk.hash, Some(hash.to_string()));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hashing_again_errors() {
|
|
for content in data() {
|
|
let file = create_file(content);
|
|
let mut chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
|
|
|
|
chk.hash(drop).expect("`hash` should succeed");
|
|
let err = chk.hash(drop).expect_err("`hash` twice should fail");
|
|
|
|
assert!(err.is_mismatch("unhashed file", chk.path.display().to_string()));
|
|
}
|
|
}
|
|
}
|