1use fuser::{
4 FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty,
5 ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow,
6};
7use libc::{EEXIST, EINVAL, EIO, EISDIR, ENOENT, ENOSYS, ENOTDIR, ENOTEMPTY, EPERM};
8use ragfs_core::{Embedder, VectorStore};
9use ragfs_query::QueryExecutor;
10use std::collections::HashMap;
11use std::ffi::OsStr;
12use std::fs;
13use std::os::unix::fs::MetadataExt;
14use std::path::PathBuf;
15use std::sync::Arc;
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
17use tokio::runtime::Handle;
18use tokio::sync::{RwLock, mpsc};
19use tracing::{debug, info, warn};
20
21use crate::inode::{
22 APPROVE_FILE_INO, CLEANUP_FILE_INO, CONFIG_FILE_INO, DEDUPE_FILE_INO, FIRST_REAL_INO,
23 HELP_FILE_INO, HISTORY_FILE_INO, INDEX_FILE_INO, InodeKind, InodeTable, OPS_BATCH_INO,
24 OPS_CREATE_INO, OPS_DELETE_INO, OPS_DIR_INO, OPS_MOVE_INO, OPS_RESULT_INO, ORGANIZE_FILE_INO,
25 PENDING_DIR_INO, QUERY_DIR_INO, RAGFS_DIR_INO, REINDEX_FILE_INO, REJECT_FILE_INO, ROOT_INO,
26 SAFETY_DIR_INO, SEARCH_DIR_INO, SEMANTIC_DIR_INO, SIMILAR_DIR_INO, SIMILAR_OPS_FILE_INO,
27 TRASH_DIR_INO, UNDO_FILE_INO,
28};
29use crate::ops::OpsManager;
30use crate::safety::SafetyManager;
31use crate::semantic::SemanticManager;
32
33const TTL: Duration = Duration::from_secs(1);
34const BLOCK_SIZE: u64 = 512;
35
36pub struct RagFs {
38 source: PathBuf,
40 inodes: Arc<RwLock<InodeTable>>,
42 store: Option<Arc<dyn VectorStore>>,
44 query_executor: Option<Arc<QueryExecutor>>,
46 runtime: Handle,
48 content_cache: Arc<RwLock<HashMap<u64, Vec<u8>>>>,
50 reindex_sender: Option<mpsc::Sender<PathBuf>>,
52 ops_manager: Arc<OpsManager>,
54 safety_manager: Arc<SafetyManager>,
56 semantic_manager: Arc<SemanticManager>,
58}
59
60impl RagFs {
61 #[must_use]
63 pub fn new(source: PathBuf) -> Self {
64 let safety_manager = Arc::new(SafetyManager::new(&source, None));
65 let ops_manager = Arc::new(OpsManager::with_safety(
66 source.clone(),
67 None,
68 None,
69 safety_manager.clone(),
70 ));
71 let semantic_manager = Arc::new(SemanticManager::with_ops(
72 source.clone(),
73 None,
74 None,
75 None,
76 ops_manager.clone(),
77 ));
78 Self {
79 source,
80 inodes: Arc::new(RwLock::new(InodeTable::new())),
81 store: None,
82 query_executor: None,
83 runtime: Handle::current(),
84 content_cache: Arc::new(RwLock::new(HashMap::new())),
85 reindex_sender: None,
86 ops_manager,
87 safety_manager,
88 semantic_manager,
89 }
90 }
91
92 pub fn with_rag(
94 source: PathBuf,
95 store: Arc<dyn VectorStore>,
96 embedder: Arc<dyn Embedder>,
97 runtime: Handle,
98 reindex_sender: Option<mpsc::Sender<PathBuf>>,
99 ) -> Self {
100 let query_executor = Arc::new(QueryExecutor::new(
101 store.clone(),
102 embedder.clone(),
103 10, false, ));
106
107 let safety_manager = Arc::new(SafetyManager::new(&source, None));
108 let ops_manager = Arc::new(OpsManager::with_safety(
109 source.clone(),
110 Some(store.clone()),
111 reindex_sender.clone(),
112 safety_manager.clone(),
113 ));
114 let semantic_manager = Arc::new(SemanticManager::with_ops(
115 source.clone(),
116 Some(store.clone()),
117 Some(embedder.clone()),
118 None,
119 ops_manager.clone(),
120 ));
121
122 Self {
123 source,
124 inodes: Arc::new(RwLock::new(InodeTable::new())),
125 store: Some(store),
126 query_executor: Some(query_executor),
127 runtime,
128 content_cache: Arc::new(RwLock::new(HashMap::new())),
129 reindex_sender,
130 ops_manager,
131 safety_manager,
132 semantic_manager,
133 }
134 }
135
136 #[must_use]
138 pub fn source(&self) -> &PathBuf {
139 &self.source
140 }
141
142 fn real_path_to_attr(&self, path: &PathBuf, ino: u64) -> Option<FileAttr> {
144 let metadata = fs::metadata(path).ok()?;
145 let kind = if metadata.is_dir() {
146 FileType::Directory
147 } else if metadata.is_file() {
148 FileType::RegularFile
149 } else if metadata.file_type().is_symlink() {
150 FileType::Symlink
151 } else {
152 return None;
153 };
154
155 let atime = metadata.accessed().unwrap_or(SystemTime::UNIX_EPOCH);
156 let mtime = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
157 let ctime = UNIX_EPOCH + Duration::from_secs(metadata.ctime() as u64);
158
159 Some(FileAttr {
160 ino,
161 size: metadata.len(),
162 blocks: metadata.len().div_ceil(BLOCK_SIZE),
163 atime,
164 mtime,
165 ctime,
166 crtime: ctime,
167 kind,
168 perm: (metadata.mode() & 0o7777) as u16,
169 nlink: metadata.nlink() as u32,
170 uid: metadata.uid(),
171 gid: metadata.gid(),
172 rdev: metadata.rdev() as u32,
173 blksize: BLOCK_SIZE as u32,
174 flags: 0,
175 })
176 }
177
178 #[allow(unsafe_code)]
179 fn make_attr(&self, ino: u64, kind: FileType, size: u64) -> FileAttr {
180 let now = SystemTime::now();
181 let uid = unsafe { libc::getuid() };
183 let gid = unsafe { libc::getgid() };
184 FileAttr {
185 ino,
186 size,
187 blocks: size.div_ceil(BLOCK_SIZE),
188 atime: now,
189 mtime: now,
190 ctime: now,
191 crtime: now,
192 kind,
193 perm: if kind == FileType::Directory {
194 0o755
195 } else {
196 0o644
197 },
198 nlink: if kind == FileType::Directory { 2 } else { 1 },
199 uid,
200 gid,
201 rdev: 0,
202 blksize: BLOCK_SIZE as u32,
203 flags: 0,
204 }
205 }
206
207 fn get_index_status(&self) -> Vec<u8> {
209 if let Some(ref store) = self.store {
210 let store = store.clone();
211 let result = self.runtime.block_on(async { store.stats().await });
212
213 match result {
214 Ok(stats) => {
215 let json = serde_json::json!({
216 "status": "indexed",
217 "total_files": stats.total_files,
218 "total_chunks": stats.total_chunks,
219 "index_size_bytes": stats.index_size_bytes,
220 "last_updated": stats.last_updated.map(|t| t.to_rfc3339()),
221 });
222 serde_json::to_string_pretty(&json)
223 .unwrap_or_default()
224 .into_bytes()
225 }
226 Err(e) => {
227 let json = serde_json::json!({
228 "status": "error",
229 "error": e.to_string(),
230 });
231 serde_json::to_string_pretty(&json)
232 .unwrap_or_default()
233 .into_bytes()
234 }
235 }
236 } else {
237 let json = serde_json::json!({
238 "status": "not_initialized",
239 "message": "No store configured",
240 });
241 serde_json::to_string_pretty(&json)
242 .unwrap_or_default()
243 .into_bytes()
244 }
245 }
246
247 fn execute_query(&self, query: &str) -> Vec<u8> {
249 if let Some(ref executor) = self.query_executor {
250 let executor = executor.clone();
251 let query_str = query.to_string();
252 let query_for_result = query_str.clone();
253 let result = self
254 .runtime
255 .block_on(async move { executor.execute(&query_str).await });
256
257 match result {
258 Ok(results) => {
259 let json_results: Vec<_> = results
260 .iter()
261 .map(|r| {
262 serde_json::json!({
263 "file": r.file_path.to_string_lossy(),
264 "score": r.score,
265 "content": truncate(&r.content, 500),
266 "byte_range": [r.byte_range.start, r.byte_range.end],
267 "line_range": r.line_range.as_ref().map(|lr| [lr.start, lr.end]),
268 })
269 })
270 .collect();
271
272 let json = serde_json::json!({
273 "query": query_for_result,
274 "results": json_results,
275 });
276 serde_json::to_string_pretty(&json)
277 .unwrap_or_default()
278 .into_bytes()
279 }
280 Err(e) => {
281 let json = serde_json::json!({
282 "query": query_for_result,
283 "error": e.to_string(),
284 });
285 serde_json::to_string_pretty(&json)
286 .unwrap_or_default()
287 .into_bytes()
288 }
289 }
290 } else {
291 let json = serde_json::json!({
292 "error": "Query executor not configured",
293 });
294 serde_json::to_string_pretty(&json)
295 .unwrap_or_default()
296 .into_bytes()
297 }
298 }
299
300 fn get_config(&self) -> Vec<u8> {
302 let json = serde_json::json!({
303 "source": self.source.to_string_lossy(),
304 "store_configured": self.store.is_some(),
305 "query_executor_configured": self.query_executor.is_some(),
306 });
307 serde_json::to_string_pretty(&json)
308 .unwrap_or_default()
309 .into_bytes()
310 }
311
312 fn resolve_parent_path(&self, parent: u64) -> Option<PathBuf> {
315 if parent == ROOT_INO {
316 return Some(self.source.clone());
317 }
318
319 let inodes = self.runtime.block_on(self.inodes.read());
320 if let Some(entry) = inodes.get(parent)
321 && let InodeKind::Real { path, .. } = &entry.kind
322 {
323 Some(path.clone())
324 } else {
325 None
326 }
327 }
328
329 fn get_help_content(&self) -> Vec<u8> {
331 r#"RAGFS Virtual Control Directory
332================================
333
334The .ragfs directory provides a virtual interface to RAGFS functionality.
335
336Available paths:
337
338 .index Read to get index statistics (JSON)
339 Shows file count, chunk count, and last update time.
340
341 .config Read to get current configuration (JSON)
342 Shows source directory and component status.
343
344 .reindex Write a file path to trigger reindexing.
345 Example: echo "src/main.rs" > .ragfs/.reindex
346
347 .query/<q> Read to execute a semantic search query.
348 The filename is the query string.
349 Returns JSON with matching results.
350 Example: cat .ragfs/.query/authentication
351
352 .search/ Directory for search results (symlinks).
353 Access .search/<query>/ to get symlinks to matching files.
354
355 .similar/ Directory for finding similar files.
356 Access .similar/<path>/ to get symlinks to similar files.
357
358 .help This help file.
359
360Examples:
361
362 # Check index status
363 cat .ragfs/.index
364
365 # Search for files about authentication
366 cat ".ragfs/.query/how to authenticate users"
367
368 # Trigger reindex of a specific file
369 echo "src/lib.rs" > .ragfs/.reindex
370
371 # View configuration
372 cat .ragfs/.config
373"#
374 .as_bytes()
375 .to_vec()
376 }
377}
378
379impl Filesystem for RagFs {
380 fn init(
381 &mut self,
382 _req: &Request<'_>,
383 _config: &mut fuser::KernelConfig,
384 ) -> Result<(), libc::c_int> {
385 debug!("FUSE init for source: {:?}", self.source);
386 Ok(())
387 }
388
389 fn destroy(&mut self) {
390 debug!("FUSE destroy");
391 }
392
393 fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
394 let name_str = name.to_string_lossy();
395 debug!("lookup: parent={}, name={}", parent, name_str);
396
397 if parent == ROOT_INO {
399 if name_str == ".ragfs" {
400 let attr = self.make_attr(RAGFS_DIR_INO, FileType::Directory, 0);
401 reply.entry(&TTL, &attr, 0);
402 return;
403 }
404
405 let real_path = self.source.join(&*name_str);
407 if real_path.exists() {
408 let metadata = if let Ok(m) = fs::metadata(&real_path) {
409 m
410 } else {
411 reply.error(ENOENT);
412 return;
413 };
414
415 let mut inodes = self.runtime.block_on(self.inodes.write());
416 let ino = inodes.get_or_create_real(real_path.clone(), metadata.ino());
417 drop(inodes);
418
419 if let Some(attr) = self.real_path_to_attr(&real_path, ino) {
420 reply.entry(&TTL, &attr, 0);
421 return;
422 }
423 }
424 }
425
426 if parent == RAGFS_DIR_INO {
428 match name_str.as_ref() {
429 ".query" => {
430 let attr = self.make_attr(QUERY_DIR_INO, FileType::Directory, 0);
431 reply.entry(&TTL, &attr, 0);
432 return;
433 }
434 ".search" => {
435 let attr = self.make_attr(SEARCH_DIR_INO, FileType::Directory, 0);
436 reply.entry(&TTL, &attr, 0);
437 return;
438 }
439 ".index" => {
440 let content = self.get_index_status();
441 let attr =
442 self.make_attr(INDEX_FILE_INO, FileType::RegularFile, content.len() as u64);
443 let mut cache = self.runtime.block_on(self.content_cache.write());
444 cache.insert(INDEX_FILE_INO, content);
445 reply.entry(&TTL, &attr, 0);
446 return;
447 }
448 ".config" => {
449 let content = self.get_config();
450 let attr = self.make_attr(
451 CONFIG_FILE_INO,
452 FileType::RegularFile,
453 content.len() as u64,
454 );
455 let mut cache = self.runtime.block_on(self.content_cache.write());
456 cache.insert(CONFIG_FILE_INO, content);
457 reply.entry(&TTL, &attr, 0);
458 return;
459 }
460 ".reindex" => {
461 let attr = self.make_attr(REINDEX_FILE_INO, FileType::RegularFile, 0);
462 reply.entry(&TTL, &attr, 0);
463 return;
464 }
465 ".similar" => {
466 let attr = self.make_attr(SIMILAR_DIR_INO, FileType::Directory, 0);
467 reply.entry(&TTL, &attr, 0);
468 return;
469 }
470 ".help" => {
471 let content = self.get_help_content();
472 let attr =
473 self.make_attr(HELP_FILE_INO, FileType::RegularFile, content.len() as u64);
474 let mut cache = self.runtime.block_on(self.content_cache.write());
475 cache.insert(HELP_FILE_INO, content);
476 reply.entry(&TTL, &attr, 0);
477 return;
478 }
479 ".ops" => {
480 let attr = self.make_attr(OPS_DIR_INO, FileType::Directory, 0);
481 reply.entry(&TTL, &attr, 0);
482 return;
483 }
484 ".safety" => {
485 let attr = self.make_attr(SAFETY_DIR_INO, FileType::Directory, 0);
486 reply.entry(&TTL, &attr, 0);
487 return;
488 }
489 ".semantic" => {
490 let attr = self.make_attr(SEMANTIC_DIR_INO, FileType::Directory, 0);
491 reply.entry(&TTL, &attr, 0);
492 return;
493 }
494 _ => {}
495 }
496 }
497
498 if parent == SEMANTIC_DIR_INO {
500 match name_str.as_ref() {
501 ".organize" => {
502 let attr = self.make_attr(ORGANIZE_FILE_INO, FileType::RegularFile, 0);
503 reply.entry(&TTL, &attr, 0);
504 return;
505 }
506 ".similar" => {
507 let content = self
508 .runtime
509 .block_on(self.semantic_manager.get_similar_json());
510 let attr = self.make_attr(
511 SIMILAR_OPS_FILE_INO,
512 FileType::RegularFile,
513 content.len() as u64,
514 );
515 let mut cache = self.runtime.block_on(self.content_cache.write());
516 cache.insert(SIMILAR_OPS_FILE_INO, content);
517 reply.entry(&TTL, &attr, 0);
518 return;
519 }
520 ".cleanup" => {
521 let content = self
522 .runtime
523 .block_on(self.semantic_manager.get_cleanup_json());
524 let attr = self.make_attr(
525 CLEANUP_FILE_INO,
526 FileType::RegularFile,
527 content.len() as u64,
528 );
529 let mut cache = self.runtime.block_on(self.content_cache.write());
530 cache.insert(CLEANUP_FILE_INO, content);
531 reply.entry(&TTL, &attr, 0);
532 return;
533 }
534 ".dedupe" => {
535 let content = self
536 .runtime
537 .block_on(self.semantic_manager.get_dedupe_json());
538 let attr = self.make_attr(
539 DEDUPE_FILE_INO,
540 FileType::RegularFile,
541 content.len() as u64,
542 );
543 let mut cache = self.runtime.block_on(self.content_cache.write());
544 cache.insert(DEDUPE_FILE_INO, content);
545 reply.entry(&TTL, &attr, 0);
546 return;
547 }
548 ".pending" => {
549 let attr = self.make_attr(PENDING_DIR_INO, FileType::Directory, 0);
550 reply.entry(&TTL, &attr, 0);
551 return;
552 }
553 ".approve" => {
554 let attr = self.make_attr(APPROVE_FILE_INO, FileType::RegularFile, 0);
555 reply.entry(&TTL, &attr, 0);
556 return;
557 }
558 ".reject" => {
559 let attr = self.make_attr(REJECT_FILE_INO, FileType::RegularFile, 0);
560 reply.entry(&TTL, &attr, 0);
561 return;
562 }
563 _ => {}
564 }
565 }
566
567 if parent == PENDING_DIR_INO {
569 let plan_ids = self
571 .runtime
572 .block_on(self.semantic_manager.get_pending_plan_ids());
573 if plan_ids.contains(&name_str.to_string()) {
574 let content = self
575 .runtime
576 .block_on(self.semantic_manager.get_plan_json(&name_str));
577 let mut inodes = self.runtime.block_on(self.inodes.write());
579 let ino = inodes.get_or_create_query_result(PENDING_DIR_INO, name_str.to_string());
580 let attr = self.make_attr(ino, FileType::RegularFile, content.len() as u64);
581 let mut cache = self.runtime.block_on(self.content_cache.write());
582 cache.insert(ino, content);
583 reply.entry(&TTL, &attr, 0);
584 return;
585 }
586 }
587
588 if parent == SAFETY_DIR_INO {
590 match name_str.as_ref() {
591 ".trash" => {
592 let attr = self.make_attr(TRASH_DIR_INO, FileType::Directory, 0);
593 reply.entry(&TTL, &attr, 0);
594 return;
595 }
596 ".history" => {
597 let content = self.safety_manager.get_history_json(Some(100));
598 let attr = self.make_attr(
599 HISTORY_FILE_INO,
600 FileType::RegularFile,
601 content.len() as u64,
602 );
603 let mut cache = self.runtime.block_on(self.content_cache.write());
604 cache.insert(HISTORY_FILE_INO, content);
605 reply.entry(&TTL, &attr, 0);
606 return;
607 }
608 ".undo" => {
609 let attr = self.make_attr(UNDO_FILE_INO, FileType::RegularFile, 0);
610 reply.entry(&TTL, &attr, 0);
611 return;
612 }
613 _ => {}
614 }
615 }
616
617 if parent == OPS_DIR_INO {
619 match name_str.as_ref() {
620 ".create" => {
621 let attr = self.make_attr(OPS_CREATE_INO, FileType::RegularFile, 0);
622 reply.entry(&TTL, &attr, 0);
623 return;
624 }
625 ".delete" => {
626 let attr = self.make_attr(OPS_DELETE_INO, FileType::RegularFile, 0);
627 reply.entry(&TTL, &attr, 0);
628 return;
629 }
630 ".move" => {
631 let attr = self.make_attr(OPS_MOVE_INO, FileType::RegularFile, 0);
632 reply.entry(&TTL, &attr, 0);
633 return;
634 }
635 ".batch" => {
636 let attr = self.make_attr(OPS_BATCH_INO, FileType::RegularFile, 0);
637 reply.entry(&TTL, &attr, 0);
638 return;
639 }
640 ".result" => {
641 let content = self.runtime.block_on(self.ops_manager.get_last_result());
642 let attr =
643 self.make_attr(OPS_RESULT_INO, FileType::RegularFile, content.len() as u64);
644 let mut cache = self.runtime.block_on(self.content_cache.write());
645 cache.insert(OPS_RESULT_INO, content);
646 reply.entry(&TTL, &attr, 0);
647 return;
648 }
649 _ => {}
650 }
651 }
652
653 if parent == QUERY_DIR_INO {
655 let query = name_str.to_string();
656 let content = self.execute_query(&query);
657
658 let mut inodes = self.runtime.block_on(self.inodes.write());
659 let ino = inodes.get_or_create_query_result(QUERY_DIR_INO, query);
660 drop(inodes);
661
662 let attr = self.make_attr(ino, FileType::RegularFile, content.len() as u64);
663 let mut cache = self.runtime.block_on(self.content_cache.write());
664 cache.insert(ino, content);
665
666 reply.entry(&TTL, &attr, 0);
667 return;
668 }
669
670 let inodes = self.runtime.block_on(self.inodes.read());
672 if let Some(entry) = inodes.get(parent)
673 && let InodeKind::Real { path, .. } = &entry.kind
674 {
675 let real_path = path.join(&*name_str);
676 if real_path.exists() {
677 drop(inodes);
678 let metadata = if let Ok(m) = fs::metadata(&real_path) {
679 m
680 } else {
681 reply.error(ENOENT);
682 return;
683 };
684
685 let mut inodes = self.runtime.block_on(self.inodes.write());
686 let ino = inodes.get_or_create_real(real_path.clone(), metadata.ino());
687 drop(inodes);
688
689 if let Some(attr) = self.real_path_to_attr(&real_path, ino) {
690 reply.entry(&TTL, &attr, 0);
691 return;
692 }
693 }
694 }
695
696 reply.error(ENOENT);
697 }
698
699 fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
700 debug!("getattr: ino={}", ino);
701
702 match ino {
703 ROOT_INO => {
704 let attr = self.make_attr(ROOT_INO, FileType::Directory, 0);
705 reply.attr(&TTL, &attr);
706 }
707 RAGFS_DIR_INO => {
708 let attr = self.make_attr(RAGFS_DIR_INO, FileType::Directory, 0);
709 reply.attr(&TTL, &attr);
710 }
711 QUERY_DIR_INO | SEARCH_DIR_INO | SIMILAR_DIR_INO => {
712 let attr = self.make_attr(ino, FileType::Directory, 0);
713 reply.attr(&TTL, &attr);
714 }
715 INDEX_FILE_INO => {
716 let content = self.get_index_status();
717 let size = content.len() as u64;
718 let mut cache = self.runtime.block_on(self.content_cache.write());
719 cache.insert(INDEX_FILE_INO, content);
720 let attr = self.make_attr(ino, FileType::RegularFile, size);
721 reply.attr(&TTL, &attr);
722 }
723 CONFIG_FILE_INO => {
724 let content = self.get_config();
725 let size = content.len() as u64;
726 let mut cache = self.runtime.block_on(self.content_cache.write());
727 cache.insert(CONFIG_FILE_INO, content);
728 let attr = self.make_attr(ino, FileType::RegularFile, size);
729 reply.attr(&TTL, &attr);
730 }
731 REINDEX_FILE_INO => {
732 let attr = self.make_attr(ino, FileType::RegularFile, 0);
733 reply.attr(&TTL, &attr);
734 }
735 HELP_FILE_INO => {
736 let content = self.get_help_content();
737 let size = content.len() as u64;
738 let mut cache = self.runtime.block_on(self.content_cache.write());
739 cache.insert(HELP_FILE_INO, content);
740 let attr = self.make_attr(ino, FileType::RegularFile, size);
741 reply.attr(&TTL, &attr);
742 }
743 OPS_DIR_INO => {
745 let attr = self.make_attr(ino, FileType::Directory, 0);
746 reply.attr(&TTL, &attr);
747 }
748 OPS_CREATE_INO | OPS_DELETE_INO | OPS_MOVE_INO | OPS_BATCH_INO => {
749 let attr = self.make_attr(ino, FileType::RegularFile, 0);
751 reply.attr(&TTL, &attr);
752 }
753 OPS_RESULT_INO => {
754 let content = self.runtime.block_on(self.ops_manager.get_last_result());
755 let size = content.len() as u64;
756 let mut cache = self.runtime.block_on(self.content_cache.write());
757 cache.insert(OPS_RESULT_INO, content);
758 let attr = self.make_attr(ino, FileType::RegularFile, size);
759 reply.attr(&TTL, &attr);
760 }
761 SAFETY_DIR_INO | TRASH_DIR_INO => {
763 let attr = self.make_attr(ino, FileType::Directory, 0);
764 reply.attr(&TTL, &attr);
765 }
766 HISTORY_FILE_INO => {
767 let content = self.safety_manager.get_history_json(Some(100));
768 let size = content.len() as u64;
769 let mut cache = self.runtime.block_on(self.content_cache.write());
770 cache.insert(HISTORY_FILE_INO, content);
771 let attr = self.make_attr(ino, FileType::RegularFile, size);
772 reply.attr(&TTL, &attr);
773 }
774 UNDO_FILE_INO => {
775 let attr = self.make_attr(ino, FileType::RegularFile, 0);
777 reply.attr(&TTL, &attr);
778 }
779 SEMANTIC_DIR_INO | PENDING_DIR_INO => {
781 let attr = self.make_attr(ino, FileType::Directory, 0);
782 reply.attr(&TTL, &attr);
783 }
784 SIMILAR_OPS_FILE_INO => {
785 let content = self
786 .runtime
787 .block_on(self.semantic_manager.get_similar_json());
788 let size = content.len() as u64;
789 let mut cache = self.runtime.block_on(self.content_cache.write());
790 cache.insert(SIMILAR_OPS_FILE_INO, content);
791 let attr = self.make_attr(ino, FileType::RegularFile, size);
792 reply.attr(&TTL, &attr);
793 }
794 CLEANUP_FILE_INO => {
795 let content = self
796 .runtime
797 .block_on(self.semantic_manager.get_cleanup_json());
798 let size = content.len() as u64;
799 let mut cache = self.runtime.block_on(self.content_cache.write());
800 cache.insert(CLEANUP_FILE_INO, content);
801 let attr = self.make_attr(ino, FileType::RegularFile, size);
802 reply.attr(&TTL, &attr);
803 }
804 DEDUPE_FILE_INO => {
805 let content = self
806 .runtime
807 .block_on(self.semantic_manager.get_dedupe_json());
808 let size = content.len() as u64;
809 let mut cache = self.runtime.block_on(self.content_cache.write());
810 cache.insert(DEDUPE_FILE_INO, content);
811 let attr = self.make_attr(ino, FileType::RegularFile, size);
812 reply.attr(&TTL, &attr);
813 }
814 ORGANIZE_FILE_INO | APPROVE_FILE_INO | REJECT_FILE_INO => {
815 let attr = self.make_attr(ino, FileType::RegularFile, 0);
817 reply.attr(&TTL, &attr);
818 }
819 _ => {
820 let cache = self.runtime.block_on(self.content_cache.read());
822 if let Some(content) = cache.get(&ino) {
823 let attr = self.make_attr(ino, FileType::RegularFile, content.len() as u64);
824 reply.attr(&TTL, &attr);
825 return;
826 }
827 drop(cache);
828
829 let inodes = self.runtime.block_on(self.inodes.read());
831 if let Some(entry) = inodes.get(ino)
832 && let InodeKind::Real { path, .. } = &entry.kind
833 && let Some(attr) = self.real_path_to_attr(path, ino)
834 {
835 reply.attr(&TTL, &attr);
836 return;
837 }
838 reply.error(ENOENT);
839 }
840 }
841 }
842
843 fn read(
844 &mut self,
845 _req: &Request<'_>,
846 ino: u64,
847 _fh: u64,
848 offset: i64,
849 size: u32,
850 _flags: i32,
851 _lock_owner: Option<u64>,
852 reply: ReplyData,
853 ) {
854 debug!("read: ino={}, offset={}, size={}", ino, offset, size);
855
856 let cache = self.runtime.block_on(self.content_cache.read());
858 if let Some(content) = cache.get(&ino) {
859 let offset = offset as usize;
860 let size = size as usize;
861 if offset >= content.len() {
862 reply.data(&[]);
863 } else {
864 let end = (offset + size).min(content.len());
865 reply.data(&content[offset..end]);
866 }
867 return;
868 }
869 drop(cache);
870
871 match ino {
873 INDEX_FILE_INO => {
874 let content = self.get_index_status();
875 let offset = offset as usize;
876 let size = size as usize;
877 if offset >= content.len() {
878 reply.data(&[]);
879 } else {
880 let end = (offset + size).min(content.len());
881 reply.data(&content[offset..end]);
882 }
883 return;
884 }
885 CONFIG_FILE_INO => {
886 let content = self.get_config();
887 let offset = offset as usize;
888 let size = size as usize;
889 if offset >= content.len() {
890 reply.data(&[]);
891 } else {
892 let end = (offset + size).min(content.len());
893 reply.data(&content[offset..end]);
894 }
895 return;
896 }
897 REINDEX_FILE_INO => {
898 reply.data(&[]);
899 return;
900 }
901 HELP_FILE_INO => {
902 let content = self.get_help_content();
903 let offset = offset as usize;
904 let size = size as usize;
905 if offset >= content.len() {
906 reply.data(&[]);
907 } else {
908 let end = (offset + size).min(content.len());
909 reply.data(&content[offset..end]);
910 }
911 return;
912 }
913 OPS_RESULT_INO => {
914 let content = self.runtime.block_on(self.ops_manager.get_last_result());
915 let offset = offset as usize;
916 let size = size as usize;
917 if offset >= content.len() {
918 reply.data(&[]);
919 } else {
920 let end = (offset + size).min(content.len());
921 reply.data(&content[offset..end]);
922 }
923 return;
924 }
925 OPS_CREATE_INO | OPS_DELETE_INO | OPS_MOVE_INO | OPS_BATCH_INO => {
927 reply.data(&[]);
928 return;
929 }
930 HISTORY_FILE_INO => {
931 let content = self.safety_manager.get_history_json(Some(100));
932 let offset = offset as usize;
933 let size = size as usize;
934 if offset >= content.len() {
935 reply.data(&[]);
936 } else {
937 let end = (offset + size).min(content.len());
938 reply.data(&content[offset..end]);
939 }
940 return;
941 }
942 UNDO_FILE_INO => {
944 reply.data(&[]);
945 return;
946 }
947 SIMILAR_OPS_FILE_INO => {
949 let content = self
950 .runtime
951 .block_on(self.semantic_manager.get_similar_json());
952 let offset = offset as usize;
953 let size = size as usize;
954 if offset >= content.len() {
955 reply.data(&[]);
956 } else {
957 let end = (offset + size).min(content.len());
958 reply.data(&content[offset..end]);
959 }
960 return;
961 }
962 CLEANUP_FILE_INO => {
963 let content = self
964 .runtime
965 .block_on(self.semantic_manager.get_cleanup_json());
966 let offset = offset as usize;
967 let size = size as usize;
968 if offset >= content.len() {
969 reply.data(&[]);
970 } else {
971 let end = (offset + size).min(content.len());
972 reply.data(&content[offset..end]);
973 }
974 return;
975 }
976 DEDUPE_FILE_INO => {
977 let content = self
978 .runtime
979 .block_on(self.semantic_manager.get_dedupe_json());
980 let offset = offset as usize;
981 let size = size as usize;
982 if offset >= content.len() {
983 reply.data(&[]);
984 } else {
985 let end = (offset + size).min(content.len());
986 reply.data(&content[offset..end]);
987 }
988 return;
989 }
990 ORGANIZE_FILE_INO | APPROVE_FILE_INO | REJECT_FILE_INO => {
992 reply.data(&[]);
993 return;
994 }
995 _ => {}
996 }
997
998 let inodes = self.runtime.block_on(self.inodes.read());
1000 if let Some(entry) = inodes.get(ino)
1001 && let InodeKind::Real { path, .. } = &entry.kind
1002 {
1003 let path = path.clone();
1004 drop(inodes);
1005
1006 match fs::read(&path) {
1007 Ok(content) => {
1008 let offset = offset as usize;
1009 let size = size as usize;
1010 if offset >= content.len() {
1011 reply.data(&[]);
1012 } else {
1013 let end = (offset + size).min(content.len());
1014 reply.data(&content[offset..end]);
1015 }
1016 return;
1017 }
1018 Err(e) => {
1019 warn!("Failed to read file {:?}: {}", path, e);
1020 reply.error(EIO);
1021 return;
1022 }
1023 }
1024 }
1025
1026 reply.error(ENOENT);
1027 }
1028
1029 fn readdir(
1030 &mut self,
1031 _req: &Request<'_>,
1032 ino: u64,
1033 _fh: u64,
1034 offset: i64,
1035 mut reply: ReplyDirectory,
1036 ) {
1037 debug!("readdir: ino={}, offset={}", ino, offset);
1038
1039 match ino {
1040 ROOT_INO => {
1041 let mut entries = vec![
1042 (ROOT_INO, FileType::Directory, ".".to_string()),
1043 (ROOT_INO, FileType::Directory, "..".to_string()),
1044 (RAGFS_DIR_INO, FileType::Directory, ".ragfs".to_string()),
1045 ];
1046
1047 if let Ok(read_dir) = fs::read_dir(&self.source) {
1049 for entry in read_dir.flatten() {
1050 let name = entry.file_name().to_string_lossy().to_string();
1051 if name.starts_with('.') {
1052 continue; }
1054 let file_type = if entry.path().is_dir() {
1055 FileType::Directory
1056 } else {
1057 FileType::RegularFile
1058 };
1059
1060 let metadata = match entry.metadata() {
1061 Ok(m) => m,
1062 Err(_) => continue,
1063 };
1064
1065 let mut inodes = self.runtime.block_on(self.inodes.write());
1066 let entry_ino = inodes.get_or_create_real(entry.path(), metadata.ino());
1067 entries.push((entry_ino, file_type, name));
1068 }
1069 }
1070
1071 for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
1072 if reply.add(*ino, (i + 1) as i64, *kind, name) {
1073 break;
1074 }
1075 }
1076 reply.ok();
1077 }
1078 RAGFS_DIR_INO => {
1079 let entries = [
1080 (RAGFS_DIR_INO, FileType::Directory, "."),
1081 (ROOT_INO, FileType::Directory, ".."),
1082 (QUERY_DIR_INO, FileType::Directory, ".query"),
1083 (SEARCH_DIR_INO, FileType::Directory, ".search"),
1084 (INDEX_FILE_INO, FileType::RegularFile, ".index"),
1085 (CONFIG_FILE_INO, FileType::RegularFile, ".config"),
1086 (REINDEX_FILE_INO, FileType::RegularFile, ".reindex"),
1087 (HELP_FILE_INO, FileType::RegularFile, ".help"),
1088 (SIMILAR_DIR_INO, FileType::Directory, ".similar"),
1089 (OPS_DIR_INO, FileType::Directory, ".ops"),
1090 (SAFETY_DIR_INO, FileType::Directory, ".safety"),
1091 (SEMANTIC_DIR_INO, FileType::Directory, ".semantic"),
1092 ];
1093
1094 for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
1095 if reply.add(*ino, (i + 1) as i64, *kind, name) {
1096 break;
1097 }
1098 }
1099 reply.ok();
1100 }
1101 SAFETY_DIR_INO => {
1102 let entries = [
1103 (SAFETY_DIR_INO, FileType::Directory, "."),
1104 (RAGFS_DIR_INO, FileType::Directory, ".."),
1105 (TRASH_DIR_INO, FileType::Directory, ".trash"),
1106 (HISTORY_FILE_INO, FileType::RegularFile, ".history"),
1107 (UNDO_FILE_INO, FileType::RegularFile, ".undo"),
1108 ];
1109
1110 for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
1111 if reply.add(*ino, (i + 1) as i64, *kind, name) {
1112 break;
1113 }
1114 }
1115 reply.ok();
1116 }
1117 TRASH_DIR_INO => {
1118 let mut entries: Vec<(u64, FileType, String)> = vec![
1120 (TRASH_DIR_INO, FileType::Directory, ".".to_string()),
1121 (SAFETY_DIR_INO, FileType::Directory, "..".to_string()),
1122 ];
1123
1124 let trash = self.runtime.block_on(self.safety_manager.list_trash());
1126 for entry in trash {
1127 let entry_ino =
1129 FIRST_REAL_INO + 500_000 + (entry.id.as_u128() % 100_000) as u64;
1130 entries.push((entry_ino, FileType::RegularFile, entry.id.to_string()));
1131 }
1132
1133 for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
1134 if reply.add(*ino, (i + 1) as i64, *kind, name) {
1135 break;
1136 }
1137 }
1138 reply.ok();
1139 }
1140 OPS_DIR_INO => {
1141 let entries = [
1142 (OPS_DIR_INO, FileType::Directory, "."),
1143 (RAGFS_DIR_INO, FileType::Directory, ".."),
1144 (OPS_CREATE_INO, FileType::RegularFile, ".create"),
1145 (OPS_DELETE_INO, FileType::RegularFile, ".delete"),
1146 (OPS_MOVE_INO, FileType::RegularFile, ".move"),
1147 (OPS_BATCH_INO, FileType::RegularFile, ".batch"),
1148 (OPS_RESULT_INO, FileType::RegularFile, ".result"),
1149 ];
1150
1151 for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
1152 if reply.add(*ino, (i + 1) as i64, *kind, name) {
1153 break;
1154 }
1155 }
1156 reply.ok();
1157 }
1158 SEMANTIC_DIR_INO => {
1159 let entries = [
1160 (SEMANTIC_DIR_INO, FileType::Directory, "."),
1161 (RAGFS_DIR_INO, FileType::Directory, ".."),
1162 (ORGANIZE_FILE_INO, FileType::RegularFile, ".organize"),
1163 (SIMILAR_OPS_FILE_INO, FileType::RegularFile, ".similar"),
1164 (CLEANUP_FILE_INO, FileType::RegularFile, ".cleanup"),
1165 (DEDUPE_FILE_INO, FileType::RegularFile, ".dedupe"),
1166 (PENDING_DIR_INO, FileType::Directory, ".pending"),
1167 (APPROVE_FILE_INO, FileType::RegularFile, ".approve"),
1168 (REJECT_FILE_INO, FileType::RegularFile, ".reject"),
1169 ];
1170
1171 for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
1172 if reply.add(*ino, (i + 1) as i64, *kind, name) {
1173 break;
1174 }
1175 }
1176 reply.ok();
1177 }
1178 PENDING_DIR_INO => {
1179 let mut entries: Vec<(u64, FileType, String)> = vec![
1181 (PENDING_DIR_INO, FileType::Directory, ".".to_string()),
1182 (SEMANTIC_DIR_INO, FileType::Directory, "..".to_string()),
1183 ];
1184
1185 let plan_ids = self
1187 .runtime
1188 .block_on(self.semantic_manager.get_pending_plan_ids());
1189 for plan_id in plan_ids {
1190 let mut inodes = self.runtime.block_on(self.inodes.write());
1192 let entry_ino =
1193 inodes.get_or_create_query_result(PENDING_DIR_INO, plan_id.clone());
1194 entries.push((entry_ino, FileType::RegularFile, plan_id));
1195 }
1196
1197 for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
1198 if reply.add(*ino, (i + 1) as i64, *kind, name) {
1199 break;
1200 }
1201 }
1202 reply.ok();
1203 }
1204 QUERY_DIR_INO | SEARCH_DIR_INO | SIMILAR_DIR_INO => {
1205 let entries = [
1207 (ino, FileType::Directory, "."),
1208 (RAGFS_DIR_INO, FileType::Directory, ".."),
1209 ];
1210
1211 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset as usize)
1212 {
1213 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1214 break;
1215 }
1216 }
1217 reply.ok();
1218 }
1219 _ => {
1220 let inodes = self.runtime.block_on(self.inodes.read());
1222 if let Some(entry) = inodes.get(ino)
1223 && let InodeKind::Real { path, .. } = &entry.kind
1224 {
1225 let path = path.clone();
1226 let parent_ino = entry.parent;
1227 drop(inodes);
1228
1229 if path.is_dir() {
1230 let mut entries = vec![
1231 (ino, FileType::Directory, ".".to_string()),
1232 (parent_ino, FileType::Directory, "..".to_string()),
1233 ];
1234
1235 if let Ok(read_dir) = fs::read_dir(&path) {
1236 for dir_entry in read_dir.flatten() {
1237 let name = dir_entry.file_name().to_string_lossy().to_string();
1238 if name.starts_with('.') {
1239 continue;
1240 }
1241 let file_type = if dir_entry.path().is_dir() {
1242 FileType::Directory
1243 } else {
1244 FileType::RegularFile
1245 };
1246
1247 let metadata = match dir_entry.metadata() {
1248 Ok(m) => m,
1249 Err(_) => continue,
1250 };
1251
1252 let mut inodes = self.runtime.block_on(self.inodes.write());
1253 let entry_ino =
1254 inodes.get_or_create_real(dir_entry.path(), metadata.ino());
1255 entries.push((entry_ino, file_type, name));
1256 }
1257 }
1258
1259 for (i, (entry_ino, kind, name)) in
1260 entries.iter().enumerate().skip(offset as usize)
1261 {
1262 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1263 break;
1264 }
1265 }
1266 reply.ok();
1267 return;
1268 }
1269 }
1270 reply.error(ENOENT);
1271 }
1272 }
1273 }
1274
1275 fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) {
1276 debug!("open: ino={}, flags={}", ino, flags);
1277 reply.opened(0, 0);
1279 }
1280
1281 fn opendir(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) {
1282 debug!("opendir: ino={}, flags={}", ino, flags);
1283 reply.opened(0, 0);
1284 }
1285
1286 fn write(
1287 &mut self,
1288 _req: &Request<'_>,
1289 ino: u64,
1290 _fh: u64,
1291 _offset: i64,
1292 data: &[u8],
1293 _write_flags: u32,
1294 _flags: i32,
1295 _lock_owner: Option<u64>,
1296 reply: ReplyWrite,
1297 ) {
1298 debug!("write: ino={}, len={}", ino, data.len());
1299
1300 if ino == REINDEX_FILE_INO {
1302 let path_str = String::from_utf8_lossy(data).trim().to_string();
1303
1304 if path_str.is_empty() {
1305 debug!("Empty reindex request, ignoring");
1306 reply.written(data.len() as u32);
1307 return;
1308 }
1309
1310 let path = PathBuf::from(&path_str);
1311
1312 let absolute_path = if path.is_absolute() {
1314 path
1315 } else {
1316 self.source.join(&path)
1317 };
1318
1319 info!("Reindex requested for: {:?}", absolute_path);
1320
1321 if let Some(ref sender) = self.reindex_sender {
1323 let sender = sender.clone();
1324 let path_to_send = absolute_path.clone();
1325
1326 self.runtime.spawn(async move {
1328 if let Err(e) = sender.send(path_to_send).await {
1329 warn!("Failed to send reindex request: {}", e);
1330 }
1331 });
1332
1333 debug!("Reindex request sent for: {:?}", absolute_path);
1334 } else {
1335 warn!("Reindex requested but no sender configured");
1336 }
1337
1338 reply.written(data.len() as u32);
1339 return;
1340 }
1341
1342 if ino == OPS_CREATE_INO {
1344 let data_str = String::from_utf8_lossy(data).to_string();
1345 let ops_manager = self.ops_manager.clone();
1346 self.runtime.block_on(async move {
1347 ops_manager.parse_and_create(&data_str).await;
1348 });
1349 reply.written(data.len() as u32);
1350 return;
1351 }
1352
1353 if ino == OPS_DELETE_INO {
1354 let data_str = String::from_utf8_lossy(data).to_string();
1355 let ops_manager = self.ops_manager.clone();
1356 self.runtime.block_on(async move {
1357 ops_manager.parse_and_delete(&data_str).await;
1358 });
1359 reply.written(data.len() as u32);
1360 return;
1361 }
1362
1363 if ino == OPS_MOVE_INO {
1364 let data_str = String::from_utf8_lossy(data).to_string();
1365 let ops_manager = self.ops_manager.clone();
1366 self.runtime.block_on(async move {
1367 ops_manager.parse_and_move(&data_str).await;
1368 });
1369 reply.written(data.len() as u32);
1370 return;
1371 }
1372
1373 if ino == OPS_BATCH_INO {
1374 let data_str = String::from_utf8_lossy(data).to_string();
1375 let ops_manager = self.ops_manager.clone();
1376 self.runtime.block_on(async move {
1377 ops_manager.parse_and_batch(&data_str).await;
1378 });
1379 reply.written(data.len() as u32);
1380 return;
1381 }
1382
1383 if ino == UNDO_FILE_INO {
1385 let data_str = String::from_utf8_lossy(data).trim().to_string();
1386 if let Ok(operation_id) = uuid::Uuid::parse_str(&data_str) {
1387 let safety_manager = self.safety_manager.clone();
1388 let result = self
1389 .runtime
1390 .block_on(async move { safety_manager.undo(operation_id).await });
1391 match result {
1392 Ok(msg) => info!("Undo successful: {}", msg),
1393 Err(e) => warn!("Undo failed: {}", e),
1394 }
1395 } else {
1396 warn!("Invalid operation ID for undo: {}", data_str);
1397 }
1398 reply.written(data.len() as u32);
1399 return;
1400 }
1401
1402 if ino == ORGANIZE_FILE_INO {
1404 let data_str = String::from_utf8_lossy(data).to_string();
1405 let semantic_manager = self.semantic_manager.clone();
1406 let result = self.runtime.block_on(async move {
1407 match serde_json::from_str::<crate::semantic::OrganizeRequest>(&data_str) {
1408 Ok(request) => semantic_manager.create_organize_plan(request).await,
1409 Err(e) => Err(format!("Invalid OrganizeRequest JSON: {e}")),
1410 }
1411 });
1412 match result {
1413 Ok(plan) => info!("Created organization plan: {}", plan.id),
1414 Err(e) => warn!("Failed to create organization plan: {}", e),
1415 }
1416 reply.written(data.len() as u32);
1417 return;
1418 }
1419
1420 if ino == SIMILAR_OPS_FILE_INO {
1422 let data_str = String::from_utf8_lossy(data).trim().to_string();
1423 let path = PathBuf::from(&data_str);
1424 let semantic_manager = self.semantic_manager.clone();
1425 let result = self
1426 .runtime
1427 .block_on(async move { semantic_manager.find_similar(&path).await });
1428 match result {
1429 Ok(r) => info!("Found {} similar files to {}", r.similar.len(), data_str),
1430 Err(e) => warn!("Failed to find similar files: {}", e),
1431 }
1432 reply.written(data.len() as u32);
1433 return;
1434 }
1435
1436 if ino == APPROVE_FILE_INO {
1438 let data_str = String::from_utf8_lossy(data).trim().to_string();
1439 if let Ok(plan_id) = uuid::Uuid::parse_str(&data_str) {
1440 let semantic_manager = self.semantic_manager.clone();
1441 let result = self
1442 .runtime
1443 .block_on(async move { semantic_manager.approve_plan(plan_id).await });
1444 match result {
1445 Ok(plan) => info!("Approved plan: {}", plan.id),
1446 Err(e) => warn!("Failed to approve plan: {}", e),
1447 }
1448 } else {
1449 warn!("Invalid plan ID for approve: {}", data_str);
1450 }
1451 reply.written(data.len() as u32);
1452 return;
1453 }
1454
1455 if ino == REJECT_FILE_INO {
1457 let data_str = String::from_utf8_lossy(data).trim().to_string();
1458 if let Ok(plan_id) = uuid::Uuid::parse_str(&data_str) {
1459 let semantic_manager = self.semantic_manager.clone();
1460 let result = self
1461 .runtime
1462 .block_on(async move { semantic_manager.reject_plan(plan_id).await });
1463 match result {
1464 Ok(plan) => info!("Rejected plan: {}", plan.id),
1465 Err(e) => warn!("Failed to reject plan: {}", e),
1466 }
1467 } else {
1468 warn!("Invalid plan ID for reject: {}", data_str);
1469 }
1470 reply.written(data.len() as u32);
1471 return;
1472 }
1473
1474 let inodes = self.runtime.block_on(self.inodes.read());
1476 if let Some(entry) = inodes.get(ino)
1477 && let InodeKind::Real { path, .. } = &entry.kind
1478 {
1479 let path = path.clone();
1480 drop(inodes);
1481
1482 match fs::write(&path, data) {
1483 Ok(()) => {
1484 reply.written(data.len() as u32);
1485 return;
1486 }
1487 Err(e) => {
1488 warn!("Failed to write file {:?}: {}", path, e);
1489 reply.error(EIO);
1490 return;
1491 }
1492 }
1493 }
1494
1495 reply.error(ENOSYS);
1496 }
1497
1498 fn forget(&mut self, _req: &Request<'_>, ino: u64, nlookup: u64) {
1499 debug!("forget: ino={}, nlookup={}", ino, nlookup);
1500 let mut inodes = self.runtime.block_on(self.inodes.write());
1501 inodes.forget(ino, nlookup);
1502 }
1503
1504 fn create(
1505 &mut self,
1506 _req: &Request<'_>,
1507 parent: u64,
1508 name: &OsStr,
1509 mode: u32,
1510 _umask: u32,
1511 flags: i32,
1512 reply: ReplyCreate,
1513 ) {
1514 let name_str = name.to_string_lossy();
1515 debug!(
1516 "create: parent={}, name={}, mode={:o}",
1517 parent, name_str, mode
1518 );
1519
1520 if parent < FIRST_REAL_INO && parent != ROOT_INO {
1522 reply.error(EPERM);
1523 return;
1524 }
1525
1526 let Some(parent_path) = self.resolve_parent_path(parent) else {
1528 reply.error(ENOENT);
1529 return;
1530 };
1531
1532 let new_path = parent_path.join(&*name_str);
1533
1534 if new_path.exists() {
1536 reply.error(EEXIST);
1537 return;
1538 }
1539
1540 match fs::File::create(&new_path) {
1542 Ok(_) => {
1543 let metadata = match fs::metadata(&new_path) {
1545 Ok(m) => m,
1546 Err(e) => {
1547 warn!("Failed to get metadata for new file {:?}: {}", new_path, e);
1548 reply.error(EIO);
1549 return;
1550 }
1551 };
1552
1553 let mut inodes = self.runtime.block_on(self.inodes.write());
1555 let ino = inodes.get_or_create_real(new_path.clone(), metadata.ino());
1556 drop(inodes);
1557
1558 if let Some(ref sender) = self.reindex_sender {
1560 let sender = sender.clone();
1561 let path_to_send = new_path.clone();
1562 self.runtime.spawn(async move {
1563 if let Err(e) = sender.send(path_to_send).await {
1564 warn!("Failed to send reindex request: {}", e);
1565 }
1566 });
1567 }
1568
1569 if let Some(attr) = self.real_path_to_attr(&new_path, ino) {
1570 reply.created(&TTL, &attr, 0, 0, flags as u32);
1571 } else {
1572 reply.error(EIO);
1573 }
1574 }
1575 Err(e) => {
1576 warn!("Failed to create file {:?}: {}", new_path, e);
1577 reply.error(EIO);
1578 }
1579 }
1580 }
1581
1582 fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
1583 let name_str = name.to_string_lossy();
1584 debug!("unlink: parent={}, name={}", parent, name_str);
1585
1586 if parent < FIRST_REAL_INO && parent != ROOT_INO {
1588 reply.error(EPERM);
1589 return;
1590 }
1591
1592 let Some(parent_path) = self.resolve_parent_path(parent) else {
1594 reply.error(ENOENT);
1595 return;
1596 };
1597
1598 let file_path = parent_path.join(&*name_str);
1599
1600 if !file_path.exists() {
1602 reply.error(ENOENT);
1603 return;
1604 }
1605
1606 if file_path.is_dir() {
1607 reply.error(EISDIR);
1608 return;
1609 }
1610
1611 if let Some(ref store) = self.store {
1613 let store = store.clone();
1614 let path_for_delete = file_path.clone();
1615 let result = self
1616 .runtime
1617 .block_on(async move { store.delete_by_file_path(&path_for_delete).await });
1618 if let Err(e) = result {
1619 warn!("Failed to delete from store {:?}: {}", file_path, e);
1620 }
1622 }
1623
1624 match fs::remove_file(&file_path) {
1626 Ok(()) => {
1627 let mut inodes = self.runtime.block_on(self.inodes.write());
1629 if let Some(ino) = inodes.get_by_path(&file_path) {
1630 inodes.remove(ino);
1631 }
1632 info!("Deleted file: {:?}", file_path);
1633 reply.ok();
1634 }
1635 Err(e) => {
1636 warn!("Failed to delete file {:?}: {}", file_path, e);
1637 reply.error(EIO);
1638 }
1639 }
1640 }
1641
1642 fn mkdir(
1643 &mut self,
1644 _req: &Request<'_>,
1645 parent: u64,
1646 name: &OsStr,
1647 mode: u32,
1648 _umask: u32,
1649 reply: ReplyEntry,
1650 ) {
1651 let name_str = name.to_string_lossy();
1652 debug!(
1653 "mkdir: parent={}, name={}, mode={:o}",
1654 parent, name_str, mode
1655 );
1656
1657 if parent < FIRST_REAL_INO && parent != ROOT_INO {
1659 reply.error(EPERM);
1660 return;
1661 }
1662
1663 let Some(parent_path) = self.resolve_parent_path(parent) else {
1665 reply.error(ENOENT);
1666 return;
1667 };
1668
1669 let new_path = parent_path.join(&*name_str);
1670
1671 if new_path.exists() {
1673 reply.error(EEXIST);
1674 return;
1675 }
1676
1677 match fs::create_dir(&new_path) {
1679 Ok(()) => {
1680 let metadata = match fs::metadata(&new_path) {
1682 Ok(m) => m,
1683 Err(e) => {
1684 warn!("Failed to get metadata for new dir {:?}: {}", new_path, e);
1685 reply.error(EIO);
1686 return;
1687 }
1688 };
1689
1690 let mut inodes = self.runtime.block_on(self.inodes.write());
1692 let ino = inodes.get_or_create_real(new_path.clone(), metadata.ino());
1693 drop(inodes);
1694
1695 if let Some(attr) = self.real_path_to_attr(&new_path, ino) {
1696 info!("Created directory: {:?}", new_path);
1697 reply.entry(&TTL, &attr, 0);
1698 } else {
1699 reply.error(EIO);
1700 }
1701 }
1702 Err(e) => {
1703 warn!("Failed to create directory {:?}: {}", new_path, e);
1704 reply.error(EIO);
1705 }
1706 }
1707 }
1708
1709 fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
1710 let name_str = name.to_string_lossy();
1711 debug!("rmdir: parent={}, name={}", parent, name_str);
1712
1713 if parent < FIRST_REAL_INO && parent != ROOT_INO {
1715 reply.error(EPERM);
1716 return;
1717 }
1718
1719 let Some(parent_path) = self.resolve_parent_path(parent) else {
1721 reply.error(ENOENT);
1722 return;
1723 };
1724
1725 let dir_path = parent_path.join(&*name_str);
1726
1727 if !dir_path.exists() {
1729 reply.error(ENOENT);
1730 return;
1731 }
1732
1733 if !dir_path.is_dir() {
1734 reply.error(ENOTDIR);
1735 return;
1736 }
1737
1738 match fs::remove_dir(&dir_path) {
1740 Ok(()) => {
1741 let mut inodes = self.runtime.block_on(self.inodes.write());
1743 if let Some(ino) = inodes.get_by_path(&dir_path) {
1744 inodes.remove(ino);
1745 }
1746 info!("Removed directory: {:?}", dir_path);
1747 reply.ok();
1748 }
1749 Err(e) => {
1750 if e.kind() == std::io::ErrorKind::DirectoryNotEmpty
1751 || e.raw_os_error() == Some(libc::ENOTEMPTY)
1752 {
1753 reply.error(ENOTEMPTY);
1754 } else {
1755 warn!("Failed to remove directory {:?}: {}", dir_path, e);
1756 reply.error(EIO);
1757 }
1758 }
1759 }
1760 }
1761
1762 fn rename(
1763 &mut self,
1764 _req: &Request<'_>,
1765 parent: u64,
1766 name: &OsStr,
1767 newparent: u64,
1768 newname: &OsStr,
1769 _flags: u32,
1770 reply: ReplyEmpty,
1771 ) {
1772 let name_str = name.to_string_lossy();
1773 let newname_str = newname.to_string_lossy();
1774 debug!(
1775 "rename: parent={}, name={}, newparent={}, newname={}",
1776 parent, name_str, newparent, newname_str
1777 );
1778
1779 if (parent < FIRST_REAL_INO && parent != ROOT_INO)
1781 || (newparent < FIRST_REAL_INO && newparent != ROOT_INO)
1782 {
1783 reply.error(EPERM);
1784 return;
1785 }
1786
1787 let Some(src_parent_path) = self.resolve_parent_path(parent) else {
1789 reply.error(ENOENT);
1790 return;
1791 };
1792
1793 let Some(dst_parent_path) = self.resolve_parent_path(newparent) else {
1795 reply.error(ENOENT);
1796 return;
1797 };
1798
1799 let src_path = src_parent_path.join(&*name_str);
1800 let dst_path = dst_parent_path.join(&*newname_str);
1801
1802 if !src_path.exists() {
1804 reply.error(ENOENT);
1805 return;
1806 }
1807
1808 match fs::rename(&src_path, &dst_path) {
1810 Ok(()) => {
1811 if let Some(ref store) = self.store {
1813 let store = store.clone();
1814 let src = src_path.clone();
1815 let dst = dst_path.clone();
1816 let result = self
1817 .runtime
1818 .block_on(async move { store.update_file_path(&src, &dst).await });
1819 if let Err(e) = result {
1820 warn!(
1821 "Failed to update store path {:?} -> {:?}: {}",
1822 src_path, dst_path, e
1823 );
1824 }
1825 }
1826
1827 let mut inodes = self.runtime.block_on(self.inodes.write());
1829 if let Some(ino) = inodes.get_by_path(&src_path) {
1830 inodes.update_path(ino, dst_path.clone());
1831 }
1832 drop(inodes);
1833
1834 info!("Renamed: {:?} -> {:?}", src_path, dst_path);
1835 reply.ok();
1836 }
1837 Err(e) => {
1838 warn!("Failed to rename {:?} -> {:?}: {}", src_path, dst_path, e);
1839 reply.error(EIO);
1840 }
1841 }
1842 }
1843
1844 fn setattr(
1845 &mut self,
1846 _req: &Request<'_>,
1847 ino: u64,
1848 _mode: Option<u32>,
1849 _uid: Option<u32>,
1850 _gid: Option<u32>,
1851 size: Option<u64>,
1852 _atime: Option<TimeOrNow>,
1853 _mtime: Option<TimeOrNow>,
1854 _ctime: Option<SystemTime>,
1855 _fh: Option<u64>,
1856 _crtime: Option<SystemTime>,
1857 _chgtime: Option<SystemTime>,
1858 _bkuptime: Option<SystemTime>,
1859 _flags: Option<u32>,
1860 reply: ReplyAttr,
1861 ) {
1862 debug!("setattr: ino={}, size={:?}", ino, size);
1863
1864 if ino < FIRST_REAL_INO {
1866 reply.error(EPERM);
1867 return;
1868 }
1869
1870 let inodes = self.runtime.block_on(self.inodes.read());
1872 let Some(entry) = inodes.get(ino) else {
1873 drop(inodes);
1874 reply.error(ENOENT);
1875 return;
1876 };
1877 let InodeKind::Real { path, .. } = &entry.kind else {
1878 drop(inodes);
1879 reply.error(EINVAL);
1880 return;
1881 };
1882 let path = path.clone();
1883 drop(inodes);
1884
1885 if let Some(new_size) = size {
1887 match fs::OpenOptions::new().write(true).open(&path) {
1888 Ok(file) => {
1889 if let Err(e) = file.set_len(new_size) {
1890 warn!("Failed to truncate {:?}: {}", path, e);
1891 reply.error(EIO);
1892 return;
1893 }
1894
1895 if let Some(ref sender) = self.reindex_sender {
1897 let sender = sender.clone();
1898 let path_to_send = path.clone();
1899 self.runtime.spawn(async move {
1900 if let Err(e) = sender.send(path_to_send).await {
1901 warn!("Failed to send reindex request: {}", e);
1902 }
1903 });
1904 }
1905 }
1906 Err(e) => {
1907 warn!("Failed to open {:?} for truncate: {}", path, e);
1908 reply.error(EIO);
1909 return;
1910 }
1911 }
1912 }
1913
1914 if let Some(attr) = self.real_path_to_attr(&path, ino) {
1916 reply.attr(&TTL, &attr);
1917 } else {
1918 reply.error(EIO);
1919 }
1920 }
1921}
1922
1923fn truncate(s: &str, max_len: usize) -> String {
1925 let s = s.replace('\n', " ").replace('\r', "");
1926 if s.len() <= max_len {
1927 s
1928 } else {
1929 format!("{}...", &s[..max_len.saturating_sub(3)])
1930 }
1931}
1932
1933#[cfg(test)]
1934mod tests {
1935 use super::*;
1936
1937 #[test]
1940 fn test_truncate_short_string() {
1941 let result = truncate("Hello", 10);
1942 assert_eq!(result, "Hello");
1943 }
1944
1945 #[test]
1946 fn test_truncate_exact_length() {
1947 let result = truncate("Hello", 5);
1948 assert_eq!(result, "Hello");
1949 }
1950
1951 #[test]
1952 fn test_truncate_long_string() {
1953 let result = truncate("Hello, World!", 8);
1954 assert_eq!(result, "Hello...");
1955 }
1956
1957 #[test]
1958 fn test_truncate_removes_newlines() {
1959 let result = truncate("Hello\nWorld\nTest", 100);
1960 assert_eq!(result, "Hello World Test");
1961 }
1962
1963 #[test]
1964 fn test_truncate_removes_carriage_returns() {
1965 let result = truncate("Hello\r\nWorld", 100);
1967 assert_eq!(result, "Hello World");
1968 }
1969
1970 #[test]
1971 fn test_truncate_empty_string() {
1972 let result = truncate("", 10);
1973 assert_eq!(result, "");
1974 }
1975
1976 #[test]
1977 fn test_truncate_very_short_max() {
1978 let result = truncate("Hello", 3);
1979 assert_eq!(result, "...");
1980 }
1981
1982 #[test]
1983 fn test_truncate_max_zero() {
1984 let result = truncate("Hello", 0);
1985 assert_eq!(result, "...");
1986 }
1987
1988 #[test]
1989 fn test_truncate_unicode() {
1990 let result = truncate("こんにちは世界", 100);
1991 assert_eq!(result, "こんにちは世界");
1992 }
1993
1994 #[test]
1995 fn test_truncate_with_mixed_whitespace() {
1996 let result = truncate("Line1\n\nLine2\r\n\r\nLine3", 100);
1998 assert_eq!(result, "Line1 Line2 Line3");
1999 }
2000
2001 #[tokio::test]
2004 async fn test_ragfs_new() {
2005 let source = PathBuf::from("/tmp/test");
2006 let fs = RagFs::new(source.clone());
2007
2008 assert_eq!(fs.source(), &source);
2009 assert!(fs.store.is_none());
2010 assert!(fs.query_executor.is_none());
2011 }
2012
2013 #[tokio::test]
2014 async fn test_ragfs_source_getter() {
2015 let source = PathBuf::from("/my/test/directory");
2016 let fs = RagFs::new(source.clone());
2017
2018 assert_eq!(fs.source(), &source);
2019 }
2020
2021 #[tokio::test]
2022 async fn test_ragfs_inode_table_initialized() {
2023 let fs = RagFs::new(PathBuf::from("/tmp/test"));
2024
2025 let inodes = fs.inodes.read().await;
2026 assert!(inodes.get(ROOT_INO).is_some());
2028 assert!(inodes.get(RAGFS_DIR_INO).is_some());
2029 assert!(inodes.get(QUERY_DIR_INO).is_some());
2030 }
2031
2032 #[tokio::test]
2033 async fn test_ragfs_content_cache_empty() {
2034 let fs = RagFs::new(PathBuf::from("/tmp/test"));
2035
2036 let cache = fs.content_cache.read().await;
2037 assert!(cache.is_empty());
2038 }
2039
2040 #[tokio::test]
2043 async fn test_get_config_without_rag() {
2044 let fs = RagFs::new(PathBuf::from("/tmp/test-config"));
2045 let config = fs.get_config();
2046
2047 let json: serde_json::Value = serde_json::from_slice(&config).expect("Valid JSON");
2048
2049 assert_eq!(json["source"], "/tmp/test-config");
2050 assert_eq!(json["store_configured"], false);
2051 assert_eq!(json["query_executor_configured"], false);
2052 }
2053
2054 #[tokio::test]
2055 async fn test_get_config_returns_valid_json() {
2056 let fs = RagFs::new(PathBuf::from("/test/path"));
2057 let config = fs.get_config();
2058
2059 let config_str = String::from_utf8(config).expect("Valid UTF-8");
2061
2062 let json: serde_json::Value = serde_json::from_str(&config_str).expect("Valid JSON");
2064
2065 assert!(json.get("source").is_some());
2067 assert!(json.get("store_configured").is_some());
2068 assert!(json.get("query_executor_configured").is_some());
2069 }
2070
2071 #[tokio::test]
2074 async fn test_get_index_status_without_store() {
2075 let fs = RagFs::new(PathBuf::from("/tmp/test"));
2076 let status = fs.get_index_status();
2077
2078 let json: serde_json::Value = serde_json::from_slice(&status).expect("Valid JSON");
2079
2080 assert_eq!(json["status"], "not_initialized");
2081 assert_eq!(json["message"], "No store configured");
2082 }
2083
2084 #[tokio::test]
2085 async fn test_get_index_status_returns_valid_json() {
2086 let fs = RagFs::new(PathBuf::from("/test/path"));
2087 let status = fs.get_index_status();
2088
2089 let status_str = String::from_utf8(status).expect("Valid UTF-8");
2091
2092 let _json: serde_json::Value = serde_json::from_str(&status_str).expect("Valid JSON");
2094 }
2095
2096 #[tokio::test]
2099 async fn test_execute_query_without_executor() {
2100 let fs = RagFs::new(PathBuf::from("/tmp/test"));
2101 let result = fs.execute_query("test query");
2102
2103 let json: serde_json::Value = serde_json::from_slice(&result).expect("Valid JSON");
2104
2105 assert_eq!(json["error"], "Query executor not configured");
2106 }
2107
2108 #[tokio::test]
2109 async fn test_execute_query_returns_valid_json() {
2110 let fs = RagFs::new(PathBuf::from("/test/path"));
2111 let result = fs.execute_query("any query");
2112
2113 let result_str = String::from_utf8(result).expect("Valid UTF-8");
2115
2116 let _json: serde_json::Value = serde_json::from_str(&result_str).expect("Valid JSON");
2118 }
2119
2120 #[test]
2123 fn test_ttl_is_reasonable() {
2124 assert_eq!(TTL, Duration::from_secs(1));
2125 }
2126
2127 #[test]
2128 fn test_block_size_is_standard() {
2129 assert_eq!(BLOCK_SIZE, 512);
2130 }
2131
2132 #[tokio::test]
2135 async fn test_make_attr_directory() {
2136 let fs = RagFs::new(PathBuf::from("/tmp/test"));
2137 let attr = fs.make_attr(100, fuser::FileType::Directory, 0);
2138
2139 assert_eq!(attr.ino, 100);
2140 assert_eq!(attr.size, 0);
2141 assert_eq!(attr.kind, fuser::FileType::Directory);
2142 assert_eq!(attr.perm, 0o755);
2143 assert_eq!(attr.nlink, 2);
2144 }
2145
2146 #[tokio::test]
2147 async fn test_make_attr_regular_file() {
2148 let fs = RagFs::new(PathBuf::from("/tmp/test"));
2149 let attr = fs.make_attr(200, fuser::FileType::RegularFile, 1024);
2150
2151 assert_eq!(attr.ino, 200);
2152 assert_eq!(attr.size, 1024);
2153 assert_eq!(attr.kind, fuser::FileType::RegularFile);
2154 assert_eq!(attr.perm, 0o644);
2155 assert_eq!(attr.nlink, 1);
2156 }
2157
2158 #[tokio::test]
2159 async fn test_make_attr_blocks_calculation() {
2160 let fs = RagFs::new(PathBuf::from("/tmp/test"));
2161
2162 let attr = fs.make_attr(1, fuser::FileType::RegularFile, 512);
2164 assert_eq!(attr.blocks, 1);
2165
2166 let attr = fs.make_attr(1, fuser::FileType::RegularFile, 513);
2168 assert_eq!(attr.blocks, 2);
2169
2170 let attr = fs.make_attr(1, fuser::FileType::RegularFile, 0);
2172 assert_eq!(attr.blocks, 0);
2173 }
2174
2175 #[tokio::test]
2176 async fn test_make_attr_has_current_uid_gid() {
2177 let fs = RagFs::new(PathBuf::from("/tmp/test"));
2178 let attr = fs.make_attr(1, fuser::FileType::RegularFile, 0);
2179
2180 #[allow(unsafe_code)]
2182 let expected_uid = unsafe { libc::getuid() };
2183 #[allow(unsafe_code)]
2184 let expected_gid = unsafe { libc::getgid() };
2185
2186 assert_eq!(attr.uid, expected_uid);
2187 assert_eq!(attr.gid, expected_gid);
2188 }
2189}