Files
MusicFS/src/item.rs
T
2026-06-30 12:17:48 +02:00

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);
}
}