mneme/sync/transport/
file.rs1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4
5use crate::error::{MnemeError, Result};
6use crate::sync::protocol::MemoryChangeset;
7
8pub struct FileTransport {
10 directory: PathBuf,
11}
12
13#[derive(Debug, Clone, Default)]
15pub struct ExportStats {
16 pub memories_exported: u32,
18 pub bytes_written: u64,
20}
21
22#[derive(Debug, Clone, Default)]
24pub struct ImportStats {
25 pub memories_imported: u32,
27 pub files_processed: u32,
29}
30
31impl FileTransport {
32 pub fn new(directory: PathBuf) -> Result<Self> {
34 fs::create_dir_all(&directory)?;
35 Ok(Self { directory })
36 }
37
38 pub fn export(
40 &self,
41 project: &str,
42 changes: &[MemoryChangeset],
43 ) -> Result<(PathBuf, ExportStats)> {
44 let filename = format!("{}_{}.zst", project, chrono::Utc::now().timestamp());
45 let path = self.directory.join(&filename);
46
47 let payload = serde_json::to_vec(changes)?;
48 let compressed =
49 zstd::encode_all(std::io::Cursor::new(payload), 3).map_err(MnemeError::Io)?;
50
51 fs::write(&path, &compressed)?;
52
53 let stats = ExportStats {
54 memories_exported: changes.len() as u32,
55 bytes_written: compressed.len() as u64,
56 };
57
58 tracing::info!(
59 "exported {} memories to {} ({} bytes)",
60 stats.memories_exported,
61 path.display(),
62 stats.bytes_written
63 );
64
65 Ok((path, stats))
66 }
67
68 pub fn import_pending(
70 &self,
71 project: &str,
72 ) -> Result<(Vec<MemoryChangeset>, HashMap<String, bool>)> {
73 let mut all_changes = Vec::new();
74 let mut processed = HashMap::new();
75
76 for entry in fs::read_dir(&self.directory)? {
77 let entry = entry?;
78 let path = entry.path();
79 let name = path
80 .file_stem()
81 .and_then(|s| s.to_str())
82 .unwrap_or("")
83 .to_string();
84
85 if !name.starts_with(project)
86 || path.extension().and_then(|s| s.to_str()) != Some("zst")
87 {
88 continue;
89 }
90
91 if processed.get(&name).copied().unwrap_or(false) {
92 continue;
93 }
94
95 let compressed = fs::read(&path)?;
96 let decompressed =
97 zstd::decode_all(std::io::Cursor::new(compressed)).map_err(MnemeError::Io)?;
98 let changes: Vec<MemoryChangeset> = serde_json::from_slice(&decompressed)?;
99 all_changes.extend(changes);
100 processed.insert(name, false);
101 }
102
103 Ok((all_changes, processed))
104 }
105
106 pub fn mark_applied(&self, filename: &str) -> Result<()> {
108 let src = self.directory.join(format!("{}.zst", filename));
109 let dst = self.directory.join(format!("{}.applied", filename));
110 if src.exists() {
111 fs::rename(&src, &dst)?;
112 tracing::info!("marked applied: {}", filename);
113 }
114 Ok(())
115 }
116
117 pub fn list_pending(&self, project: &str) -> Result<Vec<PathBuf>> {
119 let mut pending = Vec::new();
120 for entry in fs::read_dir(&self.directory)? {
121 let entry = entry?;
122 let path = entry.path();
123 let name = path
124 .file_stem()
125 .and_then(|s| s.to_str())
126 .unwrap_or("")
127 .to_string();
128
129 if name.starts_with(project) && path.extension().and_then(|s| s.to_str()) == Some("zst")
130 {
131 pending.push(path);
132 }
133 }
134 Ok(pending)
135 }
136}