use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::fs::Fs;
use crate::inode::{FileContent, Inode, Metadata};
use crate::storage::BlockStorage;
use crate::time::TimeProvider;
use crate::types::InodeId;
macro_rules! inode_read {
($inode:expr) => {{
#[cfg(feature = "thread-safe")]
{
$inode.read().unwrap()
}
#[cfg(not(feature = "thread-safe"))]
{
$inode.borrow()
}
}};
}
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FsSnapshot {
pub next_inode: InodeId,
pub root_inode: InodeId,
pub inodes: Vec<InodeSnapshot>,
}
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct InodeSnapshot {
pub id: InodeId,
pub metadata: MetadataSnapshot,
pub content: FileContentSnapshot,
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MetadataSnapshot {
pub size: u64,
pub created: u64,
pub modified: u64,
pub permissions: u16,
pub is_dir: bool,
}
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum FileContentSnapshot {
File(FileDataSnapshot),
Dir(BTreeMap<String, InodeId>),
}
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FileDataSnapshot {
pub size: usize,
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub data: Vec<u8>,
}
impl From<&Metadata> for MetadataSnapshot {
fn from(m: &Metadata) -> Self {
Self {
size: m.size,
created: m.created,
modified: m.modified,
permissions: m.permissions,
is_dir: m.is_dir,
}
}
}
impl From<&MetadataSnapshot> for Metadata {
fn from(m: &MetadataSnapshot) -> Self {
Self {
size: m.size,
created: m.created,
modified: m.modified,
permissions: m.permissions,
is_dir: m.is_dir,
}
}
}
impl From<&BlockStorage> for FileDataSnapshot {
fn from(storage: &BlockStorage) -> Self {
let size = storage.size();
let mut data = vec![0u8; size];
storage.read(0, &mut data);
Self { size, data }
}
}
impl<T: TimeProvider> Fs<T> {
pub fn to_snapshot(&self) -> FsSnapshot {
let mut inodes: Vec<InodeSnapshot> = Vec::new();
self.inode_for_each(|id, inode_rc| {
let inode = inode_read!(inode_rc);
inodes.push(InodeSnapshot {
id,
metadata: MetadataSnapshot::from(&inode.metadata),
content: match &inode.content {
FileContent::File(storage) => {
FileContentSnapshot::File(FileDataSnapshot::from(storage))
}
FileContent::Dir(entries) => FileContentSnapshot::Dir(entries.clone()),
},
});
});
FsSnapshot {
next_inode: self.next_inode_value(),
root_inode: self.root_inode,
inodes,
}
}
pub fn from_snapshot(snapshot: FsSnapshot, time_provider: T) -> Self {
let fs = Self::empty_with(time_provider, snapshot.root_inode, snapshot.next_inode);
for inode_snap in snapshot.inodes {
let content = match inode_snap.content {
FileContentSnapshot::File(file_data) => {
let mut storage = BlockStorage::new();
if !file_data.data.is_empty() {
storage.write(0, &file_data.data);
}
if storage.size() != file_data.size {
storage.truncate(file_data.size);
}
FileContent::File(storage)
}
FileContentSnapshot::Dir(entries) => FileContent::Dir(entries),
};
let inode = Inode {
id: inode_snap.id,
metadata: Metadata::from(&inode_snap.metadata),
content,
};
fs.inode_insert(inode_snap.id, Fs::<T>::new_inode_ref(inode));
}
fs
}
}
#[cfg(all(test, feature = "serde"))]
mod tests {
use super::*;
use crate::time::MonotonicCounter;
#[test]
fn test_snapshot_roundtrip() {
let fs = Fs::new();
fs.mkdir("/test").unwrap();
fs.mkdir_p("/test/nested/dir").unwrap();
let fd = fs.open_path("/test/file.txt").unwrap();
fs.write(fd, b"Hello, World!").unwrap();
fs.close(fd).unwrap();
let snapshot = fs.to_snapshot();
let json = serde_json::to_string(&snapshot).unwrap();
let restored_snapshot: FsSnapshot = serde_json::from_str(&json).unwrap();
let restored_fs = Fs::from_snapshot(restored_snapshot, MonotonicCounter::new());
assert!(restored_fs.stat("/test").is_ok());
assert!(restored_fs.stat("/test/nested/dir").is_ok());
assert!(restored_fs.stat("/test/file.txt").is_ok());
let fd = restored_fs
.open_path_with_flags("/test/file.txt", crate::types::O_RDONLY)
.unwrap();
let mut buf = [0u8; 20];
let n = restored_fs.read(fd, &mut buf).unwrap();
assert_eq!(&buf[..n], b"Hello, World!");
}
#[test]
fn test_snapshot_json_format() {
let fs = Fs::new();
fs.mkdir("/data").unwrap();
let fd = fs.open_path("/data/test.txt").unwrap();
fs.write(fd, b"test content").unwrap();
fs.close(fd).unwrap();
let snapshot = fs.to_snapshot();
let json = serde_json::to_string_pretty(&snapshot).unwrap();
assert!(json.contains("\"next_inode\""));
assert!(json.contains("\"inodes\""));
}
}