use oxiarc_archive::zip::{ZipReader, ZipWriter};
use std::path::Path;
pub async fn backup_fs_dir(src_dir: &Path, dest_archive: &Path) -> anyhow::Result<()> {
let src_dir = src_dir.to_path_buf();
let dest_archive = dest_archive.to_path_buf();
tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let mut entries: Vec<(std::path::PathBuf, std::path::PathBuf)> = Vec::new();
for entry in walkdir::WalkDir::new(&src_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let abs = entry.path().to_path_buf();
let rel = abs
.strip_prefix(&src_dir)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| abs.clone());
entries.push((abs, rel));
}
if let Some(parent) = dest_archive.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::File::create(&dest_archive)?;
let mut writer = ZipWriter::new(file);
for (abs_path, rel_path) in entries {
let data = std::fs::read(&abs_path)?;
let rel_str = rel_path.to_string_lossy();
writer
.add_file(rel_str.as_ref(), &data)
.map_err(|e| anyhow::anyhow!("ZipWriter::add_file failed: {}", e))?;
}
writer
.finish()
.map_err(|e| anyhow::anyhow!("ZipWriter::finish failed: {}", e))?;
tracing::info!(
"Backup complete: {} → {}",
src_dir.display(),
dest_archive.display()
);
Ok(())
})
.await
.map_err(|e| anyhow::anyhow!("spawn_blocking failed: {}", e))?
}
pub async fn restore_fs_dir(src_archive: &Path, dest_dir: &Path) -> anyhow::Result<()> {
let src_archive = src_archive.to_path_buf();
let dest_dir = dest_dir.to_path_buf();
tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
std::fs::create_dir_all(&dest_dir)?;
let file = std::fs::File::open(&src_archive)?;
let mut reader =
ZipReader::new(file).map_err(|e| anyhow::anyhow!("ZipReader::new failed: {}", e))?;
let entries = reader.entries().to_vec();
for entry in entries {
let dest_path = dest_dir.join(&entry.name);
if let Some(parent) = dest_path.parent() {
std::fs::create_dir_all(parent)?;
}
let data = reader
.extract(&entry)
.map_err(|e| anyhow::anyhow!("ZipReader::extract failed: {}", e))?;
std::fs::write(&dest_path, &data)?;
}
tracing::info!(
"Restore complete: {} → {}",
src_archive.display(),
dest_dir.display()
);
Ok(())
})
.await
.map_err(|e| anyhow::anyhow!("spawn_blocking failed: {}", e))?
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_backup_restore_roundtrip() {
let src_dir = tempfile::tempdir().expect("src tempdir");
let dest_dir = tempfile::tempdir().expect("dest tempdir");
let archive_dir = tempfile::tempdir().expect("archive tempdir");
let archive_path = archive_dir.path().join("backup.zip");
tokio::fs::create_dir_all(src_dir.path().join("mailboxes/inbox/new"))
.await
.expect("create_dir_all should succeed");
tokio::fs::write(
src_dir.path().join("mailboxes/inbox/new/msg1"),
b"message 1",
)
.await
.expect("write msg1 should succeed");
tokio::fs::write(
src_dir.path().join("mailboxes/inbox/new/msg2"),
b"message 2",
)
.await
.expect("write msg2 should succeed");
backup_fs_dir(src_dir.path(), &archive_path)
.await
.expect("backup should succeed");
assert!(archive_path.exists(), "Archive file should exist");
restore_fs_dir(&archive_path, dest_dir.path())
.await
.expect("restore should succeed");
let restored1 = tokio::fs::read(dest_dir.path().join("mailboxes/inbox/new/msg1"))
.await
.expect("msg1 should exist after restore");
assert_eq!(restored1, b"message 1");
let restored2 = tokio::fs::read(dest_dir.path().join("mailboxes/inbox/new/msg2"))
.await
.expect("msg2 should exist after restore");
assert_eq!(restored2, b"message 2");
}
}