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::Vec};
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11use crate::fs::Fs;
12use crate::inode::{FileContent, Inode, Metadata};
13use crate::storage::BlockStorage;
14use crate::time::TimeProvider;
15use crate::types::InodeId;
16
17// We use the same inode-read macro shape that `fs.rs` uses, so that
18// snapshot iteration works identically across both backends.
19macro_rules! inode_read {
20    ($inode:expr) => {{
21        #[cfg(feature = "thread-safe")]
22        {
23            $inode.read().unwrap()
24        }
25        #[cfg(not(feature = "thread-safe"))]
26        {
27            $inode.borrow()
28        }
29    }};
30}
31
32/// Snapshot of the entire filesystem state.
33#[derive(Clone)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35pub struct FsSnapshot {
36    /// Next inode ID to allocate.
37    pub next_inode: InodeId,
38    /// Root inode ID.
39    pub root_inode: InodeId,
40    /// All inodes in the filesystem.
41    pub inodes: Vec<InodeSnapshot>,
42}
43
44/// Snapshot of a single inode.
45#[derive(Clone)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47pub struct InodeSnapshot {
48    pub id: InodeId,
49    pub metadata: MetadataSnapshot,
50    pub content: FileContentSnapshot,
51}
52
53/// Snapshot of file/directory metadata.
54#[derive(Clone, Copy)]
55#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
56pub struct MetadataSnapshot {
57    pub size: u64,
58    pub created: u64,
59    pub modified: u64,
60    pub permissions: u16,
61    pub is_dir: bool,
62}
63
64/// Snapshot of file content.
65#[derive(Clone)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67pub enum FileContentSnapshot {
68    /// Regular file with its data.
69    File(FileDataSnapshot),
70    /// Directory with name -> inode_id mappings.
71    Dir(BTreeMap<String, InodeId>),
72}
73
74/// Snapshot of file data (block storage).
75#[derive(Clone)]
76#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
77pub struct FileDataSnapshot {
78    /// File size in bytes.
79    pub size: usize,
80    /// File data as a contiguous byte array.
81    /// (sparse blocks are materialized as zeros)
82    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
83    pub data: Vec<u8>,
84}
85
86impl From<&Metadata> for MetadataSnapshot {
87    fn from(m: &Metadata) -> Self {
88        Self {
89            size: m.size,
90            created: m.created,
91            modified: m.modified,
92            permissions: m.permissions,
93            is_dir: m.is_dir,
94        }
95    }
96}
97
98impl From<&MetadataSnapshot> for Metadata {
99    fn from(m: &MetadataSnapshot) -> Self {
100        Self {
101            size: m.size,
102            created: m.created,
103            modified: m.modified,
104            permissions: m.permissions,
105            is_dir: m.is_dir,
106        }
107    }
108}
109
110impl From<&BlockStorage> for FileDataSnapshot {
111    fn from(storage: &BlockStorage) -> Self {
112        let size = storage.size();
113        let mut data = vec![0u8; size];
114        storage.read(0, &mut data);
115        Self { size, data }
116    }
117}
118
119impl<T: TimeProvider> Fs<T> {
120    /// Create a snapshot of the current filesystem state.
121    pub fn to_snapshot(&self) -> FsSnapshot {
122        let mut inodes: Vec<InodeSnapshot> = Vec::new();
123        self.inode_for_each(|id, inode_rc| {
124            let inode = inode_read!(inode_rc);
125            inodes.push(InodeSnapshot {
126                id,
127                metadata: MetadataSnapshot::from(&inode.metadata),
128                content: match &inode.content {
129                    FileContent::File(storage) => {
130                        FileContentSnapshot::File(FileDataSnapshot::from(storage))
131                    }
132                    FileContent::Dir(entries) => FileContentSnapshot::Dir(entries.clone()),
133                },
134            });
135        });
136
137        FsSnapshot {
138            next_inode: self.next_inode_value(),
139            root_inode: self.root_inode,
140            inodes,
141        }
142    }
143
144    /// Restore filesystem state from a snapshot.
145    pub fn from_snapshot(snapshot: FsSnapshot, time_provider: T) -> Self {
146        let fs = Self::empty_with(time_provider, snapshot.root_inode, snapshot.next_inode);
147
148        for inode_snap in snapshot.inodes {
149            let content = match inode_snap.content {
150                FileContentSnapshot::File(file_data) => {
151                    let mut storage = BlockStorage::new();
152                    if !file_data.data.is_empty() {
153                        storage.write(0, &file_data.data);
154                    }
155                    // Ensure size is correct (handles sparse files)
156                    if storage.size() != file_data.size {
157                        storage.truncate(file_data.size);
158                    }
159                    FileContent::File(storage)
160                }
161                FileContentSnapshot::Dir(entries) => FileContent::Dir(entries),
162            };
163
164            let inode = Inode {
165                id: inode_snap.id,
166                metadata: Metadata::from(&inode_snap.metadata),
167                content,
168            };
169
170            fs.inode_insert(inode_snap.id, Fs::<T>::new_inode_ref(inode));
171        }
172
173        fs
174    }
175}
176
177#[cfg(all(test, feature = "serde"))]
178mod tests {
179    use super::*;
180    use crate::time::MonotonicCounter;
181
182    #[test]
183    fn test_snapshot_roundtrip() {
184        let fs = Fs::new();
185
186        // Create some files and directories
187        fs.mkdir("/test").unwrap();
188        fs.mkdir_p("/test/nested/dir").unwrap();
189
190        let fd = fs.open_path("/test/file.txt").unwrap();
191        fs.write(fd, b"Hello, World!").unwrap();
192        fs.close(fd).unwrap();
193
194        // Create snapshot
195        let snapshot = fs.to_snapshot();
196
197        // Serialize to JSON
198        let json = serde_json::to_string(&snapshot).unwrap();
199
200        // Deserialize
201        let restored_snapshot: FsSnapshot = serde_json::from_str(&json).unwrap();
202
203        // Restore filesystem
204        let restored_fs = Fs::from_snapshot(restored_snapshot, MonotonicCounter::new());
205
206        // Verify structure
207        assert!(restored_fs.stat("/test").is_ok());
208        assert!(restored_fs.stat("/test/nested/dir").is_ok());
209        assert!(restored_fs.stat("/test/file.txt").is_ok());
210
211        // Verify file content
212        let fd = restored_fs
213            .open_path_with_flags("/test/file.txt", crate::types::O_RDONLY)
214            .unwrap();
215        let mut buf = [0u8; 20];
216        let n = restored_fs.read(fd, &mut buf).unwrap();
217        assert_eq!(&buf[..n], b"Hello, World!");
218    }
219
220    #[test]
221    fn test_snapshot_json_format() {
222        let fs = Fs::new();
223        fs.mkdir("/data").unwrap();
224        let fd = fs.open_path("/data/test.txt").unwrap();
225        fs.write(fd, b"test content").unwrap();
226        fs.close(fd).unwrap();
227
228        let snapshot = fs.to_snapshot();
229        let json = serde_json::to_string_pretty(&snapshot).unwrap();
230
231        // Verify JSON is readable
232        assert!(json.contains("\"next_inode\""));
233        assert!(json.contains("\"inodes\""));
234    }
235}