use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use crate::error::{MnemeError, Result};
use crate::sync::protocol::MemoryChangeset;
pub struct FileTransport {
directory: PathBuf,
}
#[derive(Debug, Clone, Default)]
pub struct ExportStats {
pub memories_exported: u32,
pub bytes_written: u64,
}
#[derive(Debug, Clone, Default)]
pub struct ImportStats {
pub memories_imported: u32,
pub files_processed: u32,
}
impl FileTransport {
pub fn new(directory: PathBuf) -> Result<Self> {
fs::create_dir_all(&directory)?;
Ok(Self { directory })
}
pub fn export(
&self,
project: &str,
changes: &[MemoryChangeset],
) -> Result<(PathBuf, ExportStats)> {
let filename = format!("{}_{}.zst", project, chrono::Utc::now().timestamp());
let path = self.directory.join(&filename);
let payload = serde_json::to_vec(changes)?;
let compressed =
zstd::encode_all(std::io::Cursor::new(payload), 3).map_err(MnemeError::Io)?;
fs::write(&path, &compressed)?;
let stats = ExportStats {
memories_exported: changes.len() as u32,
bytes_written: compressed.len() as u64,
};
tracing::info!(
"exported {} memories to {} ({} bytes)",
stats.memories_exported,
path.display(),
stats.bytes_written
);
Ok((path, stats))
}
pub fn import_pending(
&self,
project: &str,
) -> Result<(Vec<MemoryChangeset>, HashMap<String, bool>)> {
let mut all_changes = Vec::new();
let mut processed = HashMap::new();
for entry in fs::read_dir(&self.directory)? {
let entry = entry?;
let path = entry.path();
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if !name.starts_with(project)
|| path.extension().and_then(|s| s.to_str()) != Some("zst")
{
continue;
}
if processed.get(&name).copied().unwrap_or(false) {
continue;
}
let compressed = fs::read(&path)?;
let decompressed =
zstd::decode_all(std::io::Cursor::new(compressed)).map_err(MnemeError::Io)?;
let changes: Vec<MemoryChangeset> = serde_json::from_slice(&decompressed)?;
all_changes.extend(changes);
processed.insert(name, false);
}
Ok((all_changes, processed))
}
pub fn mark_applied(&self, filename: &str) -> Result<()> {
let src = self.directory.join(format!("{}.zst", filename));
let dst = self.directory.join(format!("{}.applied", filename));
if src.exists() {
fs::rename(&src, &dst)?;
tracing::info!("marked applied: {}", filename);
}
Ok(())
}
pub fn list_pending(&self, project: &str) -> Result<Vec<PathBuf>> {
let mut pending = Vec::new();
for entry in fs::read_dir(&self.directory)? {
let entry = entry?;
let path = entry.path();
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if name.starts_with(project) && path.extension().and_then(|s| s.to_str()) == Some("zst")
{
pending.push(path);
}
}
Ok(pending)
}
}