1use std::collections::{HashMap, HashSet};
2use std::hash::{Hash as StdHash, Hasher};
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::{Arc, Mutex, RwLock};
5
6use futures::executor::block_on;
7use hashtree_core::{
8 sha256, to_hex, Cid, HashTree, HashTreeConfig, HashTreeError, LinkType, Store,
9};
10use thiserror::Error;
11
12pub const ROOT_INODE: u64 = 1;
13pub const DIRECTORY_REFRESH_SENTINEL_NAME: &str = "iris-drive-refresh";
15const WHOLE_FILE_HASH_META_KEY: &str = "whole_file_hash";
16
17#[derive(Debug, Error)]
18pub enum FsError {
19 #[error("root hash is not a directory")]
20 InvalidRoot,
21 #[error("entry not found")]
22 NotFound,
23 #[error("not a directory")]
24 NotDir,
25 #[error("is a directory")]
26 IsDir,
27 #[error("entry already exists")]
28 AlreadyExists,
29 #[error("directory not empty")]
30 NotEmpty,
31 #[error("invalid entry name")]
32 InvalidName,
33 #[error("tree error: {0}")]
34 Tree(String),
35 #[error("publish error: {0}")]
36 Publish(String),
37}
38
39impl From<HashTreeError> for FsError {
40 fn from(err: HashTreeError) -> Self {
41 FsError::Tree(err.to_string())
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum EntryKind {
47 File,
48 Directory,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct EntryAttr {
53 pub inode: u64,
54 pub size: u64,
55 pub kind: EntryKind,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct DirEntry {
60 pub inode: u64,
61 pub name: String,
62 pub kind: EntryKind,
63}
64
65pub trait RootPublisher: Send + Sync {
66 fn publish(&self, cid: &Cid) -> Result<(), FsError>;
67}
68
69#[derive(Debug, Clone, Eq)]
70struct ChildKey {
71 parent: u64,
72 name: String,
73}
74
75impl PartialEq for ChildKey {
76 fn eq(&self, other: &Self) -> bool {
77 self.parent == other.parent && self.name == other.name
78 }
79}
80
81impl StdHash for ChildKey {
82 fn hash<H: Hasher>(&self, state: &mut H) {
83 self.parent.hash(state);
84 self.name.hash(state);
85 }
86}
87
88#[derive(Debug, Clone, PartialEq)]
89struct ResolvedEntry {
90 cid: Cid,
91 link_type: LinkType,
92 size: u64,
93 meta: Option<HashMap<String, serde_json::Value>>,
94}
95
96#[cfg(feature = "fuse")]
97#[derive(Debug, Clone, PartialEq, Eq)]
98enum FuseInvalidation {
99 Entry {
100 parent: u64,
101 name: String,
102 },
103 Delete {
104 parent: u64,
105 child: u64,
106 name: String,
107 },
108}
109
110pub struct HashtreeFuseInner<S: Store> {
111 tree: HashTree<S>,
112 root: RwLock<Cid>,
113 paths: RwLock<HashMap<u64, Vec<String>>>,
114 children: RwLock<HashMap<ChildKey, u64>>,
115 parents: RwLock<HashMap<u64, u64>>,
116 next_inode: AtomicU64,
117 publisher: Option<Arc<dyn RootPublisher>>,
118 refresh_unlinks: Mutex<HashSet<Vec<String>>>,
119 modify_lock: Mutex<()>,
120 #[cfg(feature = "fuse")]
121 notifier: Mutex<Option<fuser::Notifier>>,
122}
123
124pub struct HashtreeFuse<S: Store> {
125 inner: Arc<HashtreeFuseInner<S>>,
126}
127
128impl<S: Store> Clone for HashtreeFuse<S> {
129 fn clone(&self) -> Self {
130 Self {
131 inner: self.inner.clone(),
132 }
133 }
134}
135
136impl<S: Store> std::ops::Deref for HashtreeFuse<S> {
137 type Target = HashtreeFuseInner<S>;
138
139 fn deref(&self) -> &Self::Target {
140 &self.inner
141 }
142}
143
144impl<S: Store> HashtreeFuse<S> {
145 pub fn new(store: Arc<S>, root: Cid) -> Result<Self, FsError> {
146 Self::new_with_publisher(store, root, None)
147 }
148
149 pub fn new_with_publisher(
150 store: Arc<S>,
151 root: Cid,
152 publisher: Option<Arc<dyn RootPublisher>>,
153 ) -> Result<Self, FsError> {
154 let mut config = HashTreeConfig::new(store);
155 if root.key.is_none() {
156 config = config.public();
157 }
158 let tree = HashTree::new(config);
159
160 let is_dir = block_on(tree.get_directory_node(&root))?.is_some();
161 if !is_dir {
162 return Err(FsError::InvalidRoot);
163 }
164
165 let mut paths = HashMap::new();
166 paths.insert(ROOT_INODE, Vec::new());
167 let mut parents = HashMap::new();
168 parents.insert(ROOT_INODE, ROOT_INODE);
169
170 Ok(Self {
171 inner: Arc::new(HashtreeFuseInner {
172 tree,
173 root: RwLock::new(root),
174 paths: RwLock::new(paths),
175 children: RwLock::new(HashMap::new()),
176 parents: RwLock::new(parents),
177 next_inode: AtomicU64::new(ROOT_INODE + 1),
178 publisher,
179 refresh_unlinks: Mutex::new(HashSet::new()),
180 modify_lock: Mutex::new(()),
181 #[cfg(feature = "fuse")]
182 notifier: Mutex::new(None),
183 }),
184 })
185 }
186
187 pub fn current_root(&self) -> Cid {
188 self.root.read().unwrap().clone()
189 }
190
191 pub fn replace_root(&self, root: Cid) -> Result<(), FsError> {
192 let _guard = self.modify_lock.lock().unwrap();
193 self.replace_root_locked(root)
194 }
195
196 pub fn replace_root_if_current(
197 &self,
198 expected_current: &Cid,
199 root: Cid,
200 ) -> Result<bool, FsError> {
201 let _guard = self.modify_lock.lock().unwrap();
202 if self.current_root() != *expected_current {
203 return Ok(false);
204 }
205 self.replace_root_locked(root)?;
206 Ok(true)
207 }
208
209 fn replace_root_locked(&self, root: Cid) -> Result<(), FsError> {
210 let is_dir = block_on(self.tree.get_directory_node(&root))?.is_some();
211 if !is_dir {
212 return Err(FsError::InvalidRoot);
213 }
214
215 #[cfg(feature = "fuse")]
216 let invalidations = self.changed_known_entries_for_root(&root);
217 #[cfg(feature = "fuse")]
218 let paths_needing_new_inode = self.changed_known_paths_requiring_new_inode_for_root(&root);
219
220 *self.root.write().unwrap() = root;
221
222 self.retain_existing_paths_after_root_update()?;
223 #[cfg(feature = "fuse")]
224 self.drop_paths(paths_needing_new_inode);
225
226 #[cfg(feature = "fuse")]
227 self.notify_entries_invalidated(&invalidations);
228
229 Ok(())
230 }
231
232 #[cfg(feature = "fuse")]
233 pub fn removed_known_entry_paths_for_root(&self, root: &Cid) -> Vec<Vec<String>> {
234 self.changed_known_entries_for_root(root)
235 .into_iter()
236 .filter_map(|invalidation| match invalidation {
237 FuseInvalidation::Delete { parent, name, .. } => {
238 let mut path = self.path_for_inode(parent).ok()?;
239 path.push(name);
240 Some(path)
241 }
242 _ => None,
243 })
244 .collect()
245 }
246
247 #[cfg(not(feature = "fuse"))]
248 pub fn removed_known_entry_paths_for_root(&self, _root: &Cid) -> Vec<Vec<String>> {
249 Vec::new()
250 }
251
252 pub fn begin_refresh_unlinks(&self, paths: impl IntoIterator<Item = Vec<String>>) {
253 self.refresh_unlinks.lock().unwrap().extend(paths);
254 }
255
256 pub fn clear_refresh_unlinks(&self) {
257 self.refresh_unlinks.lock().unwrap().clear();
258 }
259
260 #[cfg(feature = "fuse")]
261 fn set_notifier(&self, notifier: fuser::Notifier) {
262 *self.notifier.lock().unwrap() = Some(notifier);
263 }
264
265 #[cfg(feature = "fuse")]
266 fn clear_notifier(&self) {
267 *self.notifier.lock().unwrap() = None;
268 }
269
270 #[cfg(feature = "fuse")]
271 fn notify_entries_invalidated(&self, invalidations: &[FuseInvalidation]) {
272 let guard = self.notifier.lock().unwrap();
273 let Some(notifier) = guard.as_ref() else {
274 return;
275 };
276 for invalidation in invalidations {
277 match invalidation {
278 FuseInvalidation::Entry { parent, name } => {
279 let _ = notifier.inval_entry(*parent, std::ffi::OsStr::new(name));
280 }
281 FuseInvalidation::Delete {
282 parent,
283 child,
284 name,
285 } => {
286 let _ = notifier.delete(*parent, *child, std::ffi::OsStr::new(name));
287 }
288 }
289 }
290 }
291
292 #[cfg(feature = "fuse")]
293 fn changed_known_entries_for_root(&self, new_root: &Cid) -> Vec<FuseInvalidation> {
294 let old_root = self.current_root();
295 let paths = self.paths.read().unwrap().clone();
296 let children = self.children.read().unwrap().clone();
297 let mut invalidations = Vec::new();
298
299 for (inode, path) in paths {
300 let old_entries = self.directory_entry_names_at_root(&old_root, &path);
301 let new_entries = self.directory_entry_names_at_root(new_root, &path);
302 if old_entries.is_none() && new_entries.is_none() {
303 continue;
304 }
305
306 let old_entries = old_entries.unwrap_or_default();
307 let new_entries = new_entries.unwrap_or_default();
308 for removed in old_entries.difference(&new_entries) {
309 invalidations.push(FuseInvalidation::Entry {
310 parent: inode,
311 name: removed.clone(),
312 });
313 if let Some(child) = children
314 .get(&ChildKey {
315 parent: inode,
316 name: removed.clone(),
317 })
318 .copied()
319 {
320 invalidations.push(FuseInvalidation::Delete {
321 parent: inode,
322 child,
323 name: removed.clone(),
324 });
325 }
326 }
327
328 for retained in old_entries.intersection(&new_entries) {
329 if self.entry_changed_between_roots(&old_root, new_root, &path, retained) {
330 invalidations.push(FuseInvalidation::Entry {
331 parent: inode,
332 name: retained.clone(),
333 });
334 }
335 }
336
337 for added in new_entries.difference(&old_entries) {
338 invalidations.push(FuseInvalidation::Entry {
339 parent: inode,
340 name: added.clone(),
341 });
342 }
343 }
344
345 invalidations
346 }
347
348 #[cfg(feature = "fuse")]
349 fn changed_known_paths_requiring_new_inode_for_root(&self, new_root: &Cid) -> Vec<Vec<String>> {
350 let old_root = self.current_root();
351 let paths: Vec<Vec<String>> = self.paths.read().unwrap().values().cloned().collect();
352
353 paths
354 .into_iter()
355 .filter(|path| {
356 !path.is_empty()
357 && !Self::is_directory_refresh_sentinel_path(path)
358 && self.entry_path_changed_between_roots(&old_root, new_root, path)
359 && self.changed_entry_should_get_new_inode(&old_root, new_root, path)
360 })
361 .collect()
362 }
363
364 #[cfg(feature = "fuse")]
365 fn changed_entry_should_get_new_inode(
366 &self,
367 old_root: &Cid,
368 new_root: &Cid,
369 path: &[String],
370 ) -> bool {
371 match (
372 self.resolve_entry_at_root(old_root, path),
373 self.resolve_entry_at_root(new_root, path),
374 ) {
375 (Ok(old), Ok(new)) => {
376 !old.link_type.is_directory_like() || !new.link_type.is_directory_like()
377 }
378 _ => true,
379 }
380 }
381
382 #[cfg(feature = "fuse")]
383 fn entry_path_changed_between_roots(
384 &self,
385 old_root: &Cid,
386 new_root: &Cid,
387 path: &[String],
388 ) -> bool {
389 match (
390 self.resolve_entry_at_root(old_root, path),
391 self.resolve_entry_at_root(new_root, path),
392 ) {
393 (Ok(old), Ok(new)) => old != new,
394 (Err(_), Err(_)) => false,
395 _ => true,
396 }
397 }
398
399 #[cfg(feature = "fuse")]
400 fn entry_changed_between_roots(
401 &self,
402 old_root: &Cid,
403 new_root: &Cid,
404 parent_path: &[String],
405 name: &str,
406 ) -> bool {
407 let mut child_path = parent_path.to_vec();
408 child_path.push(name.to_string());
409 match (
410 self.resolve_entry_at_root(old_root, &child_path),
411 self.resolve_entry_at_root(new_root, &child_path),
412 ) {
413 (Ok(old), Ok(new)) => old != new,
414 (Err(_), Err(_)) => false,
415 _ => true,
416 }
417 }
418
419 pub fn lookup_child(&self, parent: u64, name: &str) -> Result<EntryAttr, FsError> {
420 if name.is_empty() {
421 return Err(FsError::NotFound);
422 }
423 if name == "." {
424 return self.get_attr(parent);
425 }
426 if name == ".." {
427 let parent_inode = self.parent_inode(parent);
428 return self.get_attr(parent_inode);
429 }
430 if Self::is_directory_refresh_sentinel(name) {
431 if self.get_attr(parent)?.kind != EntryKind::Directory {
432 return Err(FsError::NotDir);
433 }
434 let inode = self.get_or_create_child_inode(parent, name)?;
435 return Ok(Self::directory_refresh_sentinel_attr(inode));
436 }
437
438 let child_inode = self.get_or_create_child_inode(parent, name)?;
439 let path = self.path_for_inode(child_inode)?;
440
441 match self.resolve_entry(&path) {
442 Ok(entry) => self.entry_attr_from_resolved(child_inode, entry),
443 Err(FsError::NotFound) => {
444 self.drop_inode(child_inode);
445 Err(FsError::NotFound)
446 }
447 Err(err) => Err(err),
448 }
449 }
450
451 pub fn get_attr(&self, inode: u64) -> Result<EntryAttr, FsError> {
452 if inode == ROOT_INODE {
453 return Ok(EntryAttr {
454 inode,
455 size: 0,
456 kind: EntryKind::Directory,
457 });
458 }
459
460 let path = self.path_for_inode(inode)?;
461 if Self::is_directory_refresh_sentinel_path(&path) {
462 let parent_path = &path[..path.len() - 1];
463 self.resolve_dir_cid(parent_path)?;
464 return Ok(Self::directory_refresh_sentinel_attr(inode));
465 }
466 let entry = self.resolve_entry(&path)?;
467 self.entry_attr_from_resolved(inode, entry)
468 }
469
470 pub fn read_file(&self, inode: u64, offset: u64, size: u32) -> Result<Vec<u8>, FsError> {
471 let path = self.path_for_inode(inode)?;
472 if Self::is_directory_refresh_sentinel_path(&path) {
473 return Ok(Vec::new());
474 }
475 let entry = self.resolve_entry(&path)?;
476 if entry.link_type.is_directory_like() {
477 return Err(FsError::IsDir);
478 }
479
480 let file_size = self.entry_size(&entry)?;
481 if offset >= file_size {
482 return Ok(vec![]);
483 }
484 let read_len = (size as u64).min(file_size - offset);
485 if read_len == 0 {
486 return Ok(vec![]);
487 }
488
489 if entry.cid.key.is_some() {
490 let data = block_on(self.tree.get(&entry.cid, None))?.ok_or(FsError::NotFound)?;
491 let start = usize::try_from(offset).unwrap_or(usize::MAX);
492 if start >= data.len() {
493 return Ok(vec![]);
494 }
495 let end_u64 = offset.saturating_add(read_len);
496 let mut end = usize::try_from(end_u64).unwrap_or(data.len());
497 if end > data.len() {
498 end = data.len();
499 }
500 return Ok(data[start..end].to_vec());
501 }
502
503 let end = offset.saturating_add(read_len);
504 let data = block_on(
505 self.tree
506 .read_file_range(&entry.cid.hash, offset, Some(end)),
507 )?
508 .ok_or(FsError::NotFound)?;
509 Ok(data)
510 }
511
512 pub fn read_dir(&self, inode: u64) -> Result<Vec<DirEntry>, FsError> {
513 let path = self.path_for_inode(inode)?;
514 let dir_cid = self.resolve_dir_cid(&path)?;
515
516 let entries = block_on(self.tree.list_directory(&dir_cid))?;
517 let mut out = Vec::with_capacity(entries.len());
518
519 for entry in entries {
520 if Self::is_directory_refresh_sentinel(&entry.name) {
521 continue;
522 }
523 let child_inode = self.get_or_create_child_inode(inode, &entry.name)?;
524 out.push(DirEntry {
525 inode: child_inode,
526 name: entry.name,
527 kind: Self::kind_from_link(entry.link_type),
528 });
529 }
530
531 Ok(out)
532 }
533
534 pub fn create_file(&self, parent: u64, name: &str) -> Result<EntryAttr, FsError> {
535 self.ensure_valid_name(name)?;
536 let _guard = self.modify_lock.lock().unwrap();
537
538 let parent_path = self.path_for_inode(parent)?;
539 let mut child_path = parent_path.clone();
540 child_path.push(name.to_string());
541
542 if self.resolve_entry(&child_path).is_ok() {
543 return Err(FsError::AlreadyExists);
544 }
545
546 let (cid, size) = block_on(self.tree.put(&[]))?;
547 let link_type = self.link_type_for_size(size);
548 let meta = Self::file_entry_meta(&[]);
549 let new_root = block_on(self.tree.set_entry_with_meta(
550 &self.current_root(),
551 &self.path_refs(&parent_path),
552 name,
553 &cid,
554 size,
555 link_type,
556 Some(meta),
557 ))?;
558
559 self.apply_root_update(new_root)?;
560
561 let inode = self.insert_path(parent, name.to_string(), child_path);
562 Ok(EntryAttr {
563 inode,
564 size,
565 kind: EntryKind::File,
566 })
567 }
568
569 pub fn mkdir(&self, parent: u64, name: &str) -> Result<EntryAttr, FsError> {
570 self.ensure_valid_name(name)?;
571 let _guard = self.modify_lock.lock().unwrap();
572
573 let parent_path = self.path_for_inode(parent)?;
574 let mut child_path = parent_path.clone();
575 child_path.push(name.to_string());
576
577 if self.resolve_entry(&child_path).is_ok() {
578 return Err(FsError::AlreadyExists);
579 }
580
581 let dir_cid = block_on(self.tree.put_directory(Vec::new()))?;
582 let new_root = block_on(self.tree.set_entry(
583 &self.current_root(),
584 &self.path_refs(&parent_path),
585 name,
586 &dir_cid,
587 0,
588 LinkType::Dir,
589 ))?;
590
591 self.apply_root_update(new_root)?;
592
593 let inode = self.insert_path(parent, name.to_string(), child_path);
594 Ok(EntryAttr {
595 inode,
596 size: 0,
597 kind: EntryKind::Directory,
598 })
599 }
600
601 pub fn write_file(&self, inode: u64, offset: u64, data: &[u8]) -> Result<u32, FsError> {
602 let _guard = self.modify_lock.lock().unwrap();
603 let path = self.path_for_inode(inode)?;
604 let existing = self.read_file_full(&path)?;
605 let new_data = Self::apply_write(existing, offset, data);
606 self.update_file_at_path(&path, new_data)?;
607 Ok(data.len() as u32)
608 }
609
610 pub fn truncate_file(&self, inode: u64, size: u64) -> Result<(), FsError> {
611 let _guard = self.modify_lock.lock().unwrap();
612 let path = self.path_for_inode(inode)?;
613 let existing = self.read_file_full(&path)?;
614 let new_data = Self::apply_truncate(existing, size);
615 self.update_file_at_path(&path, new_data)?;
616 Ok(())
617 }
618
619 pub fn unlink(&self, parent: u64, name: &str) -> Result<(), FsError> {
620 if Self::is_directory_refresh_sentinel(name) {
621 return Ok(());
622 }
623 if self.take_refresh_unlink(parent, name)? {
624 return Ok(());
625 }
626 self.ensure_valid_name(name)?;
627 let _guard = self.modify_lock.lock().unwrap();
628
629 let parent_path = self.path_for_inode(parent)?;
630 let mut child_path = parent_path.clone();
631 child_path.push(name.to_string());
632 let entry = self.resolve_entry(&child_path)?;
633 if entry.link_type.is_directory_like() {
634 return Err(FsError::IsDir);
635 }
636
637 let new_root = block_on(self.tree.remove_entry(
638 &self.current_root(),
639 &self.path_refs(&parent_path),
640 name,
641 ))?;
642
643 self.apply_root_update(new_root)?;
644 self.remove_paths_prefix(&child_path);
645 self.children.write().unwrap().remove(&ChildKey {
646 parent,
647 name: name.to_string(),
648 });
649
650 Ok(())
651 }
652
653 pub fn rmdir(&self, parent: u64, name: &str) -> Result<(), FsError> {
654 if self.take_refresh_unlink(parent, name)? {
655 return Ok(());
656 }
657 self.ensure_valid_name(name)?;
658 let _guard = self.modify_lock.lock().unwrap();
659
660 let parent_path = self.path_for_inode(parent)?;
661 let mut child_path = parent_path.clone();
662 child_path.push(name.to_string());
663 let entry = self.resolve_entry(&child_path)?;
664 if !entry.link_type.is_directory_like() {
665 return Err(FsError::NotDir);
666 }
667
668 let dir_entries = block_on(self.tree.list_directory(&entry.cid))?;
669 if !dir_entries.is_empty() {
670 return Err(FsError::NotEmpty);
671 }
672
673 let new_root = block_on(self.tree.remove_entry(
674 &self.current_root(),
675 &self.path_refs(&parent_path),
676 name,
677 ))?;
678
679 self.apply_root_update(new_root)?;
680 self.remove_paths_prefix(&child_path);
681 self.children.write().unwrap().remove(&ChildKey {
682 parent,
683 name: name.to_string(),
684 });
685
686 Ok(())
687 }
688
689 pub fn rename(
690 &self,
691 parent: u64,
692 name: &str,
693 new_parent: u64,
694 new_name: &str,
695 ) -> Result<(), FsError> {
696 self.ensure_valid_name(name)?;
697 self.ensure_valid_name(new_name)?;
698 let _guard = self.modify_lock.lock().unwrap();
699
700 if parent == new_parent && name == new_name {
701 return Ok(());
702 }
703
704 let parent_path = self.path_for_inode(parent)?;
705 let new_parent_path = self.path_for_inode(new_parent)?;
706
707 let mut old_path = parent_path.clone();
708 old_path.push(name.to_string());
709 let entry = self.resolve_entry(&old_path)?;
710
711 let new_root = block_on(self.tree.set_entry_with_meta(
712 &self.current_root(),
713 &self.path_refs(&new_parent_path),
714 new_name,
715 &entry.cid,
716 entry.size,
717 entry.link_type,
718 entry.meta.clone(),
719 ))?;
720 let new_root = block_on(self.tree.remove_entry(
721 &new_root,
722 &self.path_refs(&parent_path),
723 name,
724 ))?;
725
726 self.apply_root_update(new_root)?;
727
728 let inode = self.get_or_create_child_inode(parent, name)?;
729 let mut new_path = new_parent_path.clone();
730 new_path.push(new_name.to_string());
731
732 self.children.write().unwrap().remove(&ChildKey {
733 parent,
734 name: name.to_string(),
735 });
736 self.children.write().unwrap().insert(
737 ChildKey {
738 parent: new_parent,
739 name: new_name.to_string(),
740 },
741 inode,
742 );
743
744 self.parents.write().unwrap().insert(inode, new_parent);
745 self.update_paths_prefix(&old_path, &new_path);
746
747 Ok(())
748 }
749
750 fn ensure_valid_name(&self, name: &str) -> Result<(), FsError> {
751 if name.is_empty() || name.contains('/') || Self::is_directory_refresh_sentinel(name) {
752 return Err(FsError::InvalidName);
753 }
754 Ok(())
755 }
756
757 fn is_directory_refresh_sentinel(name: &str) -> bool {
758 name == DIRECTORY_REFRESH_SENTINEL_NAME
759 }
760
761 fn is_directory_refresh_sentinel_path(path: &[String]) -> bool {
762 path.last()
763 .is_some_and(|name| Self::is_directory_refresh_sentinel(name))
764 }
765
766 fn directory_refresh_sentinel_attr(inode: u64) -> EntryAttr {
767 EntryAttr {
768 inode,
769 size: 0,
770 kind: EntryKind::File,
771 }
772 }
773
774 fn path_for_inode(&self, inode: u64) -> Result<Vec<String>, FsError> {
775 self.paths
776 .read()
777 .unwrap()
778 .get(&inode)
779 .cloned()
780 .ok_or(FsError::NotFound)
781 }
782
783 fn take_refresh_unlink(&self, parent: u64, name: &str) -> Result<bool, FsError> {
784 let parent_path = self.path_for_inode(parent)?;
785 let mut child_path = parent_path;
786 child_path.push(name.to_string());
787 Ok(self.refresh_unlinks.lock().unwrap().remove(&child_path))
788 }
789
790 fn resolve_entry(&self, path: &[String]) -> Result<ResolvedEntry, FsError> {
791 self.resolve_entry_at_root(&self.current_root(), path)
792 }
793
794 fn resolve_entry_at_root(&self, root: &Cid, path: &[String]) -> Result<ResolvedEntry, FsError> {
795 if path.is_empty() {
796 return Ok(ResolvedEntry {
797 cid: root.clone(),
798 link_type: LinkType::Dir,
799 size: 0,
800 meta: None,
801 });
802 }
803
804 let (parent_path, name) = path.split_at(path.len() - 1);
805 let parent_cid = self.resolve_dir_cid_at_root(root, parent_path)?;
806 let entries = block_on(self.tree.list_directory(&parent_cid))?;
807 let entry = entries
808 .into_iter()
809 .find(|e| e.name == name[0])
810 .ok_or(FsError::NotFound)?;
811
812 Ok(ResolvedEntry {
813 cid: Cid {
814 hash: entry.hash,
815 key: entry.key,
816 },
817 link_type: entry.link_type,
818 size: entry.size,
819 meta: entry.meta,
820 })
821 }
822
823 fn resolve_dir_cid(&self, path: &[String]) -> Result<Cid, FsError> {
824 self.resolve_dir_cid_at_root(&self.current_root(), path)
825 }
826
827 fn resolve_dir_cid_at_root(&self, root: &Cid, path: &[String]) -> Result<Cid, FsError> {
828 if path.is_empty() {
829 return Ok(root.clone());
830 }
831
832 let path_str = path.join("/");
833 let cid = block_on(self.tree.resolve(root, &path_str))?.ok_or(FsError::NotFound)?;
834
835 let is_dir = block_on(self.tree.is_dir(&cid))?;
836 if !is_dir {
837 return Err(FsError::NotDir);
838 }
839
840 Ok(cid)
841 }
842
843 #[cfg(feature = "fuse")]
844 fn directory_entry_names_at_root(
845 &self,
846 root: &Cid,
847 path: &[String],
848 ) -> Option<HashSet<String>> {
849 let dir_cid = self.resolve_dir_cid_at_root(root, path).ok()?;
850 let entries = block_on(self.tree.list_directory(&dir_cid)).ok()?;
851 Some(
852 entries
853 .into_iter()
854 .map(|entry| entry.name)
855 .filter(|name| !Self::is_directory_refresh_sentinel(name))
856 .collect(),
857 )
858 }
859
860 fn retain_existing_paths_after_root_update(&self) -> Result<(), FsError> {
861 let known: Vec<(u64, Vec<String>)> = self
862 .paths
863 .read()
864 .unwrap()
865 .iter()
866 .map(|(inode, path)| (*inode, path.clone()))
867 .collect();
868 let mut keep = HashSet::new();
869 keep.insert(ROOT_INODE);
870
871 for (inode, path) in known {
872 if inode == ROOT_INODE {
873 continue;
874 }
875 if Self::is_directory_refresh_sentinel_path(&path) {
876 if self.resolve_dir_cid(&path[..path.len() - 1]).is_ok() {
877 keep.insert(inode);
878 }
879 continue;
880 }
881 match self.resolve_entry(&path) {
882 Ok(_) => {
883 keep.insert(inode);
884 }
885 Err(FsError::NotFound) | Err(FsError::NotDir) => {}
886 Err(error) => return Err(error),
887 }
888 }
889
890 self.paths
891 .write()
892 .unwrap()
893 .retain(|inode, _| keep.contains(inode));
894 self.parents
895 .write()
896 .unwrap()
897 .retain(|inode, parent| keep.contains(inode) && keep.contains(parent));
898 self.children
899 .write()
900 .unwrap()
901 .retain(|key, inode| keep.contains(&key.parent) && keep.contains(inode));
902 Ok(())
903 }
904
905 fn entry_attr_from_resolved(
906 &self,
907 inode: u64,
908 entry: ResolvedEntry,
909 ) -> Result<EntryAttr, FsError> {
910 let kind = Self::kind_from_link(entry.link_type);
911 let size = if kind == EntryKind::Directory {
912 0
913 } else {
914 self.entry_size(&entry)?
915 };
916
917 Ok(EntryAttr { inode, size, kind })
918 }
919
920 fn entry_size(&self, entry: &ResolvedEntry) -> Result<u64, FsError> {
921 if entry.link_type.is_directory_like() {
922 return Ok(0);
923 }
924 if entry.size > 0 {
925 return Ok(entry.size);
926 }
927
928 let data = block_on(self.tree.get(&entry.cid, None))?.ok_or(FsError::NotFound)?;
929 Ok(data.len() as u64)
930 }
931
932 fn read_file_full(&self, path: &[String]) -> Result<Vec<u8>, FsError> {
933 let entry = self.resolve_entry(path)?;
934 if entry.link_type.is_directory_like() {
935 return Err(FsError::IsDir);
936 }
937 let data = block_on(self.tree.get(&entry.cid, None))?.ok_or(FsError::NotFound)?;
938 Ok(data)
939 }
940
941 fn update_file_at_path(&self, path: &[String], data: Vec<u8>) -> Result<(), FsError> {
942 let (parent_path, name) = path.split_at(path.len() - 1);
943 let (cid, size) = block_on(self.tree.put(&data))?;
944 let link_type = self.link_type_for_size(size);
945 let meta = Self::file_entry_meta(&data);
946
947 let new_root = block_on(self.tree.set_entry_with_meta(
948 &self.current_root(),
949 &self.path_refs(parent_path),
950 name[0].as_str(),
951 &cid,
952 size,
953 link_type,
954 Some(meta),
955 ))?;
956
957 self.apply_root_update(new_root)
958 }
959
960 fn file_entry_meta(data: &[u8]) -> HashMap<String, serde_json::Value> {
961 HashMap::from([(
962 WHOLE_FILE_HASH_META_KEY.to_string(),
963 serde_json::Value::String(to_hex(&sha256(data))),
964 )])
965 }
966
967 fn apply_root_update(&self, new_root: Cid) -> Result<(), FsError> {
968 if let Some(publisher) = &self.publisher {
969 publisher.publish(&new_root)?;
970 }
971 *self.root.write().unwrap() = new_root;
972 Ok(())
973 }
974
975 fn link_type_for_size(&self, size: u64) -> LinkType {
976 if size as usize > self.tree.chunk_size() {
977 LinkType::File
978 } else {
979 LinkType::Blob
980 }
981 }
982
983 fn get_or_create_child_inode(&self, parent: u64, name: &str) -> Result<u64, FsError> {
984 let key = ChildKey {
985 parent,
986 name: name.to_string(),
987 };
988 if let Some(inode) = self.children.read().unwrap().get(&key).copied() {
989 return Ok(inode);
990 }
991
992 let parent_path = self.path_for_inode(parent)?;
993 let mut child_path = parent_path.clone();
994 child_path.push(name.to_string());
995
996 if let Some(existing) = self.find_inode_by_path(&child_path) {
997 self.children.write().unwrap().insert(key, existing);
998 return Ok(existing);
999 }
1000
1001 Ok(self.insert_path(parent, name.to_string(), child_path))
1002 }
1003
1004 fn insert_path(&self, parent: u64, name: String, path: Vec<String>) -> u64 {
1005 let inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
1006 self.paths.write().unwrap().insert(inode, path);
1007 self.parents.write().unwrap().insert(inode, parent);
1008 self.children
1009 .write()
1010 .unwrap()
1011 .insert(ChildKey { parent, name }, inode);
1012 inode
1013 }
1014
1015 fn find_inode_by_path(&self, path: &[String]) -> Option<u64> {
1016 self.paths
1017 .read()
1018 .unwrap()
1019 .iter()
1020 .find_map(|(inode, inode_path)| {
1021 if inode_path == path {
1022 Some(*inode)
1023 } else {
1024 None
1025 }
1026 })
1027 }
1028
1029 fn update_paths_prefix(&self, old_prefix: &[String], new_prefix: &[String]) {
1030 let mut paths = self.paths.write().unwrap();
1031 for path in paths.values_mut() {
1032 if Self::path_has_prefix(path, old_prefix) {
1033 let mut updated = new_prefix.to_vec();
1034 updated.extend_from_slice(&path[old_prefix.len()..]);
1035 *path = updated;
1036 }
1037 }
1038 }
1039
1040 fn remove_paths_prefix(&self, prefix: &[String]) {
1041 let mut to_remove = Vec::new();
1042 {
1043 let paths = self.paths.read().unwrap();
1044 for (inode, path) in paths.iter() {
1045 if *inode == ROOT_INODE {
1046 continue;
1047 }
1048 if Self::path_has_prefix(path, prefix) {
1049 to_remove.push(*inode);
1050 }
1051 }
1052 }
1053
1054 if to_remove.is_empty() {
1055 return;
1056 }
1057
1058 let remove_set: std::collections::HashSet<u64> = to_remove.into_iter().collect();
1059 self.paths
1060 .write()
1061 .unwrap()
1062 .retain(|inode, _| !remove_set.contains(inode));
1063 self.parents
1064 .write()
1065 .unwrap()
1066 .retain(|inode, _| !remove_set.contains(inode));
1067 self.children
1068 .write()
1069 .unwrap()
1070 .retain(|_, inode| !remove_set.contains(inode));
1071 }
1072
1073 #[cfg(feature = "fuse")]
1074 fn drop_paths(&self, paths: impl IntoIterator<Item = Vec<String>>) {
1075 for path in paths {
1076 if let Some(inode) = self.find_inode_by_path(&path) {
1077 self.drop_inode(inode);
1078 }
1079 }
1080 }
1081
1082 fn drop_inode(&self, inode: u64) {
1083 if inode == ROOT_INODE {
1084 return;
1085 }
1086 let mut paths = self.paths.write().unwrap();
1087 let removed_path = paths.remove(&inode);
1088 drop(paths);
1089 self.parents.write().unwrap().remove(&inode);
1090 if let Some(path) = removed_path {
1091 if let Some((name, parent_path)) = path.split_last() {
1092 if let Some(parent_inode) = self.find_inode_by_path(parent_path) {
1093 self.children.write().unwrap().remove(&ChildKey {
1094 parent: parent_inode,
1095 name: name.to_string(),
1096 });
1097 }
1098 }
1099 }
1100 }
1101
1102 fn parent_inode(&self, inode: u64) -> u64 {
1103 self.parents
1104 .read()
1105 .unwrap()
1106 .get(&inode)
1107 .copied()
1108 .unwrap_or(ROOT_INODE)
1109 }
1110
1111 fn kind_from_link(link_type: LinkType) -> EntryKind {
1112 match link_type {
1113 LinkType::Dir | LinkType::Fanout => EntryKind::Directory,
1114 LinkType::Blob | LinkType::File => EntryKind::File,
1115 }
1116 }
1117
1118 fn apply_write(mut existing: Vec<u8>, offset: u64, data: &[u8]) -> Vec<u8> {
1119 let offset_usize = offset as usize;
1120 if existing.len() < offset_usize {
1121 existing.resize(offset_usize, 0);
1122 }
1123 if existing.len() < offset_usize + data.len() {
1124 existing.resize(offset_usize + data.len(), 0);
1125 }
1126 existing[offset_usize..offset_usize + data.len()].copy_from_slice(data);
1127 existing
1128 }
1129
1130 fn apply_truncate(mut existing: Vec<u8>, size: u64) -> Vec<u8> {
1131 let size = size as usize;
1132 if existing.len() > size {
1133 existing.truncate(size);
1134 } else if existing.len() < size {
1135 existing.resize(size, 0);
1136 }
1137 existing
1138 }
1139
1140 fn path_refs<'a>(&self, path: &'a [String]) -> Vec<&'a str> {
1141 path.iter().map(|p| p.as_str()).collect()
1142 }
1143
1144 fn path_has_prefix(path: &[String], prefix: &[String]) -> bool {
1145 if prefix.len() > path.len() {
1146 return false;
1147 }
1148 path.iter().zip(prefix.iter()).all(|(a, b)| a == b)
1149 }
1150}
1151
1152#[cfg(feature = "fuse")]
1153mod fuse_impl {
1154 use super::*;
1155 use fuser::{
1156 FileAttr, FileType, Filesystem, MountOption, ReplyAttr, ReplyCreate, ReplyData,
1157 ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyStatfs, ReplyWrite, Request,
1158 };
1159 use std::ffi::CString;
1160 use std::ffi::OsStr;
1161 use std::path::Path;
1162 use std::time::{Duration, SystemTime};
1163
1164 const TTL: Duration = Duration::ZERO;
1165 const FALLBACK_BLOCK_SIZE: u32 = 4096;
1166 const FALLBACK_TOTAL_BYTES: u64 = 1 << 40;
1167 const FALLBACK_FREE_BYTES: u64 = 1 << 39;
1168 const FALLBACK_TOTAL_FILES: u64 = 1_000_000;
1169 const FALLBACK_FREE_FILES: u64 = 900_000;
1170
1171 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1172 pub(crate) struct FsStats {
1173 pub(crate) blocks: u64,
1174 pub(crate) bfree: u64,
1175 pub(crate) bavail: u64,
1176 pub(crate) files: u64,
1177 pub(crate) ffree: u64,
1178 pub(crate) bsize: u32,
1179 pub(crate) namelen: u32,
1180 pub(crate) frsize: u32,
1181 }
1182
1183 impl FsError {
1184 fn errno(&self) -> i32 {
1185 match self {
1186 FsError::InvalidRoot | FsError::InvalidName => libc::EINVAL,
1187 FsError::NotFound => libc::ENOENT,
1188 FsError::NotDir => libc::ENOTDIR,
1189 FsError::IsDir => libc::EISDIR,
1190 FsError::AlreadyExists => libc::EEXIST,
1191 FsError::NotEmpty => libc::ENOTEMPTY,
1192 FsError::Tree(_) | FsError::Publish(_) => libc::EIO,
1193 }
1194 }
1195 }
1196
1197 pub(crate) fn fallback_fs_stats() -> FsStats {
1198 let blocks = FALLBACK_TOTAL_BYTES / u64::from(FALLBACK_BLOCK_SIZE);
1199 let free_blocks = FALLBACK_FREE_BYTES / u64::from(FALLBACK_BLOCK_SIZE);
1200 FsStats {
1201 blocks,
1202 bfree: free_blocks,
1203 bavail: free_blocks,
1204 files: FALLBACK_TOTAL_FILES,
1205 ffree: FALLBACK_FREE_FILES,
1206 bsize: FALLBACK_BLOCK_SIZE,
1207 namelen: 255,
1208 frsize: FALLBACK_BLOCK_SIZE,
1209 }
1210 }
1211
1212 fn checked_u32<T: TryInto<u32>>(value: T) -> Option<u32> {
1213 value.try_into().ok()
1214 }
1215
1216 fn host_fs_stats(path: &Path) -> Option<FsStats> {
1217 let path = CString::new(path.as_os_str().as_encoded_bytes()).ok()?;
1218 let mut stat = std::mem::MaybeUninit::<libc::statfs>::uninit();
1219 let result = unsafe { libc::statfs(path.as_ptr(), stat.as_mut_ptr()) };
1220 if result != 0 {
1221 return None;
1222 }
1223
1224 let stat = unsafe { stat.assume_init() };
1225 let bsize = checked_u32(stat.f_bsize)?;
1226 Some(FsStats {
1227 blocks: stat.f_blocks,
1228 bfree: stat.f_bfree,
1229 bavail: stat.f_bavail,
1230 files: stat.f_files,
1231 ffree: stat.f_ffree,
1232 bsize,
1233 namelen: 255,
1234 frsize: bsize,
1235 })
1236 }
1237
1238 pub(crate) fn current_fs_stats() -> FsStats {
1239 host_fs_stats(Path::new("/")).unwrap_or_else(fallback_fs_stats)
1240 }
1241
1242 impl<S: Store + Send + Sync + 'static> HashtreeFuse<S> {
1243 pub fn mount(
1244 self,
1245 mountpoint: impl AsRef<Path>,
1246 options: &[MountOption],
1247 ) -> std::io::Result<()> {
1248 let mut session = fuser::Session::new(self.clone(), mountpoint.as_ref(), options)?;
1249 self.set_notifier(session.notifier());
1250 let result = session.run();
1251 self.clear_notifier();
1252 result
1253 }
1254
1255 fn file_attr(&self, attr: &EntryAttr) -> FileAttr {
1256 let (kind, perm, nlink) = match attr.kind {
1257 EntryKind::Directory => (FileType::Directory, 0o755, 2),
1258 EntryKind::File => (FileType::RegularFile, 0o644, 1),
1259 };
1260 let uid = unsafe { libc::geteuid() };
1261 let gid = unsafe { libc::getegid() };
1262 let blocks = attr.size.div_ceil(512);
1263
1264 FileAttr {
1265 ino: attr.inode,
1266 size: attr.size,
1267 blocks,
1268 atime: SystemTime::UNIX_EPOCH,
1269 mtime: SystemTime::UNIX_EPOCH,
1270 ctime: SystemTime::UNIX_EPOCH,
1271 crtime: SystemTime::UNIX_EPOCH,
1272 kind,
1273 perm,
1274 nlink,
1275 uid,
1276 gid,
1277 rdev: 0,
1278 blksize: 512,
1279 flags: 0,
1280 }
1281 }
1282
1283 fn file_type(kind: EntryKind) -> FileType {
1284 match kind {
1285 EntryKind::Directory => FileType::Directory,
1286 EntryKind::File => FileType::RegularFile,
1287 }
1288 }
1289 }
1290
1291 impl<S: Store + Send + Sync + 'static> Filesystem for HashtreeFuse<S> {
1292 fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
1293 let name = match name.to_str() {
1294 Some(value) => value,
1295 None => {
1296 reply.error(libc::ENOENT);
1297 return;
1298 }
1299 };
1300
1301 match self.lookup_child(parent, name) {
1302 Ok(attr) => reply.entry(&TTL, &self.file_attr(&attr), 0),
1303 Err(err) => reply.error(err.errno()),
1304 }
1305 }
1306
1307 fn getattr(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyAttr) {
1308 match self.get_attr(ino) {
1309 Ok(attr) => reply.attr(&TTL, &self.file_attr(&attr)),
1310 Err(err) => reply.error(err.errno()),
1311 }
1312 }
1313
1314 fn open(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: fuser::ReplyOpen) {
1315 reply.opened(0, 0);
1316 }
1317
1318 fn read(
1319 &mut self,
1320 _req: &Request<'_>,
1321 ino: u64,
1322 _fh: u64,
1323 offset: i64,
1324 size: u32,
1325 _flags: i32,
1326 _lock_owner: Option<u64>,
1327 reply: ReplyData,
1328 ) {
1329 let offset = if offset < 0 { 0 } else { offset as u64 };
1330 match self.read_file(ino, offset, size) {
1331 Ok(data) => reply.data(&data),
1332 Err(err) => reply.error(err.errno()),
1333 }
1334 }
1335
1336 fn write(
1337 &mut self,
1338 _req: &Request<'_>,
1339 ino: u64,
1340 _fh: u64,
1341 offset: i64,
1342 data: &[u8],
1343 _write_flags: u32,
1344 _flags: i32,
1345 _lock_owner: Option<u64>,
1346 reply: ReplyWrite,
1347 ) {
1348 let offset = if offset < 0 { 0 } else { offset as u64 };
1349 match self.write_file(ino, offset, data) {
1350 Ok(written) => reply.written(written),
1351 Err(err) => reply.error(err.errno()),
1352 }
1353 }
1354
1355 fn create(
1356 &mut self,
1357 _req: &Request<'_>,
1358 parent: u64,
1359 name: &OsStr,
1360 _mode: u32,
1361 _umask: u32,
1362 _flags: i32,
1363 reply: ReplyCreate,
1364 ) {
1365 let name = match name.to_str() {
1366 Some(value) => value,
1367 None => {
1368 reply.error(libc::EINVAL);
1369 return;
1370 }
1371 };
1372
1373 match self.create_file(parent, name) {
1374 Ok(attr) => reply.created(&TTL, &self.file_attr(&attr), 0, 0, 0),
1375 Err(err) => reply.error(err.errno()),
1376 }
1377 }
1378
1379 fn mkdir(
1380 &mut self,
1381 _req: &Request<'_>,
1382 parent: u64,
1383 name: &OsStr,
1384 _mode: u32,
1385 _umask: u32,
1386 reply: ReplyEntry,
1387 ) {
1388 let name = match name.to_str() {
1389 Some(value) => value,
1390 None => {
1391 reply.error(libc::EINVAL);
1392 return;
1393 }
1394 };
1395
1396 match HashtreeFuse::mkdir(self, parent, name) {
1397 Ok(attr) => reply.entry(&TTL, &self.file_attr(&attr), 0),
1398 Err(err) => reply.error(err.errno()),
1399 }
1400 }
1401
1402 fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
1403 let name = match name.to_str() {
1404 Some(value) => value,
1405 None => {
1406 reply.error(libc::EINVAL);
1407 return;
1408 }
1409 };
1410
1411 match HashtreeFuse::unlink(self, parent, name) {
1412 Ok(()) => reply.ok(),
1413 Err(err) => reply.error(err.errno()),
1414 }
1415 }
1416
1417 fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
1418 let name = match name.to_str() {
1419 Some(value) => value,
1420 None => {
1421 reply.error(libc::EINVAL);
1422 return;
1423 }
1424 };
1425
1426 match HashtreeFuse::rmdir(self, parent, name) {
1427 Ok(()) => reply.ok(),
1428 Err(err) => reply.error(err.errno()),
1429 }
1430 }
1431
1432 fn rename(
1433 &mut self,
1434 _req: &Request<'_>,
1435 parent: u64,
1436 name: &OsStr,
1437 newparent: u64,
1438 newname: &OsStr,
1439 _flags: u32,
1440 reply: ReplyEmpty,
1441 ) {
1442 let name = match name.to_str() {
1443 Some(value) => value,
1444 None => {
1445 reply.error(libc::EINVAL);
1446 return;
1447 }
1448 };
1449 let newname = match newname.to_str() {
1450 Some(value) => value,
1451 None => {
1452 reply.error(libc::EINVAL);
1453 return;
1454 }
1455 };
1456
1457 match HashtreeFuse::rename(self, parent, name, newparent, newname) {
1458 Ok(()) => reply.ok(),
1459 Err(err) => reply.error(err.errno()),
1460 }
1461 }
1462
1463 fn setattr(
1464 &mut self,
1465 _req: &Request<'_>,
1466 ino: u64,
1467 _mode: Option<u32>,
1468 _uid: Option<u32>,
1469 _gid: Option<u32>,
1470 size: Option<u64>,
1471 _atime: Option<fuser::TimeOrNow>,
1472 _mtime: Option<fuser::TimeOrNow>,
1473 _ctime: Option<SystemTime>,
1474 _fh: Option<u64>,
1475 _crtime: Option<SystemTime>,
1476 _chgtime: Option<SystemTime>,
1477 _bkuptime: Option<SystemTime>,
1478 _flags: Option<u32>,
1479 reply: ReplyAttr,
1480 ) {
1481 if let Some(size) = size {
1482 match self.truncate_file(ino, size) {
1483 Ok(()) => {
1484 if let Ok(attr) = self.get_attr(ino) {
1485 reply.attr(&TTL, &self.file_attr(&attr));
1486 } else {
1487 reply.error(libc::EIO);
1488 }
1489 }
1490 Err(err) => reply.error(err.errno()),
1491 }
1492 } else {
1493 match self.get_attr(ino) {
1494 Ok(attr) => reply.attr(&TTL, &self.file_attr(&attr)),
1495 Err(err) => reply.error(err.errno()),
1496 }
1497 }
1498 }
1499
1500 fn readdir(
1501 &mut self,
1502 _req: &Request<'_>,
1503 ino: u64,
1504 _fh: u64,
1505 offset: i64,
1506 mut reply: ReplyDirectory,
1507 ) {
1508 let mut entries = Vec::new();
1509 entries.push((ino, EntryKind::Directory, ".".to_string()));
1510 let parent = self.parent_inode(ino);
1511 entries.push((parent, EntryKind::Directory, "..".to_string()));
1512
1513 match self.read_dir(ino) {
1514 Ok(children) => {
1515 for entry in children {
1516 entries.push((entry.inode, entry.kind, entry.name));
1517 }
1518 }
1519 Err(err) => {
1520 reply.error(err.errno());
1521 return;
1522 }
1523 }
1524
1525 let start = if offset < 0 { 0 } else { offset as usize };
1526 for (index, (inode, kind, name)) in entries.into_iter().enumerate().skip(start) {
1527 let next_offset = (index + 1) as i64;
1528 let full = reply.add(inode, next_offset, Self::file_type(kind), name);
1529 if full {
1530 break;
1531 }
1532 }
1533
1534 reply.ok();
1535 }
1536
1537 fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) {
1538 let stats = current_fs_stats();
1539 reply.statfs(
1540 stats.blocks,
1541 stats.bfree,
1542 stats.bavail,
1543 stats.files,
1544 stats.ffree,
1545 stats.bsize,
1546 stats.namelen,
1547 stats.frsize,
1548 );
1549 }
1550 }
1551}
1552
1553#[cfg(test)]
1554mod tests {
1555 use super::*;
1556 use hashtree_core::store::MemoryStore;
1557
1558 #[cfg(feature = "fuse")]
1559 use super::fuse_impl::{current_fs_stats, fallback_fs_stats};
1560
1561 struct RecordingPublisher {
1562 updates: Mutex<Vec<Cid>>,
1563 }
1564
1565 impl RecordingPublisher {
1566 fn new() -> Self {
1567 Self {
1568 updates: Mutex::new(Vec::new()),
1569 }
1570 }
1571
1572 fn updates(&self) -> Vec<Cid> {
1573 self.updates.lock().unwrap().clone()
1574 }
1575 }
1576
1577 impl RootPublisher for RecordingPublisher {
1578 fn publish(&self, cid: &Cid) -> Result<(), FsError> {
1579 self.updates.lock().unwrap().push(cid.clone());
1580 Ok(())
1581 }
1582 }
1583
1584 async fn empty_root(store: Arc<MemoryStore>) -> Cid {
1585 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1586 tree.put_directory(Vec::new()).await.unwrap()
1587 }
1588
1589 #[tokio::test]
1590 async fn test_create_write_read_file() {
1591 let store = Arc::new(MemoryStore::new());
1592 let root = empty_root(store.clone()).await;
1593 let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1594
1595 let attr = fs.create_file(ROOT_INODE, "hello.txt").unwrap();
1596 assert_eq!(attr.kind, EntryKind::File);
1597
1598 fs.write_file(attr.inode, 0, b"hello").unwrap();
1599 let read = fs.read_file(attr.inode, 0, 5).unwrap();
1600 assert_eq!(read, b"hello");
1601
1602 let tree = HashTree::new(HashTreeConfig::new(store));
1603 let entries = tree.list_directory(&fs.current_root()).await.unwrap();
1604 let file = entries
1605 .iter()
1606 .find(|entry| entry.name == "hello.txt")
1607 .unwrap();
1608 assert_eq!(
1609 file.meta
1610 .as_ref()
1611 .and_then(|meta| meta.get("whole_file_hash"))
1612 .and_then(serde_json::Value::as_str),
1613 Some("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
1614 );
1615 }
1616
1617 #[tokio::test]
1618 async fn test_mkdir_and_rename() {
1619 let store = Arc::new(MemoryStore::new());
1620 let root = empty_root(store.clone()).await;
1621 let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1622
1623 let dir = fs.mkdir(ROOT_INODE, "docs").unwrap();
1624 let file = fs.create_file(dir.inode, "draft.txt").unwrap();
1625 fs.write_file(file.inode, 0, b"data").unwrap();
1626
1627 fs.rename(dir.inode, "draft.txt", dir.inode, "final.txt")
1628 .unwrap();
1629 let entries = fs.read_dir(dir.inode).unwrap();
1630 let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
1631 assert!(names.contains(&"final.txt".to_string()));
1632 assert!(!names.contains(&"draft.txt".to_string()));
1633
1634 let tree = HashTree::new(HashTreeConfig::new(store));
1635 let docs = tree
1636 .resolve(&fs.current_root(), "docs")
1637 .await
1638 .unwrap()
1639 .unwrap();
1640 let entries = tree.list_directory(&docs).await.unwrap();
1641 let file = entries
1642 .iter()
1643 .find(|entry| entry.name == "final.txt")
1644 .unwrap();
1645 let expected_hash = to_hex(&sha256(b"data"));
1646 assert_eq!(
1647 file.meta
1648 .as_ref()
1649 .and_then(|meta| meta.get("whole_file_hash"))
1650 .and_then(serde_json::Value::as_str),
1651 Some(expected_hash.as_str())
1652 );
1653 }
1654
1655 #[tokio::test]
1656 async fn test_truncate_file() {
1657 let store = Arc::new(MemoryStore::new());
1658 let root = empty_root(store.clone()).await;
1659 let fs = HashtreeFuse::new(store, root).unwrap();
1660
1661 let file = fs.create_file(ROOT_INODE, "file.bin").unwrap();
1662 fs.write_file(file.inode, 0, b"abcdef").unwrap();
1663 fs.truncate_file(file.inode, 3).unwrap();
1664 let read = fs.read_file(file.inode, 0, 10).unwrap();
1665 assert_eq!(read, b"abc");
1666 }
1667
1668 #[tokio::test]
1669 async fn test_directory_refresh_sentinel_is_virtual() {
1670 let store = Arc::new(MemoryStore::new());
1671 let root = empty_root(store.clone()).await;
1672 let fs = HashtreeFuse::new(store, root.clone()).unwrap();
1673
1674 let entries = fs.read_dir(ROOT_INODE).unwrap();
1675 assert!(!entries
1676 .iter()
1677 .any(|entry| entry.name == DIRECTORY_REFRESH_SENTINEL_NAME));
1678 let sentinel = fs
1679 .lookup_child(ROOT_INODE, DIRECTORY_REFRESH_SENTINEL_NAME)
1680 .unwrap();
1681 assert_eq!(sentinel.kind, EntryKind::File);
1682 assert!(fs.read_file(sentinel.inode, 0, 10).unwrap().is_empty());
1683 assert!(fs
1684 .create_file(ROOT_INODE, DIRECTORY_REFRESH_SENTINEL_NAME)
1685 .is_err());
1686 fs.unlink(ROOT_INODE, DIRECTORY_REFRESH_SENTINEL_NAME)
1687 .unwrap();
1688 assert_eq!(fs.current_root(), root);
1689 }
1690
1691 #[test]
1692 fn test_directory_refresh_sentinel_is_not_hidden() {
1693 assert!(!DIRECTORY_REFRESH_SENTINEL_NAME.starts_with('.'));
1694 }
1695
1696 #[tokio::test]
1697 async fn test_publisher_invoked() {
1698 let store = Arc::new(MemoryStore::new());
1699 let root = empty_root(store.clone()).await;
1700 let publisher = Arc::new(RecordingPublisher::new());
1701 let fs = HashtreeFuse::new_with_publisher(store, root, Some(publisher.clone())).unwrap();
1702
1703 let file = fs.create_file(ROOT_INODE, "note.txt").unwrap();
1704 fs.write_file(file.inode, 0, b"note").unwrap();
1705
1706 let updates = publisher.updates();
1707 assert!(!updates.is_empty());
1708 assert_eq!(updates.last().unwrap(), &fs.current_root());
1709 }
1710
1711 #[tokio::test]
1712 async fn test_replace_root_refreshes_visible_tree_without_publishing() {
1713 let store = Arc::new(MemoryStore::new());
1714 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1715 let root = empty_root(store.clone()).await;
1716 let publisher = Arc::new(RecordingPublisher::new());
1717 let fs = HashtreeFuse::new_with_publisher(store, root, Some(publisher.clone())).unwrap();
1718
1719 let old_file = fs.create_file(ROOT_INODE, "old.txt").unwrap();
1720 fs.write_file(old_file.inode, 0, b"old").unwrap();
1721 assert!(!publisher.updates().is_empty());
1722 publisher.updates.lock().unwrap().clear();
1723
1724 let (new_blob, new_size) = tree.put(b"new").await.unwrap();
1725 let new_root = tree
1726 .put_directory(vec![hashtree_core::DirEntry::from_cid(
1727 "new.txt", &new_blob,
1728 )
1729 .with_size(new_size)
1730 .with_link_type(LinkType::Blob)])
1731 .await
1732 .unwrap();
1733
1734 let old_lookup = fs.lookup_child(ROOT_INODE, "old.txt").unwrap();
1735 fs.replace_root(new_root.clone()).unwrap();
1736
1737 assert_eq!(fs.current_root(), new_root);
1738 assert!(fs.lookup_child(ROOT_INODE, "old.txt").is_err());
1739 let new_lookup = fs.lookup_child(ROOT_INODE, "new.txt").unwrap();
1740 assert_ne!(old_lookup.inode, new_lookup.inode);
1741 assert_eq!(fs.read_file(new_lookup.inode, 0, 3).unwrap(), b"new");
1742 assert!(publisher.updates().is_empty());
1743 }
1744
1745 #[tokio::test]
1746 async fn test_replace_root_if_current_preserves_dirty_root() {
1747 let store = Arc::new(MemoryStore::new());
1748 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1749 let root = empty_root(store.clone()).await;
1750 let fs = HashtreeFuse::new(store, root.clone()).unwrap();
1751
1752 let local = fs.create_file(ROOT_INODE, "local.txt").unwrap();
1753 fs.write_file(local.inode, 0, b"local").unwrap();
1754 let dirty_root = fs.current_root();
1755
1756 let (remote_blob, remote_size) = tree.put(b"remote").await.unwrap();
1757 let remote_root = tree
1758 .put_directory(vec![hashtree_core::DirEntry::from_cid(
1759 "remote.txt",
1760 &remote_blob,
1761 )
1762 .with_size(remote_size)
1763 .with_link_type(LinkType::Blob)])
1764 .await
1765 .unwrap();
1766
1767 let replaced = fs
1768 .replace_root_if_current(&root, remote_root.clone())
1769 .unwrap();
1770
1771 assert!(!replaced);
1772 assert_eq!(fs.current_root(), dirty_root);
1773 assert!(fs.lookup_child(ROOT_INODE, "local.txt").is_ok());
1774 assert!(fs.lookup_child(ROOT_INODE, "remote.txt").is_err());
1775
1776 let replaced = fs
1777 .replace_root_if_current(&dirty_root, remote_root.clone())
1778 .unwrap();
1779 assert!(replaced);
1780 assert_eq!(fs.current_root(), remote_root);
1781 }
1782
1783 #[cfg(feature = "fuse")]
1784 #[tokio::test]
1785 async fn test_replace_root_invalidates_removed_entries_without_inode_notification() {
1786 let store = Arc::new(MemoryStore::new());
1787 let root = empty_root(store.clone()).await;
1788 let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1789
1790 let old_file = fs.create_file(ROOT_INODE, "old.txt").unwrap();
1791 fs.write_file(old_file.inode, 0, b"old").unwrap();
1792 let new_root = empty_root(store).await;
1793
1794 let invalidations = fs.changed_known_entries_for_root(&new_root);
1795
1796 assert!(invalidations.contains(&FuseInvalidation::Entry {
1797 parent: ROOT_INODE,
1798 name: "old.txt".to_string(),
1799 }));
1800 assert_eq!(invalidations.len(), 2);
1801 }
1802
1803 #[cfg(feature = "fuse")]
1804 #[tokio::test]
1805 async fn test_replace_root_emits_delete_invalidation_for_removed_entries() {
1806 let store = Arc::new(MemoryStore::new());
1807 let root = empty_root(store.clone()).await;
1808 let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1809
1810 let old_file = fs.create_file(ROOT_INODE, "old.txt").unwrap();
1811 fs.write_file(old_file.inode, 0, b"old").unwrap();
1812 let new_root = empty_root(store).await;
1813
1814 let invalidations = fs.changed_known_entries_for_root(&new_root);
1815
1816 assert!(invalidations.contains(&FuseInvalidation::Delete {
1817 parent: ROOT_INODE,
1818 child: old_file.inode,
1819 name: "old.txt".to_string(),
1820 }));
1821 assert_eq!(
1822 fs.removed_known_entry_paths_for_root(&new_root),
1823 vec![vec!["old.txt".to_string()]]
1824 );
1825 let delete_index = invalidations
1826 .iter()
1827 .position(|invalidation| {
1828 matches!(invalidation, FuseInvalidation::Delete { name, .. } if name == "old.txt")
1829 })
1830 .unwrap();
1831 let entry_index = invalidations
1832 .iter()
1833 .position(|invalidation| {
1834 matches!(invalidation, FuseInvalidation::Entry { name, .. } if name == "old.txt")
1835 })
1836 .unwrap();
1837 assert!(entry_index < delete_index);
1838 }
1839
1840 #[cfg(feature = "fuse")]
1841 #[tokio::test]
1842 async fn test_replace_root_invalidates_changed_known_entry_by_name_and_new_inode() {
1843 let store = Arc::new(MemoryStore::new());
1844 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1845 let (old_blob, old_size) = tree.put(b"old").await.unwrap();
1846 let root = tree
1847 .put_directory(vec![hashtree_core::DirEntry::from_cid(
1848 "note.txt", &old_blob,
1849 )
1850 .with_size(old_size)
1851 .with_link_type(LinkType::Blob)])
1852 .await
1853 .unwrap();
1854 let fs = HashtreeFuse::new(store, root).unwrap();
1855 let old_file = fs.lookup_child(ROOT_INODE, "note.txt").unwrap();
1856
1857 let (new_blob, new_size) = tree.put(b"new").await.unwrap();
1858 let new_root = tree
1859 .put_directory(vec![hashtree_core::DirEntry::from_cid(
1860 "note.txt", &new_blob,
1861 )
1862 .with_size(new_size)
1863 .with_link_type(LinkType::Blob)])
1864 .await
1865 .unwrap();
1866
1867 let invalidations = fs.changed_known_entries_for_root(&new_root);
1868
1869 assert!(invalidations.contains(&FuseInvalidation::Entry {
1870 parent: ROOT_INODE,
1871 name: "note.txt".to_string(),
1872 }));
1873 assert_eq!(invalidations.len(), 1);
1874
1875 fs.replace_root(new_root).unwrap();
1876 let new_file = fs.lookup_child(ROOT_INODE, "note.txt").unwrap();
1877 assert_ne!(old_file.inode, new_file.inode);
1878 assert_eq!(fs.read_file(new_file.inode, 0, 3).unwrap(), b"new");
1879 }
1880
1881 #[cfg(feature = "fuse")]
1882 #[tokio::test]
1883 async fn test_replace_root_invalidates_removed_name_without_known_child_inode() {
1884 let store = Arc::new(MemoryStore::new());
1885 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1886 let (old_blob, old_size) = tree.put(b"old").await.unwrap();
1887 let root = tree
1888 .put_directory(vec![hashtree_core::DirEntry::from_cid(
1889 "old.txt", &old_blob,
1890 )
1891 .with_size(old_size)
1892 .with_link_type(LinkType::Blob)])
1893 .await
1894 .unwrap();
1895 let fs = HashtreeFuse::new(store.clone(), root).unwrap();
1896 let new_root = empty_root(store).await;
1897
1898 let invalidations = fs.changed_known_entries_for_root(&new_root);
1899
1900 assert!(invalidations.contains(&FuseInvalidation::Entry {
1901 parent: ROOT_INODE,
1902 name: "old.txt".to_string(),
1903 }));
1904 }
1905
1906 #[tokio::test]
1907 async fn test_refresh_unlink_does_not_publish_or_change_root() {
1908 let store = Arc::new(MemoryStore::new());
1909 let root = empty_root(store.clone()).await;
1910 let publisher = Arc::new(RecordingPublisher::new());
1911 let fs = HashtreeFuse::new_with_publisher(store, root, Some(publisher.clone())).unwrap();
1912
1913 let old_file = fs.create_file(ROOT_INODE, "old.txt").unwrap();
1914 fs.write_file(old_file.inode, 0, b"old").unwrap();
1915 let old_root = fs.current_root();
1916 publisher.updates.lock().unwrap().clear();
1917
1918 fs.begin_refresh_unlinks(vec![vec!["old.txt".to_string()]]);
1919 fs.unlink(ROOT_INODE, "old.txt").unwrap();
1920
1921 assert_eq!(fs.current_root(), old_root);
1922 assert!(publisher.updates().is_empty());
1923 assert!(fs.lookup_child(ROOT_INODE, "old.txt").is_ok());
1924 }
1925
1926 #[cfg(feature = "fuse")]
1927 #[tokio::test]
1928 async fn test_replace_root_invalidates_added_entries_by_name() {
1929 let store = Arc::new(MemoryStore::new());
1930 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1931 let root = empty_root(store.clone()).await;
1932 let fs = HashtreeFuse::new(store, root).unwrap();
1933
1934 let (new_blob, new_size) = tree.put(b"new").await.unwrap();
1935 let new_root = tree
1936 .put_directory(vec![hashtree_core::DirEntry::from_cid(
1937 "new.txt", &new_blob,
1938 )
1939 .with_size(new_size)
1940 .with_link_type(LinkType::Blob)])
1941 .await
1942 .unwrap();
1943
1944 let invalidations = fs.changed_known_entries_for_root(&new_root);
1945
1946 assert!(invalidations.contains(&FuseInvalidation::Entry {
1947 parent: ROOT_INODE,
1948 name: "new.txt".to_string(),
1949 }));
1950 }
1951
1952 #[tokio::test]
1953 async fn test_replace_root_preserves_existing_directory_inodes() {
1954 let store = Arc::new(MemoryStore::new());
1955 let tree = HashTree::new(HashTreeConfig::new(store.clone()));
1956 let root = empty_root(store.clone()).await;
1957 let fs = HashtreeFuse::new(store, root).unwrap();
1958
1959 let docs = fs.mkdir(ROOT_INODE, "docs").unwrap();
1960 let old_file = fs.create_file(docs.inode, "old.txt").unwrap();
1961 fs.write_file(old_file.inode, 0, b"old").unwrap();
1962 let old_entries = fs.read_dir(docs.inode).unwrap();
1963 assert!(old_entries.iter().any(|entry| entry.name == "old.txt"));
1964
1965 let (new_blob, new_size) = tree.put(b"new").await.unwrap();
1966 let new_docs = tree
1967 .put_directory(vec![hashtree_core::DirEntry::from_cid(
1968 "new.txt", &new_blob,
1969 )
1970 .with_size(new_size)
1971 .with_link_type(LinkType::Blob)])
1972 .await
1973 .unwrap();
1974 let new_root = tree
1975 .put_directory(vec![
1976 hashtree_core::DirEntry::from_cid("docs", &new_docs).with_link_type(LinkType::Dir)
1977 ])
1978 .await
1979 .unwrap();
1980
1981 fs.replace_root(new_root).unwrap();
1982
1983 assert_eq!(
1984 fs.lookup_child(ROOT_INODE, "docs").unwrap().inode,
1985 docs.inode
1986 );
1987 assert_eq!(fs.get_attr(docs.inode).unwrap().kind, EntryKind::Directory);
1988 let new_entries = fs.read_dir(docs.inode).unwrap();
1989 let names: Vec<String> = new_entries.into_iter().map(|entry| entry.name).collect();
1990 assert!(names.contains(&"new.txt".to_string()));
1991 assert!(!names.contains(&"old.txt".to_string()));
1992 }
1993
1994 #[tokio::test]
1995 async fn test_fanout_directory_entry_behaves_as_directory() {
1996 let store = Arc::new(MemoryStore::new());
1997 let tree = HashTree::new(HashTreeConfig::new(store.clone()).with_max_links(2));
1998 let mut fanout_entries = Vec::new();
1999 for index in 0..5 {
2000 let body = format!("file-{index}");
2001 let (cid, size) = tree.put(body.as_bytes()).await.unwrap();
2002 fanout_entries.push(
2003 hashtree_core::DirEntry::from_cid(format!("file-{index}.txt"), &cid)
2004 .with_size(size)
2005 .with_link_type(LinkType::Blob),
2006 );
2007 }
2008 let fanout_dir = tree.put_directory(fanout_entries).await.unwrap();
2009 let root = tree
2010 .put_directory(vec![hashtree_core::DirEntry::from_cid("big", &fanout_dir)
2011 .with_link_type(LinkType::Fanout)])
2012 .await
2013 .unwrap();
2014 let fs = HashtreeFuse::new(store, root).unwrap();
2015
2016 let big = fs.lookup_child(ROOT_INODE, "big").unwrap();
2017 assert_eq!(big.kind, EntryKind::Directory);
2018 assert!(matches!(fs.read_file(big.inode, 0, 8), Err(FsError::IsDir)));
2019 assert!(matches!(fs.unlink(ROOT_INODE, "big"), Err(FsError::IsDir)));
2020 assert!(matches!(
2021 fs.rmdir(ROOT_INODE, "big"),
2022 Err(FsError::NotEmpty)
2023 ));
2024
2025 let names: Vec<String> = fs
2026 .read_dir(big.inode)
2027 .unwrap()
2028 .into_iter()
2029 .map(|entry| entry.name)
2030 .collect();
2031 assert!(names.contains(&"file-3.txt".to_string()));
2032
2033 let file = fs.lookup_child(big.inode, "file-3.txt").unwrap();
2034 assert_eq!(file.kind, EntryKind::File);
2035 assert_eq!(fs.read_file(file.inode, 0, 6).unwrap(), b"file-3");
2036 }
2037
2038 #[cfg(feature = "fuse")]
2039 #[test]
2040 fn test_fallback_fs_stats_reports_free_space() {
2041 let stats = fallback_fs_stats();
2042 assert!(stats.blocks > 0);
2043 assert!(stats.bfree > 0);
2044 assert!(stats.bavail > 0);
2045 assert!(stats.bsize > 0);
2046 assert_eq!(stats.bfree, stats.bavail);
2047 }
2048
2049 #[cfg(feature = "fuse")]
2050 #[test]
2051 fn test_current_fs_stats_reports_free_space() {
2052 let stats = current_fs_stats();
2053 assert!(stats.blocks > 0);
2054 assert!(stats.bfree > 0);
2055 assert!(stats.bavail > 0);
2056 assert!(stats.bsize > 0);
2057 }
2058}