1use 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}