170 lines
4.0 KiB
Rust
170 lines
4.0 KiB
Rust
use std::{
|
|
hash::Hasher,
|
|
path::PathBuf,
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
use std::os::unix::ffi::OsStrExt;
|
|
|
|
use fuser::INodeNo;
|
|
use twox_hash::XxHash64;
|
|
|
|
use crate::music::metadata::MusicMetadata;
|
|
use crate::origins::attrs::FileAttrs;
|
|
|
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
|
pub enum FileType {
|
|
Directory,
|
|
File,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Item {
|
|
pub inode: INodeNo,
|
|
pub parent_inode: INodeNo,
|
|
pub name: String,
|
|
pub original_path: PathBuf,
|
|
pub local_path: PathBuf,
|
|
pub file_type: FileType,
|
|
pub attrs: FileAttrs,
|
|
pub music_metadata: Option<MusicMetadata>,
|
|
pub hash: u64,
|
|
}
|
|
|
|
fn secs_since_epoch(time: SystemTime) -> u64 {
|
|
return time
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
}
|
|
|
|
/// Hash inputs shared by every Item regardless of origin. Server manifests
|
|
/// and client Items both feed these same five values through xxhash, so a
|
|
/// file's hash is identical on both sides — that's what makes `Reconcile`'s
|
|
/// diff correct.
|
|
pub fn compute_item_hash(
|
|
inode: u64,
|
|
original_path: &[u8],
|
|
ctime_secs: u64,
|
|
mtime_secs: u64,
|
|
crtime_secs: u64,
|
|
) -> u64 {
|
|
let seed = 1234;
|
|
let mut hasher = XxHash64::with_seed(seed);
|
|
hasher.write_u64(inode);
|
|
hasher.write(original_path);
|
|
hasher.write_u64(ctime_secs);
|
|
hasher.write_u64(mtime_secs);
|
|
hasher.write_u64(crtime_secs);
|
|
return hasher.finish();
|
|
}
|
|
|
|
impl Item {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn new(
|
|
inode: INodeNo,
|
|
parent_inode: INodeNo,
|
|
name: String,
|
|
original_path: PathBuf,
|
|
local_path: PathBuf,
|
|
file_type: FileType,
|
|
attrs: FileAttrs,
|
|
music_metadata: Option<MusicMetadata>,
|
|
) -> Item {
|
|
let mut item = Item {
|
|
inode,
|
|
parent_inode,
|
|
name,
|
|
original_path,
|
|
local_path,
|
|
file_type,
|
|
attrs,
|
|
music_metadata,
|
|
hash: 0,
|
|
};
|
|
item.hash = item.compute_hash();
|
|
|
|
return item;
|
|
}
|
|
|
|
pub fn compute_hash(&self) -> u64 {
|
|
return compute_item_hash(
|
|
self.inode.0,
|
|
self.original_path.as_os_str().as_bytes(),
|
|
secs_since_epoch(self.attrs.ctime),
|
|
secs_since_epoch(self.attrs.mtime),
|
|
secs_since_epoch(self.attrs.crtime),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::origins::attrs::FileAttrs;
|
|
|
|
#[test]
|
|
fn compute_hash_deterministic() {
|
|
let attrs = file_attrs_for_tempdir();
|
|
|
|
let item1 = Item::new(
|
|
INodeNo(42),
|
|
INodeNo::ROOT,
|
|
"test".to_string(),
|
|
PathBuf::from("/some/path"),
|
|
PathBuf::from("test"),
|
|
FileType::File,
|
|
attrs.clone(),
|
|
None,
|
|
);
|
|
|
|
let item2 = Item::new(
|
|
INodeNo(42),
|
|
INodeNo::ROOT,
|
|
"test".to_string(),
|
|
PathBuf::from("/some/path"),
|
|
PathBuf::from("test"),
|
|
FileType::File,
|
|
attrs,
|
|
None,
|
|
);
|
|
|
|
assert_eq!(item1.hash, item2.hash);
|
|
}
|
|
|
|
#[test]
|
|
fn compute_hash_changes_on_path_change() {
|
|
let attrs = file_attrs_for_tempdir();
|
|
|
|
let item1 = Item::new(
|
|
INodeNo(42),
|
|
INodeNo::ROOT,
|
|
"test".to_string(),
|
|
PathBuf::from("/some/path"),
|
|
PathBuf::from("test"),
|
|
FileType::File,
|
|
attrs.clone(),
|
|
None,
|
|
);
|
|
|
|
let item2 = Item::new(
|
|
INodeNo(42),
|
|
INodeNo::ROOT,
|
|
"test".to_string(),
|
|
PathBuf::from("/different/path"),
|
|
PathBuf::from("test"),
|
|
FileType::File,
|
|
attrs,
|
|
None,
|
|
);
|
|
|
|
assert_ne!(item1.hash, item2.hash);
|
|
}
|
|
|
|
fn file_attrs_for_tempdir() -> FileAttrs {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let metadata = std::fs::metadata(tmp.path()).unwrap();
|
|
return FileAttrs::from(&metadata);
|
|
}
|
|
}
|