1use 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
30macro_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#[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#[derive(Clone)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58pub struct FsSnapshot {
59 pub next_inode: InodeId,
61 pub root_inode: InodeId,
63 pub inodes: Vec<InodeSnapshot>,
65}
66
67#[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#[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#[derive(Clone)]
89#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
90pub enum FileContentSnapshot {
91 File(FileDataSnapshot),
93 Dir(BTreeMap<String, InodeId>),
95}
96
97#[derive(Clone)]
99#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
100pub struct FileDataSnapshot {
101 pub size: usize,
103 #[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 #[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 #[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 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 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(), 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 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(), 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 let fs = Fs::new();
361
362 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 let snapshot = fs.to_snapshot();
372
373 let json = serde_json::to_string(&snapshot).unwrap();
375
376 let restored_snapshot: FsSnapshot = serde_json::from_str(&json).unwrap();
378
379 let restored_fs = Fs::from_snapshot(restored_snapshot, MonotonicCounter::new());
381
382 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 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 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 assert!(json.contains("\"next_inode\""));
410 assert!(json.contains("\"inodes\""));
411 }
412}