1use crate::engine::block::ObjectBackend;
2use crate::engine::error::{VfsError, VfsResult};
3use crate::engine::types::{normalize_path, Dentry, InodeType, ObjectMeta, Timespec, VirtualStat};
4use crate::engine::vfs::VirtualFileSystem;
5use async_trait::async_trait;
6
7#[derive(Debug, Clone)]
8pub struct ObjectFsOptions {
9 pub prefix: String,
10 pub uid: u32,
11 pub gid: u32,
12 pub file_mode: u32,
13 pub dir_mode: u32,
14}
15
16impl Default for ObjectFsOptions {
17 fn default() -> Self {
18 Self {
19 prefix: String::new(),
20 uid: 0,
21 gid: 0,
22 file_mode: 0o644,
23 dir_mode: 0o755,
24 }
25 }
26}
27
28#[derive(Debug, Clone)]
29pub struct ObjectFs<B> {
30 backend: B,
31 options: ObjectFsOptions,
32}
33
34impl<B> ObjectFs<B> {
35 pub fn new(backend: B) -> Self {
36 Self::with_options(backend, ObjectFsOptions::default())
37 }
38
39 pub fn with_options(backend: B, options: ObjectFsOptions) -> Self {
40 Self { backend, options }
41 }
42
43 fn key_for(&self, path: &str) -> VfsResult<String> {
44 let normalized = normalize_path(path)?;
45 let relative = normalized.trim_start_matches('/');
46 Ok(format!("{}{}", self.options.prefix, relative))
47 }
48
49 fn dir_prefix_for(&self, path: &str) -> VfsResult<String> {
50 let mut key = self.key_for(path)?;
51 if !key.is_empty() && !key.ends_with('/') {
52 key.push('/');
53 }
54 Ok(key)
55 }
56
57 fn file_meta(&self, size: u64) -> ObjectMeta {
58 let now = Timespec::now();
59 ObjectMeta {
60 size,
61 allocated_extents: (size > 0).then_some((0, size)).into_iter().collect(),
62 atime: now,
63 mtime: now,
64 ctime: now,
65 birthtime: now,
66 mode: self.options.file_mode,
67 uid: self.options.uid,
68 gid: self.options.gid,
69 kind: InodeType::File,
70 symlink_target: None,
71 link_id: None,
72 xattrs: Default::default(),
73 }
74 }
75
76 fn dir_meta(&self) -> ObjectMeta {
77 let now = Timespec::now();
78 ObjectMeta {
79 size: 0,
80 allocated_extents: Vec::new(),
81 atime: now,
82 mtime: now,
83 ctime: now,
84 birthtime: now,
85 mode: self.options.dir_mode,
86 uid: self.options.uid,
87 gid: self.options.gid,
88 kind: InodeType::Directory,
89 symlink_target: None,
90 link_id: None,
91 xattrs: Default::default(),
92 }
93 }
94}
95
96impl<B: ObjectBackend> ObjectFs<B> {
97 async fn collect_objects_under(&self, prefix: &str) -> VfsResult<Vec<String>> {
98 let mut pending = vec![prefix.to_string()];
99 let mut objects = Vec::new();
100 while let Some(current) = pending.pop() {
101 for entry in self.backend.list(¤t).await? {
102 if entry.is_prefix {
103 pending.push(entry.name);
104 } else {
105 objects.push(entry.name);
106 }
107 }
108 }
109 Ok(objects)
110 }
111}
112
113#[async_trait]
114impl<B: ObjectBackend> VirtualFileSystem for ObjectFs<B> {
115 async fn read_file(&self, path: &str) -> VfsResult<Vec<u8>> {
116 let key = self.key_for(path)?;
117 let meta = self
118 .backend
119 .head(&key)
120 .await?
121 .ok_or_else(|| VfsError::enoent(path))?;
122 if meta.kind == InodeType::Directory {
123 return Err(VfsError::eisdir(path));
124 }
125 if meta.kind == InodeType::Symlink {
126 let target = meta.symlink_target.ok_or_else(|| VfsError::enoent(path))?;
127 return self.read_file(&target).await;
128 }
129 self.backend.get_range(&key, 0, meta.size).await
130 }
131
132 async fn read_dir(&self, path: &str) -> VfsResult<Vec<String>> {
133 Ok(self
134 .read_dir_with_types(path)
135 .await?
136 .into_iter()
137 .map(|entry| entry.name)
138 .collect())
139 }
140
141 async fn read_dir_with_types(&self, path: &str) -> VfsResult<Vec<Dentry>> {
142 let prefix = self.dir_prefix_for(path)?;
143 let entries = self.backend.list(&prefix).await?;
144 let mut result = Vec::new();
145 for entry in entries {
146 let name = entry
147 .name
148 .trim_start_matches(&prefix)
149 .trim_end_matches('/')
150 .to_string();
151 if name.is_empty() || name.contains('/') {
152 continue;
153 }
154 result.push(Dentry {
155 name,
156 ino: object_ino(&entry.name),
157 kind: if entry.is_prefix {
158 InodeType::Directory
159 } else {
160 InodeType::File
161 },
162 });
163 }
164 Ok(result)
165 }
166
167 async fn write_file(&self, path: &str, content: &[u8]) -> VfsResult<()> {
168 let key = self.key_for(path)?;
169 self.backend
170 .put(&key, content, self.file_meta(content.len() as u64))
171 .await
172 }
173
174 async fn create_dir(&self, path: &str) -> VfsResult<()> {
175 let key = self.dir_prefix_for(path)?;
176 self.backend.put(&key, &[], self.dir_meta()).await
177 }
178
179 async fn mkdir(&self, path: &str, recursive: bool) -> VfsResult<()> {
180 if !recursive {
181 return self.create_dir(path).await;
182 }
183 let normalized = normalize_path(path)?;
184 let mut current = String::new();
185 for part in normalized
186 .trim_start_matches('/')
187 .split('/')
188 .filter(|p| !p.is_empty())
189 {
190 current.push('/');
191 current.push_str(part);
192 self.create_dir(¤t).await?;
193 }
194 Ok(())
195 }
196
197 async fn exists(&self, path: &str) -> bool {
198 let Ok(key) = self.key_for(path) else {
199 return false;
200 };
201 if self.backend.head(&key).await.ok().flatten().is_some() {
202 return true;
203 }
204 let Ok(prefix) = self.dir_prefix_for(path) else {
205 return false;
206 };
207 self.backend
208 .list(&prefix)
209 .await
210 .map(|entries| !entries.is_empty())
211 .unwrap_or(false)
212 }
213
214 async fn stat(&self, path: &str) -> VfsResult<VirtualStat> {
215 let key = self.key_for(path)?;
216 if let Some(meta) = self.backend.head(&key).await? {
217 return Ok(object_stat(meta, &key));
218 }
219 let entries = self.backend.list(&self.dir_prefix_for(path)?).await?;
220 if entries.is_empty() {
221 return Err(VfsError::enoent(path));
222 }
223 Ok(object_stat(self.dir_meta(), &key))
224 }
225
226 async fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
227 self.stat(path).await
228 }
229
230 async fn remove_file(&self, path: &str) -> VfsResult<()> {
231 self.backend.delete(&self.key_for(path)?).await
232 }
233
234 async fn remove_dir(&self, path: &str) -> VfsResult<()> {
235 let prefix = self.dir_prefix_for(path)?;
236 let entries = self.backend.list(&prefix).await?;
237 if entries.iter().any(|entry| entry.name != prefix) {
238 return Err(VfsError::enotempty(path));
239 }
240 self.backend.delete(&prefix).await
241 }
242
243 async fn rename(&self, old_path: &str, new_path: &str) -> VfsResult<()> {
244 let old_key = self.key_for(old_path)?;
245 if self.backend.head(&old_key).await?.is_some() {
246 let new_key = self.key_for(new_path)?;
247 self.backend.copy(&old_key, &new_key).await?;
248 self.backend.delete(&old_key).await?;
249 return Ok(());
250 }
251 let old_prefix = self.dir_prefix_for(old_path)?;
252 let new_prefix = self.dir_prefix_for(new_path)?;
253 let objects = self.collect_objects_under(&old_prefix).await?;
254 if objects.is_empty() {
255 return Err(VfsError::enoent(old_path));
256 }
257 for key in &objects {
258 let dst = format!("{new_prefix}{}", key.trim_start_matches(&old_prefix));
259 self.backend.copy(key, &dst).await?;
260 }
261 for key in objects {
262 self.backend.delete(&key).await?;
263 }
264 Ok(())
265 }
266
267 async fn realpath(&self, path: &str) -> VfsResult<String> {
268 if !self.exists(path).await {
269 return Err(VfsError::enoent(path));
270 }
271 normalize_path(path)
272 }
273
274 async fn symlink(&self, target: &str, link_path: &str) -> VfsResult<()> {
275 let key = self.key_for(link_path)?;
276 let now = Timespec::now();
277 let meta = ObjectMeta {
278 size: 0,
279 allocated_extents: Vec::new(),
280 atime: now,
281 mtime: now,
282 ctime: now,
283 birthtime: now,
284 mode: 0o777,
285 uid: self.options.uid,
286 gid: self.options.gid,
287 kind: InodeType::Symlink,
288 symlink_target: Some(target.to_string()),
289 link_id: None,
290 xattrs: Default::default(),
291 };
292 self.backend.put(&key, &[], meta).await
293 }
294
295 async fn readlink(&self, path: &str) -> VfsResult<String> {
296 let key = self.key_for(path)?;
297 let meta = self
298 .backend
299 .head(&key)
300 .await?
301 .ok_or_else(|| VfsError::enoent(path))?;
302 if meta.kind != InodeType::Symlink {
303 return Err(VfsError::einval(format!("not a symlink: {path}")));
304 }
305 Ok(meta.symlink_target.unwrap_or_default())
306 }
307
308 async fn link(&self, _old_path: &str, _new_path: &str) -> VfsResult<()> {
309 Err(VfsError::eopnotsupp("ObjectFs does not support hard links"))
310 }
311
312 async fn chmod(&self, _path: &str, _mode: u32) -> VfsResult<()> {
313 Ok(())
314 }
315
316 async fn chown(&self, _path: &str, _uid: u32, _gid: u32) -> VfsResult<()> {
317 Ok(())
318 }
319
320 async fn utimes(&self, _path: &str, _atime_ms: u64, _mtime_ms: u64) -> VfsResult<()> {
321 Ok(())
322 }
323
324 async fn truncate(&self, path: &str, length: u64) -> VfsResult<()> {
325 let mut data = self.read_file(path).await?;
326 let length = usize::try_from(length)
327 .map_err(|_| VfsError::einval(format!("truncate length is too large: {length}")))?;
328 data.resize(length, 0);
329 self.write_file(path, &data).await
330 }
331
332 async fn pread(&self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
333 let key = self.key_for(path)?;
334 self.backend.get_range(&key, offset, length as u64).await
335 }
336
337 async fn pwrite(&self, path: &str, content: &[u8], offset: u64) -> VfsResult<()> {
338 let mut data = self.read_file(path).await?;
339 let start = usize::try_from(offset)
340 .map_err(|_| VfsError::einval(format!("pwrite offset is too large: {offset}")))?;
341 if start > data.len() {
342 data.resize(start, 0);
343 }
344 let end = start.saturating_add(content.len());
345 if end > data.len() {
346 data.resize(end, 0);
347 }
348 data[start..end].copy_from_slice(content);
349 self.write_file(path, &data).await
350 }
351
352 async fn append(&self, path: &str, content: &[u8]) -> VfsResult<u64> {
353 let mut data = self.read_file(path).await?;
354 data.extend_from_slice(content);
355 let len = data.len() as u64;
356 self.write_file(path, &data).await?;
357 Ok(len)
358 }
359}
360
361fn object_ino(key: &str) -> u64 {
362 let mut hash = 0xcbf2_9ce4_8422_2325u64;
363 for byte in key.trim_end_matches('/').as_bytes() {
364 hash ^= u64::from(*byte);
365 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
366 }
367 if hash == 0 {
368 1
369 } else {
370 hash
371 }
372}
373
374fn object_stat(meta: ObjectMeta, key: &str) -> VirtualStat {
375 let type_bits = match meta.kind {
376 InodeType::File => crate::engine::types::S_IFREG,
377 InodeType::Directory => crate::engine::types::S_IFDIR,
378 InodeType::Symlink => crate::engine::types::S_IFLNK,
379 InodeType::CharacterDevice => crate::engine::types::S_IFCHR,
380 InodeType::BlockDevice => crate::engine::types::S_IFBLK,
381 InodeType::Fifo => crate::engine::types::S_IFIFO,
382 };
383 VirtualStat {
384 mode: type_bits | (meta.mode & 0o7777),
385 size: meta.size,
386 blocks: meta.size.div_ceil(512),
387 rdev: 0,
388 is_directory: meta.kind == InodeType::Directory,
389 is_symbolic_link: meta.kind == InodeType::Symlink,
390 atime: meta.mtime,
391 mtime: meta.mtime,
392 ctime: meta.mtime,
393 birthtime: meta.mtime,
394 ino: object_ino(key),
395 nlink: 1,
396 uid: meta.uid,
397 gid: meta.gid,
398 }
399}