1use 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
17macro_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#[derive(Clone)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35pub struct FsSnapshot {
36 pub next_inode: InodeId,
38 pub root_inode: InodeId,
40 pub inodes: Vec<InodeSnapshot>,
42}
43
44#[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#[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#[derive(Clone)]
66#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
67pub enum FileContentSnapshot {
68 File(FileDataSnapshot),
70 Dir(BTreeMap<String, InodeId>),
72}
73
74#[derive(Clone)]
76#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
77pub struct FileDataSnapshot {
78 pub size: usize,
80 #[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 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 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 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 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 let snapshot = fs.to_snapshot();
196
197 let json = serde_json::to_string(&snapshot).unwrap();
199
200 let restored_snapshot: FsSnapshot = serde_json::from_str(&json).unwrap();
202
203 let restored_fs = Fs::from_snapshot(restored_snapshot, MonotonicCounter::new());
205
206 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 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 assert!(json.contains("\"next_inode\""));
233 assert!(json.contains("\"inodes\""));
234 }
235}