Skip to main content

ragfs_fuse/filesystem/
semantic.rs

1//! `.semantic/` virtual files.
2
3use fuser::{FileType, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyWrite};
4use libc::ENOENT;
5use std::path::PathBuf;
6use tracing::{info, warn};
7
8use super::{RagFs, TTL, reply_file_bytes};
9use crate::inode::{
10    APPROVE_FILE_INO, CLEANUP_FILE_INO, DEDUPE_FILE_INO, ORGANIZE_FILE_INO, PENDING_DIR_INO,
11    RAGFS_DIR_INO, REJECT_FILE_INO, SEMANTIC_DIR_INO, SIMILAR_OPS_FILE_INO,
12};
13
14impl RagFs {
15    pub(crate) fn lookup_semantic_dir(&mut self, name_str: &str, reply: ReplyEntry) {
16        match name_str {
17            ".organize" => {
18                let attr = self.make_attr(ORGANIZE_FILE_INO, FileType::RegularFile, 0);
19                reply.entry(&TTL, &attr, 0);
20            }
21            ".similar" => {
22                let content = self
23                    .runtime
24                    .block_on(self.semantic_manager.get_similar_json());
25                let attr = self.make_attr(
26                    SIMILAR_OPS_FILE_INO,
27                    FileType::RegularFile,
28                    content.len() as u64,
29                );
30                let mut cache = self.runtime.block_on(self.content_cache.write());
31                cache.insert(SIMILAR_OPS_FILE_INO, content);
32                reply.entry(&TTL, &attr, 0);
33            }
34            ".cleanup" => {
35                let content = self
36                    .runtime
37                    .block_on(self.semantic_manager.get_cleanup_json());
38                let attr = self.make_attr(
39                    CLEANUP_FILE_INO,
40                    FileType::RegularFile,
41                    content.len() as u64,
42                );
43                let mut cache = self.runtime.block_on(self.content_cache.write());
44                cache.insert(CLEANUP_FILE_INO, content);
45                reply.entry(&TTL, &attr, 0);
46            }
47            ".dedupe" => {
48                let content = self
49                    .runtime
50                    .block_on(self.semantic_manager.get_dedupe_json());
51                let attr =
52                    self.make_attr(DEDUPE_FILE_INO, FileType::RegularFile, content.len() as u64);
53                let mut cache = self.runtime.block_on(self.content_cache.write());
54                cache.insert(DEDUPE_FILE_INO, content);
55                reply.entry(&TTL, &attr, 0);
56            }
57            ".pending" => {
58                let attr = self.make_attr(PENDING_DIR_INO, FileType::Directory, 0);
59                reply.entry(&TTL, &attr, 0);
60            }
61            ".approve" => {
62                let attr = self.make_attr(APPROVE_FILE_INO, FileType::RegularFile, 0);
63                reply.entry(&TTL, &attr, 0);
64            }
65            ".reject" => {
66                let attr = self.make_attr(REJECT_FILE_INO, FileType::RegularFile, 0);
67                reply.entry(&TTL, &attr, 0);
68            }
69            _ => reply.error(ENOENT),
70        }
71    }
72
73    pub(crate) fn lookup_pending_dir(&mut self, name_str: &str, reply: ReplyEntry) {
74        let plan_ids = self
75            .runtime
76            .block_on(self.semantic_manager.get_pending_plan_ids());
77        if plan_ids.contains(&name_str.to_string()) {
78            let content = self
79                .runtime
80                .block_on(self.semantic_manager.get_plan_json(name_str));
81            let mut inodes = self.runtime.block_on(self.inodes.write());
82            let ino = inodes.get_or_create_query_result(PENDING_DIR_INO, name_str.to_string());
83            let attr = self.make_attr(ino, FileType::RegularFile, content.len() as u64);
84            let mut cache = self.runtime.block_on(self.content_cache.write());
85            cache.insert(ino, content);
86            reply.entry(&TTL, &attr, 0);
87            return;
88        }
89        reply.error(ENOENT);
90    }
91
92    pub(crate) fn getattr_semantic(&mut self, ino: u64, reply: ReplyAttr) {
93        match ino {
94            SEMANTIC_DIR_INO | PENDING_DIR_INO => {
95                let attr = self.make_attr(ino, FileType::Directory, 0);
96                reply.attr(&TTL, &attr);
97            }
98            SIMILAR_OPS_FILE_INO => {
99                let content = self
100                    .runtime
101                    .block_on(self.semantic_manager.get_similar_json());
102                let size = content.len() as u64;
103                let mut cache = self.runtime.block_on(self.content_cache.write());
104                cache.insert(SIMILAR_OPS_FILE_INO, content);
105                let attr = self.make_attr(ino, FileType::RegularFile, size);
106                reply.attr(&TTL, &attr);
107            }
108            CLEANUP_FILE_INO => {
109                let content = self
110                    .runtime
111                    .block_on(self.semantic_manager.get_cleanup_json());
112                let size = content.len() as u64;
113                let mut cache = self.runtime.block_on(self.content_cache.write());
114                cache.insert(CLEANUP_FILE_INO, content);
115                let attr = self.make_attr(ino, FileType::RegularFile, size);
116                reply.attr(&TTL, &attr);
117            }
118            DEDUPE_FILE_INO => {
119                let content = self
120                    .runtime
121                    .block_on(self.semantic_manager.get_dedupe_json());
122                let size = content.len() as u64;
123                let mut cache = self.runtime.block_on(self.content_cache.write());
124                cache.insert(DEDUPE_FILE_INO, content);
125                let attr = self.make_attr(ino, FileType::RegularFile, size);
126                reply.attr(&TTL, &attr);
127            }
128            ORGANIZE_FILE_INO | APPROVE_FILE_INO | REJECT_FILE_INO => {
129                let attr = self.make_attr(ino, FileType::RegularFile, 0);
130                reply.attr(&TTL, &attr);
131            }
132            _ => reply.error(ENOENT),
133        }
134    }
135
136    pub(crate) fn read_semantic(&mut self, ino: u64, offset: i64, size: u32, reply: ReplyData) {
137        match ino {
138            SIMILAR_OPS_FILE_INO => {
139                let content = self
140                    .runtime
141                    .block_on(self.semantic_manager.get_similar_json());
142                reply_file_bytes(reply, &content, offset, size);
143            }
144            CLEANUP_FILE_INO => {
145                let content = self
146                    .runtime
147                    .block_on(self.semantic_manager.get_cleanup_json());
148                reply_file_bytes(reply, &content, offset, size);
149            }
150            DEDUPE_FILE_INO => {
151                let content = self
152                    .runtime
153                    .block_on(self.semantic_manager.get_dedupe_json());
154                reply_file_bytes(reply, &content, offset, size);
155            }
156            ORGANIZE_FILE_INO | APPROVE_FILE_INO | REJECT_FILE_INO => {
157                reply.data(&[]);
158            }
159            _ => reply.error(ENOENT),
160        }
161    }
162
163    pub(crate) fn readdir_semantic(&mut self, ino: u64, offset: i64, mut reply: ReplyDirectory) {
164        match ino {
165            SEMANTIC_DIR_INO => {
166                let entries = [
167                    (SEMANTIC_DIR_INO, FileType::Directory, "."),
168                    (RAGFS_DIR_INO, FileType::Directory, ".."),
169                    (ORGANIZE_FILE_INO, FileType::RegularFile, ".organize"),
170                    (SIMILAR_OPS_FILE_INO, FileType::RegularFile, ".similar"),
171                    (CLEANUP_FILE_INO, FileType::RegularFile, ".cleanup"),
172                    (DEDUPE_FILE_INO, FileType::RegularFile, ".dedupe"),
173                    (PENDING_DIR_INO, FileType::Directory, ".pending"),
174                    (APPROVE_FILE_INO, FileType::RegularFile, ".approve"),
175                    (REJECT_FILE_INO, FileType::RegularFile, ".reject"),
176                ];
177
178                for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
179                    if reply.add(*ino, (i + 1) as i64, *kind, name) {
180                        break;
181                    }
182                }
183                reply.ok();
184            }
185            PENDING_DIR_INO => {
186                let mut entries: Vec<(u64, FileType, String)> = vec![
187                    (PENDING_DIR_INO, FileType::Directory, ".".to_string()),
188                    (SEMANTIC_DIR_INO, FileType::Directory, "..".to_string()),
189                ];
190
191                let plan_ids = self
192                    .runtime
193                    .block_on(self.semantic_manager.get_pending_plan_ids());
194                for plan_id in plan_ids {
195                    let mut inodes = self.runtime.block_on(self.inodes.write());
196                    let entry_ino =
197                        inodes.get_or_create_query_result(PENDING_DIR_INO, plan_id.clone());
198                    entries.push((entry_ino, FileType::RegularFile, plan_id));
199                }
200
201                for (i, (ino, kind, name)) in entries.iter().enumerate().skip(offset as usize) {
202                    if reply.add(*ino, (i + 1) as i64, *kind, name) {
203                        break;
204                    }
205                }
206                reply.ok();
207            }
208            _ => reply.error(ENOENT),
209        }
210    }
211
212    pub(crate) fn write_semantic(&mut self, ino: u64, data: &[u8], reply: ReplyWrite) {
213        if ino == ORGANIZE_FILE_INO {
214            let data_str = String::from_utf8_lossy(data).to_string();
215            let semantic_manager = self.semantic_manager.clone();
216            let result = self.runtime.block_on(async move {
217                match serde_json::from_str::<crate::semantic::OrganizeRequest>(&data_str) {
218                    Ok(request) => semantic_manager.create_organize_plan(request).await,
219                    Err(e) => Err(format!("Invalid OrganizeRequest JSON: {e}")),
220                }
221            });
222            match result {
223                Ok(plan) => info!("Created organization plan: {}", plan.id),
224                Err(e) => warn!("Failed to create organization plan: {}", e),
225            }
226            reply.written(data.len() as u32);
227            return;
228        }
229        if ino == SIMILAR_OPS_FILE_INO {
230            let data_str = String::from_utf8_lossy(data).trim().to_string();
231            let path = PathBuf::from(&data_str);
232            let semantic_manager = self.semantic_manager.clone();
233            let result = self
234                .runtime
235                .block_on(async move { semantic_manager.find_similar(&path).await });
236            match result {
237                Ok(r) => info!("Found {} similar files to {}", r.similar.len(), data_str),
238                Err(e) => warn!("Failed to find similar files: {}", e),
239            }
240            reply.written(data.len() as u32);
241            return;
242        }
243        if ino == APPROVE_FILE_INO {
244            let data_str = String::from_utf8_lossy(data).trim().to_string();
245            if let Ok(plan_id) = uuid::Uuid::parse_str(&data_str) {
246                let semantic_manager = self.semantic_manager.clone();
247                let result = self
248                    .runtime
249                    .block_on(async move { semantic_manager.approve_plan(plan_id).await });
250                match result {
251                    Ok(plan) => info!("Approved plan: {}", plan.id),
252                    Err(e) => warn!("Failed to approve plan: {}", e),
253                }
254            } else {
255                warn!("Invalid plan ID for approve: {}", data_str);
256            }
257            reply.written(data.len() as u32);
258            return;
259        }
260        if ino == REJECT_FILE_INO {
261            let data_str = String::from_utf8_lossy(data).trim().to_string();
262            if let Ok(plan_id) = uuid::Uuid::parse_str(&data_str) {
263                let semantic_manager = self.semantic_manager.clone();
264                let result = self
265                    .runtime
266                    .block_on(async move { semantic_manager.reject_plan(plan_id).await });
267                match result {
268                    Ok(plan) => info!("Rejected plan: {}", plan.id),
269                    Err(e) => warn!("Failed to reject plan: {}", e),
270                }
271            } else {
272                warn!("Invalid plan ID for reject: {}", data_str);
273            }
274            reply.written(data.len() as u32);
275        }
276    }
277}