Skip to main content

fs_core/
snapshot.rs

1//! Snapshot serialization/deserialization for filesystem persistence
2//!
3//! This module provides types and functions for serializing the filesystem
4//! state to a snapshot format that can be stored externally (e.g., S3).
5
6use alloc::{collections::BTreeMap, string::String, vec::Vec};
7
8#[cfg(feature = "thread-safe")]
9use std::sync::{Arc, RwLock};
10
11#[cfg(all(feature = "std", not(feature = "thread-safe")))]
12use std::cell::RefCell;
13#[cfg(all(feature = "std", not(feature = "thread-safe")))]
14use std::rc::Rc;
15
16#[cfg(not(feature = "std"))]
17use alloc::rc::Rc;
18#[cfg(not(feature = "std"))]
19use core::cell::RefCell;
20
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24use crate::fs::{Fs, InodeRef};
25use crate::inode::{FileContent, Inode, Metadata};
26use crate::storage::BlockStorage;
27use crate::time::TimeProvider;
28use crate::types::InodeId;
29
30/// Macro for read access to inode (uses RwLock::read for thread-safe, RefCell::borrow otherwise)
31macro_rules! inode_read {
32    ($inode:expr) => {{
33        #[cfg(feature = "thread-safe")]
34        {
35            $inode.read().unwrap()
36        }
37        #[cfg(not(feature = "thread-safe"))]
38        {
39            $inode.borrow()
40        }
41    }};
42}
43
44/// Helper to create a new InodeRef
45#[cfg(feature = "thread-safe")]
46fn new_inode_ref(inode: Inode) -> InodeRef {
47    Arc::new(RwLock::new(inode))
48}
49
50#[cfg(not(feature = "thread-safe"))]
51fn new_inode_ref(inode: Inode) -> InodeRef {
52    Rc::new(RefCell::new(inode))
53}
54
55/// Snapshot of the entire filesystem state
56#[derive(Clone)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58pub struct FsSnapshot {
59    /// Next inode ID to allocate
60    pub next_inode: InodeId,
61    /// Root inode ID
62    pub root_inode: InodeId,
63    /// All inodes in the filesystem
64    pub inodes: Vec<InodeSnapshot>,
65}
66
67/// Snapshot of a single inode
68#[derive(Clone)]
69#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
70pub struct InodeSnapshot {
71    pub id: InodeId,
72    pub metadata: MetadataSnapshot,
73    pub content: FileContentSnapshot,
74}
75
76/// Snapshot of file/directory metadata
77#[derive(Clone, Copy)]
78#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
79pub struct MetadataSnapshot {
80    pub size: u64,
81    pub created: u64,
82    pub modified: u64,
83    pub permissions: u16,
84    pub is_dir: bool,
85}
86
87/// Snapshot of file content
88#[derive(Clone)]
89#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
90pub enum FileContentSnapshot {
91    /// Regular file with its data
92    File(FileDataSnapshot),
93    /// Directory with name -> inode_id mappings
94    Dir(BTreeMap<String, InodeId>),
95}
96
97/// Snapshot of file data (block storage)
98#[derive(Clone)]
99#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
100pub struct FileDataSnapshot {
101    /// File size in bytes
102    pub size: usize,
103    /// File data as a contiguous byte array
104    /// (sparse blocks are materialized as zeros)
105    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
106    pub data: Vec<u8>,
107}
108
109impl From<&Metadata> for MetadataSnapshot {
110    fn from(m: &Metadata) -> Self {
111        Self {
112            size: m.size,
113            created: m.created,
114            modified: m.modified,
115            permissions: m.permissions,
116            is_dir: m.is_dir,
117        }
118    }
119}
120
121impl From<&MetadataSnapshot> for Metadata {
122    fn from(m: &MetadataSnapshot) -> Self {
123        Self {
124            size: m.size,
125            created: m.created,
126            modified: m.modified,
127            permissions: m.permissions,
128            is_dir: m.is_dir,
129        }
130    }
131}
132
133impl From<&BlockStorage> for FileDataSnapshot {
134    fn from(storage: &BlockStorage) -> Self {
135        let size = storage.size();
136        let mut data = vec![0u8; size];
137        storage.read(0, &mut data);
138        Self { size, data }
139    }
140}
141
142impl<T: TimeProvider> Fs<T> {
143    /// Create a snapshot of the current filesystem state
144    #[cfg(feature = "thread-safe")]
145    pub fn to_snapshot(&self) -> FsSnapshot {
146        use std::sync::atomic::Ordering;
147
148        let inodes: Vec<InodeSnapshot> = self
149            .inode_table
150            .iter()
151            .map(|entry| {
152                let id = *entry.key();
153                let inode_rc = entry.value();
154                let inode = inode_read!(inode_rc);
155                InodeSnapshot {
156                    id,
157                    metadata: MetadataSnapshot::from(&inode.metadata),
158                    content: match &inode.content {
159                        FileContent::File(storage) => {
160                            FileContentSnapshot::File(FileDataSnapshot::from(storage))
161                        }
162                        FileContent::Dir(entries) => FileContentSnapshot::Dir(entries.clone()),
163                    },
164                }
165            })
166            .collect();
167
168        FsSnapshot {
169            next_inode: self.next_inode.load(Ordering::Relaxed),
170            root_inode: self.root_inode,
171            inodes,
172        }
173    }
174
175    #[cfg(all(feature = "std", not(feature = "thread-safe")))]
176    pub fn to_snapshot(&self) -> FsSnapshot {
177        let inodes: Vec<InodeSnapshot> = self
178            .inode_table
179            .iter()
180            .map(|(&id, inode_rc)| {
181                let inode = inode_read!(inode_rc);
182                InodeSnapshot {
183                    id,
184                    metadata: MetadataSnapshot::from(&inode.metadata),
185                    content: match &inode.content {
186                        FileContent::File(storage) => {
187                            FileContentSnapshot::File(FileDataSnapshot::from(storage))
188                        }
189                        FileContent::Dir(entries) => FileContentSnapshot::Dir(entries.clone()),
190                    },
191                }
192            })
193            .collect();
194
195        FsSnapshot {
196            next_inode: self.next_inode,
197            root_inode: self.root_inode,
198            inodes,
199        }
200    }
201
202    #[cfg(not(feature = "std"))]
203    pub fn to_snapshot(&self) -> FsSnapshot {
204        let inodes: Vec<InodeSnapshot> = self
205            .inode_table
206            .iter()
207            .map(|(&id, inode_rc)| {
208                let inode = inode_read!(inode_rc);
209                InodeSnapshot {
210                    id,
211                    metadata: MetadataSnapshot::from(&inode.metadata),
212                    content: match &inode.content {
213                        FileContent::File(storage) => {
214                            FileContentSnapshot::File(FileDataSnapshot::from(storage))
215                        }
216                        FileContent::Dir(entries) => FileContentSnapshot::Dir(entries.clone()),
217                    },
218                }
219            })
220            .collect();
221
222        FsSnapshot {
223            next_inode: self.next_inode,
224            root_inode: self.root_inode,
225            inodes,
226        }
227    }
228
229    /// Restore filesystem state from a snapshot
230    #[cfg(feature = "thread-safe")]
231    pub fn from_snapshot(snapshot: FsSnapshot, time_provider: T) -> Self {
232        use dashmap::DashMap;
233        use std::sync::atomic::AtomicU32;
234        use std::sync::atomic::AtomicU64;
235
236        let inode_table: DashMap<InodeId, InodeRef> = DashMap::new();
237
238        for inode_snap in snapshot.inodes {
239            let content = match inode_snap.content {
240                FileContentSnapshot::File(file_data) => {
241                    let mut storage = BlockStorage::new();
242                    if !file_data.data.is_empty() {
243                        storage.write(0, &file_data.data);
244                    }
245                    // Ensure size is correct (handles sparse files)
246                    if storage.size() != file_data.size {
247                        storage.truncate(file_data.size);
248                    }
249                    FileContent::File(storage)
250                }
251                FileContentSnapshot::Dir(entries) => FileContent::Dir(entries),
252            };
253
254            let inode = Inode {
255                id: inode_snap.id,
256                metadata: Metadata::from(&inode_snap.metadata),
257                content,
258            };
259
260            inode_table.insert(inode_snap.id, new_inode_ref(inode));
261        }
262
263        Self {
264            next_inode: AtomicU64::new(snapshot.next_inode),
265            next_fd: AtomicU32::new(3),
266            fd_table: DashMap::new(),
267            inode_table,
268            root_inode: snapshot.root_inode,
269            time_provider,
270        }
271    }
272
273    #[cfg(all(feature = "std", not(feature = "thread-safe")))]
274    pub fn from_snapshot(snapshot: FsSnapshot, time_provider: T) -> Self {
275        use std::collections::HashMap;
276
277        let mut inode_table: HashMap<InodeId, InodeRef> = HashMap::new();
278
279        for inode_snap in snapshot.inodes {
280            let content = match inode_snap.content {
281                FileContentSnapshot::File(file_data) => {
282                    let mut storage = BlockStorage::new();
283                    if !file_data.data.is_empty() {
284                        storage.write(0, &file_data.data);
285                    }
286                    // Ensure size is correct (handles sparse files)
287                    if storage.size() != file_data.size {
288                        storage.truncate(file_data.size);
289                    }
290                    FileContent::File(storage)
291                }
292                FileContentSnapshot::Dir(entries) => FileContent::Dir(entries),
293            };
294
295            let inode = Inode {
296                id: inode_snap.id,
297                metadata: Metadata::from(&inode_snap.metadata),
298                content,
299            };
300
301            inode_table.insert(inode_snap.id, new_inode_ref(inode));
302        }
303
304        Self {
305            next_inode: snapshot.next_inode,
306            fd_table: HashMap::new(), // Start with empty fd_table
307            inode_table,
308            root_inode: snapshot.root_inode,
309            time_provider,
310        }
311    }
312
313    #[cfg(not(feature = "std"))]
314    pub fn from_snapshot(snapshot: FsSnapshot, time_provider: T) -> Self {
315        let mut inode_table: BTreeMap<InodeId, InodeRef> = BTreeMap::new();
316
317        for inode_snap in snapshot.inodes {
318            let content = match inode_snap.content {
319                FileContentSnapshot::File(file_data) => {
320                    let mut storage = BlockStorage::new();
321                    if !file_data.data.is_empty() {
322                        storage.write(0, &file_data.data);
323                    }
324                    // Ensure size is correct (handles sparse files)
325                    if storage.size() != file_data.size {
326                        storage.truncate(file_data.size);
327                    }
328                    FileContent::File(storage)
329                }
330                FileContentSnapshot::Dir(entries) => FileContent::Dir(entries),
331            };
332
333            let inode = Inode {
334                id: inode_snap.id,
335                metadata: Metadata::from(&inode_snap.metadata),
336                content,
337            };
338
339            inode_table.insert(inode_snap.id, new_inode_ref(inode));
340        }
341
342        Self {
343            next_inode: snapshot.next_inode,
344            fd_table: BTreeMap::new(), // Start with empty fd_table
345            inode_table,
346            root_inode: snapshot.root_inode,
347            time_provider,
348        }
349    }
350}
351
352#[cfg(all(test, feature = "serde"))]
353mod tests {
354    use super::*;
355    use crate::time::MonotonicCounter;
356
357    #[test]
358    fn test_snapshot_roundtrip() {
359        // Use mut for single-threaded version compatibility
360        let fs = Fs::new();
361
362        // Create some files and directories
363        fs.mkdir("/test").unwrap();
364        fs.mkdir_p("/test/nested/dir").unwrap();
365
366        let fd = fs.open_path("/test/file.txt").unwrap();
367        fs.write(fd, b"Hello, World!").unwrap();
368        fs.close(fd).unwrap();
369
370        // Create snapshot
371        let snapshot = fs.to_snapshot();
372
373        // Serialize to JSON
374        let json = serde_json::to_string(&snapshot).unwrap();
375
376        // Deserialize
377        let restored_snapshot: FsSnapshot = serde_json::from_str(&json).unwrap();
378
379        // Restore filesystem
380        let restored_fs = Fs::from_snapshot(restored_snapshot, MonotonicCounter::new());
381
382        // Verify structure
383        assert!(restored_fs.stat("/test").is_ok());
384        assert!(restored_fs.stat("/test/nested/dir").is_ok());
385        assert!(restored_fs.stat("/test/file.txt").is_ok());
386
387        // Verify file content
388        let fd = restored_fs
389            .open_path_with_flags("/test/file.txt", crate::types::O_RDONLY)
390            .unwrap();
391        let mut buf = [0u8; 20];
392        let n = restored_fs.read(fd, &mut buf).unwrap();
393        assert_eq!(&buf[..n], b"Hello, World!");
394    }
395
396    #[test]
397    fn test_snapshot_json_format() {
398        // Use mut for single-threaded version compatibility
399        let fs = Fs::new();
400        fs.mkdir("/data").unwrap();
401        let fd = fs.open_path("/data/test.txt").unwrap();
402        fs.write(fd, b"test content").unwrap();
403        fs.close(fd).unwrap();
404
405        let snapshot = fs.to_snapshot();
406        let json = serde_json::to_string_pretty(&snapshot).unwrap();
407
408        // Verify JSON is readable
409        assert!(json.contains("\"next_inode\""));
410        assert!(json.contains("\"inodes\""));
411    }
412}