Skip to main content

mneme/sync/transport/
file.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::PathBuf;
4
5use crate::error::{MnemeError, Result};
6use crate::sync::protocol::MemoryChangeset;
7
8/// Transporte basado en archivos para sincronizacion.
9pub struct FileTransport {
10    directory: PathBuf,
11}
12
13/// Estado de exportacion de un proyecto.
14#[derive(Debug, Clone, Default)]
15pub struct ExportStats {
16    /// Cantidad de memorias exportadas.
17    pub memories_exported: u32,
18    /// Bytes escritos al archivo.
19    pub bytes_written: u64,
20}
21
22/// Estado de importacion de un archivo.
23#[derive(Debug, Clone, Default)]
24pub struct ImportStats {
25    /// Cantidad de memorias importadas.
26    pub memories_imported: u32,
27    /// Cantidad de archivos procesados.
28    pub files_processed: u32,
29}
30
31impl FileTransport {
32    /// Crea un nuevo FileTransport.
33    pub fn new(directory: PathBuf) -> Result<Self> {
34        fs::create_dir_all(&directory)?;
35        Ok(Self { directory })
36    }
37
38    /// Exporta cambios a un archivo comprimido con zstd.
39    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    /// Importa cambios pendientes desde archivos en el directorio.
69    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    /// Marca un archivo como aplicado (renombra a .applied).
107    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    /// Lista archivos pendientes de importar.
118    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}