267 lines
7.3 KiB
Rust
267 lines
7.3 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
fs,
|
|
hash::Hasher,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use fuser::INodeNo;
|
|
use twox_hash::XxHash64;
|
|
|
|
use crate::db::entities::Model;
|
|
use crate::item::{FileType, Item};
|
|
|
|
pub fn virtual_inode(path: &str) -> INodeNo {
|
|
let mut hasher = XxHash64::with_seed(5678);
|
|
hasher.write(path.as_bytes());
|
|
|
|
return INodeNo(hasher.finish() | (1u64 << 63));
|
|
}
|
|
|
|
pub fn parent_inode_from_path(local_path: &Path) -> INodeNo {
|
|
let components: Vec<_> = local_path
|
|
.components()
|
|
.filter(|c| matches!(c, std::path::Component::Normal(_)))
|
|
.collect();
|
|
if components.len() <= 1 {
|
|
return INodeNo::ROOT;
|
|
}
|
|
let mut parent_path = String::new();
|
|
for (i, comp) in components[..components.len() - 1].iter().enumerate() {
|
|
if i > 0 {
|
|
parent_path.push('/');
|
|
}
|
|
parent_path.push_str(comp.as_os_str().to_str().unwrap_or_default());
|
|
}
|
|
|
|
return virtual_inode(&parent_path);
|
|
}
|
|
|
|
pub fn ensure_virtual_dirs(
|
|
local_path: &Path,
|
|
source: &Path,
|
|
map: &mut BTreeMap<INodeNo, Item>,
|
|
) -> INodeNo {
|
|
let components: Vec<_> = local_path
|
|
.components()
|
|
.filter(|c| matches!(c, std::path::Component::Normal(_)))
|
|
.collect();
|
|
|
|
if components.len() <= 1 {
|
|
return INodeNo::ROOT;
|
|
}
|
|
|
|
let mut current_parent = INodeNo::ROOT;
|
|
let mut current_path = String::new();
|
|
|
|
for component in &components[..components.len() - 1] {
|
|
let comp_str = component.as_os_str().to_str().unwrap_or_default();
|
|
if !current_path.is_empty() {
|
|
current_path.push('/');
|
|
}
|
|
current_path.push_str(comp_str);
|
|
|
|
let virt_ino = virtual_inode(¤t_path);
|
|
|
|
if !map.contains_key(&virt_ino) {
|
|
let virt_item = Item::new(
|
|
virt_ino,
|
|
current_parent,
|
|
comp_str.to_string(),
|
|
source.to_path_buf(),
|
|
PathBuf::from(¤t_path),
|
|
FileType::Directory,
|
|
fs::metadata(source).unwrap(),
|
|
None,
|
|
);
|
|
map.insert(virt_ino, virt_item);
|
|
}
|
|
|
|
current_parent = virt_ino;
|
|
}
|
|
|
|
return current_parent;
|
|
}
|
|
|
|
pub fn restore_virtual_paths(
|
|
snapshot: &mut BTreeMap<INodeNo, Item>,
|
|
db_items: &std::collections::HashMap<i64, Model>,
|
|
source: &Path,
|
|
) {
|
|
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
|
|
.values()
|
|
.filter_map(|db_item| {
|
|
let ino = INodeNo(db_item.inode as u64);
|
|
if ino == INodeNo::ROOT {
|
|
return None;
|
|
}
|
|
snapshot
|
|
.get(&ino)
|
|
.filter(|item| item.hash as i64 == db_item.hash)
|
|
.map(|_| {
|
|
(
|
|
ino,
|
|
db_item.name.clone(),
|
|
PathBuf::from(&db_item.local_path),
|
|
)
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
for (_, _, local_path) in &restorations {
|
|
ensure_virtual_dirs(local_path, source, snapshot);
|
|
}
|
|
for (ino, name, local_path) in restorations {
|
|
if let Some(item) = snapshot.get_mut(&ino) {
|
|
item.name = name;
|
|
item.parent_inode = parent_inode_from_path(&local_path);
|
|
item.local_path = local_path;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::HashMap;
|
|
|
|
#[test]
|
|
fn virtual_inode_deterministic() {
|
|
let ino1 = virtual_inode("foo");
|
|
let ino2 = virtual_inode("foo");
|
|
assert_eq!(ino1, ino2);
|
|
}
|
|
|
|
#[test]
|
|
fn virtual_inode_high_bit_set() {
|
|
let ino = virtual_inode("test");
|
|
assert_eq!(ino.0 & (1u64 << 63), 1u64 << 63);
|
|
}
|
|
|
|
#[test]
|
|
fn virtual_inode_different_inputs() {
|
|
let ino1 = virtual_inode("foo");
|
|
let ino2 = virtual_inode("bar");
|
|
assert_ne!(ino1, ino2);
|
|
}
|
|
|
|
#[test]
|
|
fn parent_inode_single_component() {
|
|
let ino = parent_inode_from_path(Path::new("file.txt"));
|
|
assert_eq!(ino, INodeNo::ROOT);
|
|
}
|
|
|
|
#[test]
|
|
fn parent_inode_multi_component() {
|
|
let ino = parent_inode_from_path(Path::new("a/b/c"));
|
|
let expected = virtual_inode("a/b");
|
|
assert_eq!(ino, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn parent_inode_absolute_path_filtered() {
|
|
let ino = parent_inode_from_path(Path::new("/file"));
|
|
assert_eq!(ino, INodeNo::ROOT);
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_virtual_dirs_single_component() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let source = tmp.path();
|
|
let mut map = BTreeMap::new();
|
|
|
|
let ino = ensure_virtual_dirs(Path::new("file.txt"), source, &mut map);
|
|
assert_eq!(ino, INodeNo::ROOT);
|
|
assert!(map.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_virtual_dirs_creates_hierarchy() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let source = tmp.path();
|
|
let mut map = BTreeMap::new();
|
|
|
|
let ino = ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
|
|
|
|
let a_ino = virtual_inode("a");
|
|
let ab_ino = virtual_inode("a/b");
|
|
|
|
assert_eq!(ino, ab_ino);
|
|
assert_eq!(map.len(), 2);
|
|
assert!(map.contains_key(&a_ino));
|
|
assert!(map.contains_key(&ab_ino));
|
|
|
|
let a_item = &map[&a_ino];
|
|
assert_eq!(a_item.parent_inode, INodeNo::ROOT);
|
|
|
|
let ab_item = &map[&ab_ino];
|
|
assert_eq!(ab_item.parent_inode, a_ino);
|
|
}
|
|
|
|
#[test]
|
|
fn ensure_virtual_dirs_idempotent() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let source = tmp.path();
|
|
let mut map = BTreeMap::new();
|
|
|
|
ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
|
|
let size_after_first = map.len();
|
|
|
|
ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
|
|
let size_after_second = map.len();
|
|
|
|
assert_eq!(size_after_first, size_after_second);
|
|
}
|
|
|
|
#[test]
|
|
fn restore_virtual_paths_skips_root() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let source = tmp.path();
|
|
let mut snapshot = BTreeMap::new();
|
|
|
|
let real_ino = INodeNo(42);
|
|
let real_item = Item::new(
|
|
real_ino,
|
|
INodeNo::ROOT,
|
|
"real_file".to_string(),
|
|
source.to_path_buf(),
|
|
PathBuf::from("real_file"),
|
|
FileType::File,
|
|
fs::metadata(source).unwrap(),
|
|
None,
|
|
);
|
|
snapshot.insert(real_ino, real_item);
|
|
|
|
let mut db_items = HashMap::new();
|
|
db_items.insert(
|
|
1i64,
|
|
Model {
|
|
inode: 1,
|
|
hash: 0,
|
|
name: "root".to_string(),
|
|
original_path: "/tmp/test".to_string(),
|
|
local_path: "/tmp/test".to_string(),
|
|
file_type: "directory".to_string(),
|
|
},
|
|
);
|
|
db_items.insert(
|
|
42i64,
|
|
Model {
|
|
inode: 42,
|
|
hash: 0,
|
|
name: "real_file".to_string(),
|
|
original_path: "real_file".to_string(),
|
|
local_path: "real_file".to_string(),
|
|
file_type: "file".to_string(),
|
|
},
|
|
);
|
|
|
|
restore_virtual_paths(&mut snapshot, &db_items, source);
|
|
|
|
for item in snapshot.values() {
|
|
assert_ne!(item.name, "/");
|
|
assert_ne!(item.name, "tmp");
|
|
}
|
|
}
|
|
}
|