Skip to main content

vfs/engine/engines/
object.rs

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(&current).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(&current).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}