Skip to main content

ragfs_fuse/filesystem/
passthrough.rs

1//! Real-file passthrough (root and nested directories).
2
3use fuser::{
4    FileType, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry,
5    ReplyWrite, TimeOrNow,
6};
7use libc::{EEXIST, EINVAL, EIO, EISDIR, ENOENT, ENOSYS, ENOTDIR, ENOTEMPTY, EPERM};
8use std::ffi::OsStr;
9use std::fs;
10use std::os::unix::fs::MetadataExt;
11use std::time::SystemTime;
12use tracing::{info, warn};
13
14use super::{RagFs, TTL, reply_file_bytes};
15use crate::inode::{FIRST_REAL_INO, InodeKind, RAGFS_DIR_INO, ROOT_INO};
16
17impl RagFs {
18    pub(crate) fn lookup_root(&mut self, name_str: &str, reply: ReplyEntry) {
19        if name_str == ".ragfs" {
20            let attr = self.make_attr(RAGFS_DIR_INO, FileType::Directory, 0);
21            reply.entry(&TTL, &attr, 0);
22            return;
23        }
24
25        let real_path = self.source.join(name_str);
26        if real_path.exists() {
27            let metadata = if let Ok(m) = fs::metadata(&real_path) {
28                m
29            } else {
30                reply.error(ENOENT);
31                return;
32            };
33
34            let mut inodes = self.runtime.block_on(self.inodes.write());
35            let ino = inodes.get_or_create_real(real_path.clone(), metadata.ino());
36            drop(inodes);
37
38            if let Some(attr) = self.real_path_to_attr(&real_path, ino) {
39                reply.entry(&TTL, &attr, 0);
40                return;
41            }
42        }
43
44        reply.error(ENOENT);
45    }
46
47    pub(crate) fn lookup_passthrough(&mut self, parent: u64, name_str: &str, reply: ReplyEntry) {
48        let inodes = self.runtime.block_on(self.inodes.read());
49        if let Some(entry) = inodes.get(parent)
50            && let InodeKind::Real { path, .. } = &entry.kind
51        {
52            let real_path = path.join(name_str);
53            if real_path.exists() {
54                drop(inodes);
55                let metadata = if let Ok(m) = fs::metadata(&real_path) {
56                    m
57                } else {
58                    reply.error(ENOENT);
59                    return;
60                };
61
62                let mut inodes = self.runtime.block_on(self.inodes.write());
63                let ino = inodes.get_or_create_real(real_path.clone(), metadata.ino());
64                drop(inodes);
65
66                if let Some(attr) = self.real_path_to_attr(&real_path, ino) {
67                    reply.entry(&TTL, &attr, 0);
68                    return;
69                }
70            }
71        }
72
73        reply.error(ENOENT);
74    }
75
76    pub(crate) fn getattr_cached_or_passthrough(&mut self, ino: u64, reply: ReplyAttr) {
77        let cache = self.runtime.block_on(self.content_cache.read());
78        if let Some(content) = cache.get(&ino) {
79            let attr = self.make_attr(ino, FileType::RegularFile, content.len() as u64);
80            reply.attr(&TTL, &attr);
81            return;
82        }
83        drop(cache);
84
85        let inodes = self.runtime.block_on(self.inodes.read());
86        if let Some(entry) = inodes.get(ino)
87            && let InodeKind::Real { path, .. } = &entry.kind
88            && let Some(attr) = self.real_path_to_attr(path, ino)
89        {
90            reply.attr(&TTL, &attr);
91            return;
92        }
93        reply.error(ENOENT);
94    }
95
96    pub(crate) fn read_passthrough(&mut self, ino: u64, offset: i64, size: u32, reply: ReplyData) {
97        let inodes = self.runtime.block_on(self.inodes.read());
98        if let Some(entry) = inodes.get(ino)
99            && let InodeKind::Real { path, .. } = &entry.kind
100        {
101            let path = path.clone();
102            drop(inodes);
103
104            match fs::read(&path) {
105                Ok(content) => {
106                    reply_file_bytes(reply, &content, offset, size);
107                    return;
108                }
109                Err(e) => {
110                    warn!("Failed to read file {:?}: {}", path, e);
111                    reply.error(EIO);
112                    return;
113                }
114            }
115        }
116
117        reply.error(ENOENT);
118    }
119
120    pub(crate) fn readdir_root(&mut self, offset: i64, mut reply: ReplyDirectory) {
121        let mut entries = vec![
122            (ROOT_INO, FileType::Directory, ".".to_string()),
123            (ROOT_INO, FileType::Directory, "..".to_string()),
124            (RAGFS_DIR_INO, FileType::Directory, ".ragfs".to_string()),
125        ];
126
127        if let Ok(read_dir) = fs::read_dir(&self.source) {
128            for entry in read_dir.flatten() {
129                let name = entry.file_name().to_string_lossy().to_string();
130                if name.starts_with('.') {
131                    continue;
132                }
133                let file_type = if entry.path().is_dir() {
134                    FileType::Directory
135                } else {
136                    FileType::RegularFile
137                };
138
139                let metadata = match entry.metadata() {
140                    Ok(m) => m,
141                    Err(_) => continue,
142                };
143
144                let mut inodes = self.runtime.block_on(self.inodes.write());
145                let entry_ino = inodes.get_or_create_real(entry.path(), metadata.ino());
146                entries.push((entry_ino, file_type, name));
147            }
148        }
149
150        for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
151            if reply.add(*ino, (i + 1) as i64, *kind, name) {
152                break;
153            }
154        }
155        reply.ok();
156    }
157
158    pub(crate) fn readdir_passthrough(&mut self, ino: u64, offset: i64, mut reply: ReplyDirectory) {
159        let inodes = self.runtime.block_on(self.inodes.read());
160        if let Some(entry) = inodes.get(ino)
161            && let InodeKind::Real { path, .. } = &entry.kind
162        {
163            let path = path.clone();
164            let parent_ino = entry.parent;
165            drop(inodes);
166
167            if path.is_dir() {
168                let mut entries = vec![
169                    (ino, FileType::Directory, ".".to_string()),
170                    (parent_ino, FileType::Directory, "..".to_string()),
171                ];
172
173                if let Ok(read_dir) = fs::read_dir(&path) {
174                    for dir_entry in read_dir.flatten() {
175                        let name = dir_entry.file_name().to_string_lossy().to_string();
176                        if name.starts_with('.') {
177                            continue;
178                        }
179                        let file_type = if dir_entry.path().is_dir() {
180                            FileType::Directory
181                        } else {
182                            FileType::RegularFile
183                        };
184
185                        let metadata = match dir_entry.metadata() {
186                            Ok(m) => m,
187                            Err(_) => continue,
188                        };
189
190                        let mut inodes = self.runtime.block_on(self.inodes.write());
191                        let entry_ino = inodes.get_or_create_real(dir_entry.path(), metadata.ino());
192                        entries.push((entry_ino, file_type, name));
193                    }
194                }
195
196                for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset as usize)
197                {
198                    if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
199                        break;
200                    }
201                }
202                reply.ok();
203                return;
204            }
205        }
206        reply.error(ENOENT);
207    }
208
209    pub(crate) fn write_passthrough(&mut self, ino: u64, data: &[u8], reply: ReplyWrite) {
210        let inodes = self.runtime.block_on(self.inodes.read());
211        if let Some(entry) = inodes.get(ino)
212            && let InodeKind::Real { path, .. } = &entry.kind
213        {
214            let path = path.clone();
215            drop(inodes);
216
217            match fs::write(&path, data) {
218                Ok(()) => {
219                    reply.written(data.len() as u32);
220                    return;
221                }
222                Err(e) => {
223                    warn!("Failed to write file {:?}: {}", path, e);
224                    reply.error(EIO);
225                    return;
226                }
227            }
228        }
229
230        reply.error(ENOSYS);
231    }
232
233    pub(crate) fn create_passthrough(
234        &mut self,
235        parent: u64,
236        name: &OsStr,
237        mode: u32,
238        _umask: u32,
239        flags: i32,
240        reply: ReplyCreate,
241    ) {
242        let name_str = name.to_string_lossy();
243        tracing::debug!(
244            "create: parent={}, name={}, mode={:o}",
245            parent,
246            name_str,
247            mode
248        );
249
250        if parent < FIRST_REAL_INO && parent != ROOT_INO {
251            reply.error(EPERM);
252            return;
253        }
254
255        let Some(parent_path) = self.resolve_parent_path(parent) else {
256            reply.error(ENOENT);
257            return;
258        };
259
260        let new_path = parent_path.join(&*name_str);
261
262        if new_path.exists() {
263            reply.error(EEXIST);
264            return;
265        }
266
267        match fs::File::create(&new_path) {
268            Ok(_) => {
269                let metadata = match fs::metadata(&new_path) {
270                    Ok(m) => m,
271                    Err(e) => {
272                        warn!("Failed to get metadata for new file {:?}: {}", new_path, e);
273                        reply.error(EIO);
274                        return;
275                    }
276                };
277
278                let mut inodes = self.runtime.block_on(self.inodes.write());
279                let ino = inodes.get_or_create_real(new_path.clone(), metadata.ino());
280                drop(inodes);
281
282                if let Some(ref sender) = self.reindex_sender {
283                    let sender = sender.clone();
284                    let path_to_send = new_path.clone();
285                    self.runtime.spawn(async move {
286                        if let Err(e) = sender.send(path_to_send).await {
287                            warn!("Failed to send reindex request: {}", e);
288                        }
289                    });
290                }
291
292                if let Some(attr) = self.real_path_to_attr(&new_path, ino) {
293                    reply.created(&TTL, &attr, 0, 0, flags as u32);
294                } else {
295                    reply.error(EIO);
296                }
297            }
298            Err(e) => {
299                warn!("Failed to create file {:?}: {}", new_path, e);
300                reply.error(EIO);
301            }
302        }
303    }
304
305    pub(crate) fn unlink_passthrough(&mut self, parent: u64, name: &OsStr, reply: ReplyEmpty) {
306        let name_str = name.to_string_lossy();
307        tracing::debug!("unlink: parent={}, name={}", parent, name_str);
308
309        if parent < FIRST_REAL_INO && parent != ROOT_INO {
310            reply.error(EPERM);
311            return;
312        }
313
314        let Some(parent_path) = self.resolve_parent_path(parent) else {
315            reply.error(ENOENT);
316            return;
317        };
318
319        let file_path = parent_path.join(&*name_str);
320
321        if !file_path.exists() {
322            reply.error(ENOENT);
323            return;
324        }
325
326        if file_path.is_dir() {
327            reply.error(EISDIR);
328            return;
329        }
330
331        if let Some(ref store) = self.store {
332            let store = store.clone();
333            let path_for_delete = file_path.clone();
334            let result = self
335                .runtime
336                .block_on(async move { store.delete_by_file_path(&path_for_delete).await });
337            if let Err(e) = result {
338                warn!("Failed to delete from store {:?}: {}", file_path, e);
339            }
340        }
341
342        match fs::remove_file(&file_path) {
343            Ok(()) => {
344                let mut inodes = self.runtime.block_on(self.inodes.write());
345                if let Some(ino) = inodes.get_by_path(&file_path) {
346                    inodes.remove(ino);
347                }
348                info!("Deleted file: {:?}", file_path);
349                reply.ok();
350            }
351            Err(e) => {
352                warn!("Failed to delete file {:?}: {}", file_path, e);
353                reply.error(EIO);
354            }
355        }
356    }
357
358    pub(crate) fn mkdir_passthrough(
359        &mut self,
360        parent: u64,
361        name: &OsStr,
362        mode: u32,
363        _umask: u32,
364        reply: ReplyEntry,
365    ) {
366        let name_str = name.to_string_lossy();
367        tracing::debug!(
368            "mkdir: parent={}, name={}, mode={:o}",
369            parent,
370            name_str,
371            mode
372        );
373
374        if parent < FIRST_REAL_INO && parent != ROOT_INO {
375            reply.error(EPERM);
376            return;
377        }
378
379        let Some(parent_path) = self.resolve_parent_path(parent) else {
380            reply.error(ENOENT);
381            return;
382        };
383
384        let new_path = parent_path.join(&*name_str);
385
386        if new_path.exists() {
387            reply.error(EEXIST);
388            return;
389        }
390
391        match fs::create_dir(&new_path) {
392            Ok(()) => {
393                let metadata = match fs::metadata(&new_path) {
394                    Ok(m) => m,
395                    Err(e) => {
396                        warn!("Failed to get metadata for new dir {:?}: {}", new_path, e);
397                        reply.error(EIO);
398                        return;
399                    }
400                };
401
402                let mut inodes = self.runtime.block_on(self.inodes.write());
403                let ino = inodes.get_or_create_real(new_path.clone(), metadata.ino());
404                drop(inodes);
405
406                if let Some(attr) = self.real_path_to_attr(&new_path, ino) {
407                    info!("Created directory: {:?}", new_path);
408                    reply.entry(&TTL, &attr, 0);
409                } else {
410                    reply.error(EIO);
411                }
412            }
413            Err(e) => {
414                warn!("Failed to create directory {:?}: {}", new_path, e);
415                reply.error(EIO);
416            }
417        }
418    }
419
420    pub(crate) fn rmdir_passthrough(&mut self, parent: u64, name: &OsStr, reply: ReplyEmpty) {
421        let name_str = name.to_string_lossy();
422        tracing::debug!("rmdir: parent={}, name={}", parent, name_str);
423
424        if parent < FIRST_REAL_INO && parent != ROOT_INO {
425            reply.error(EPERM);
426            return;
427        }
428
429        let Some(parent_path) = self.resolve_parent_path(parent) else {
430            reply.error(ENOENT);
431            return;
432        };
433
434        let dir_path = parent_path.join(&*name_str);
435
436        if !dir_path.exists() {
437            reply.error(ENOENT);
438            return;
439        }
440
441        if !dir_path.is_dir() {
442            reply.error(ENOTDIR);
443            return;
444        }
445
446        match fs::remove_dir(&dir_path) {
447            Ok(()) => {
448                let mut inodes = self.runtime.block_on(self.inodes.write());
449                if let Some(ino) = inodes.get_by_path(&dir_path) {
450                    inodes.remove(ino);
451                }
452                info!("Removed directory: {:?}", dir_path);
453                reply.ok();
454            }
455            Err(e) => {
456                if e.kind() == std::io::ErrorKind::DirectoryNotEmpty
457                    || e.raw_os_error() == Some(libc::ENOTEMPTY)
458                {
459                    reply.error(ENOTEMPTY);
460                } else {
461                    warn!("Failed to remove directory {:?}: {}", dir_path, e);
462                    reply.error(EIO);
463                }
464            }
465        }
466    }
467
468    pub(crate) fn rename_passthrough(
469        &mut self,
470        parent: u64,
471        name: &OsStr,
472        newparent: u64,
473        newname: &OsStr,
474        _flags: u32,
475        reply: ReplyEmpty,
476    ) {
477        let name_str = name.to_string_lossy();
478        let newname_str = newname.to_string_lossy();
479        tracing::debug!(
480            "rename: parent={}, name={}, newparent={}, newname={}",
481            parent,
482            name_str,
483            newparent,
484            newname_str
485        );
486
487        if (parent < FIRST_REAL_INO && parent != ROOT_INO)
488            || (newparent < FIRST_REAL_INO && newparent != ROOT_INO)
489        {
490            reply.error(EPERM);
491            return;
492        }
493
494        let Some(src_parent_path) = self.resolve_parent_path(parent) else {
495            reply.error(ENOENT);
496            return;
497        };
498
499        let Some(dst_parent_path) = self.resolve_parent_path(newparent) else {
500            reply.error(ENOENT);
501            return;
502        };
503
504        let src_path = src_parent_path.join(&*name_str);
505        let dst_path = dst_parent_path.join(&*newname_str);
506
507        if !src_path.exists() {
508            reply.error(ENOENT);
509            return;
510        }
511
512        match fs::rename(&src_path, &dst_path) {
513            Ok(()) => {
514                if let Some(ref store) = self.store {
515                    let store = store.clone();
516                    let src = src_path.clone();
517                    let dst = dst_path.clone();
518                    let result = self
519                        .runtime
520                        .block_on(async move { store.update_file_path(&src, &dst).await });
521                    if let Err(e) = result {
522                        warn!(
523                            "Failed to update store path {:?} -> {:?}: {}",
524                            src_path, dst_path, e
525                        );
526                    }
527                }
528
529                let mut inodes = self.runtime.block_on(self.inodes.write());
530                if let Some(ino) = inodes.get_by_path(&src_path) {
531                    inodes.update_path(ino, dst_path.clone());
532                }
533                drop(inodes);
534
535                info!("Renamed: {:?} -> {:?}", src_path, dst_path);
536                reply.ok();
537            }
538            Err(e) => {
539                warn!("Failed to rename {:?} -> {:?}: {}", src_path, dst_path, e);
540                reply.error(EIO);
541            }
542        }
543    }
544
545    pub(crate) fn setattr_passthrough(
546        &mut self,
547        ino: u64,
548        _mode: Option<u32>,
549        _uid: Option<u32>,
550        _gid: Option<u32>,
551        size: Option<u64>,
552        _atime: Option<TimeOrNow>,
553        _mtime: Option<TimeOrNow>,
554        _ctime: Option<SystemTime>,
555        _fh: Option<u64>,
556        _crtime: Option<SystemTime>,
557        _chgtime: Option<SystemTime>,
558        _bkuptime: Option<SystemTime>,
559        _flags: Option<u32>,
560        reply: ReplyAttr,
561    ) {
562        tracing::debug!("setattr: ino={}, size={:?}", ino, size);
563
564        if ino < FIRST_REAL_INO {
565            reply.error(EPERM);
566            return;
567        }
568
569        let inodes = self.runtime.block_on(self.inodes.read());
570        let Some(entry) = inodes.get(ino) else {
571            drop(inodes);
572            reply.error(ENOENT);
573            return;
574        };
575        let InodeKind::Real { path, .. } = &entry.kind else {
576            drop(inodes);
577            reply.error(EINVAL);
578            return;
579        };
580        let path = path.clone();
581        drop(inodes);
582
583        if let Some(new_size) = size {
584            match fs::OpenOptions::new().write(true).open(&path) {
585                Ok(file) => {
586                    if let Err(e) = file.set_len(new_size) {
587                        warn!("Failed to truncate {:?}: {}", path, e);
588                        reply.error(EIO);
589                        return;
590                    }
591
592                    if let Some(ref sender) = self.reindex_sender {
593                        let sender = sender.clone();
594                        let path_to_send = path.clone();
595                        self.runtime.spawn(async move {
596                            if let Err(e) = sender.send(path_to_send).await {
597                                warn!("Failed to send reindex request: {}", e);
598                            }
599                        });
600                    }
601                }
602                Err(e) => {
603                    warn!("Failed to open {:?} for truncate: {}", path, e);
604                    reply.error(EIO);
605                    return;
606                }
607            }
608        }
609
610        if let Some(attr) = self.real_path_to_attr(&path, ino) {
611            reply.attr(&TTL, &attr);
612        } else {
613            reply.error(EIO);
614        }
615    }
616}