Skip to main content

sqlite_graphrag/commands/
sync_safe_copy.rs

1use crate::errors::AppError;
2use crate::i18n::{erros, validacao};
3use crate::output;
4use crate::paths::AppPaths;
5use crate::storage::connection::open_rw;
6use serde::Serialize;
7
8#[derive(clap::Args)]
9pub struct SyncSafeCopyArgs {
10    /// Caminho do arquivo snapshot. Aceita aliases `--to` e `--output` para compatibilidade com doc bilíngue.
11    #[arg(long, alias = "to", alias = "output")]
12    pub dest: std::path::PathBuf,
13    #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
14    pub json: bool,
15    /// Formato de saída: "json" ou "text". JSON é sempre emitido no stdout independente do valor.
16    #[arg(long, value_parser = ["json", "text"], hide = true)]
17    pub format: Option<String>,
18    #[arg(long, env = "SQLITE_GRAPHRAG_DB_PATH")]
19    pub db: Option<String>,
20}
21
22#[derive(Serialize)]
23struct SyncSafeCopyResponse {
24    source_db_path: String,
25    dest_path: String,
26    bytes_copied: u64,
27    status: String,
28    /// Tempo total de execução em milissegundos desde início do handler até serialização.
29    elapsed_ms: u64,
30}
31
32pub fn run(args: SyncSafeCopyArgs) -> Result<(), AppError> {
33    let inicio = std::time::Instant::now();
34    let _ = args.format; // --format é no-op; JSON sempre emitido no stdout
35    let paths = AppPaths::resolve(args.db.as_deref())?;
36
37    if !paths.db.exists() {
38        return Err(AppError::NotFound(erros::banco_nao_encontrado(
39            &paths.db.display().to_string(),
40        )));
41    }
42
43    if args.dest == paths.db {
44        return Err(AppError::Validation(validacao::sync_destino_igual_fonte()));
45    }
46
47    if let Some(parent) = args.dest.parent() {
48        std::fs::create_dir_all(parent)?;
49    }
50
51    let conn = open_rw(&paths.db)?;
52    conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
53    drop(conn);
54
55    let bytes_copied = std::fs::copy(&paths.db, &args.dest)?;
56
57    // Aplica permissões 600 no snapshot em Unix para evitar vazamento em Dropbox/NFS compartilhado.
58    #[cfg(unix)]
59    {
60        use std::os::unix::fs::PermissionsExt;
61        let mut perms = std::fs::metadata(&args.dest)?.permissions();
62        perms.set_mode(0o600);
63        std::fs::set_permissions(&args.dest, perms)?;
64    }
65
66    output::emit_json(&SyncSafeCopyResponse {
67        source_db_path: paths.db.display().to_string(),
68        dest_path: args.dest.display().to_string(),
69        bytes_copied,
70        status: "ok".to_string(),
71        elapsed_ms: inicio.elapsed().as_millis() as u64,
72    })?;
73
74    Ok(())
75}
76
77#[cfg(test)]
78mod testes {
79    use super::*;
80
81    #[test]
82    fn sync_safe_copy_response_serializa_todos_campos() {
83        let resp = SyncSafeCopyResponse {
84            source_db_path: "/home/user/.local/share/sqlite-graphrag/db.sqlite".to_string(),
85            dest_path: "/tmp/backup.sqlite".to_string(),
86            bytes_copied: 16384,
87            status: "ok".to_string(),
88            elapsed_ms: 12,
89        };
90        let json = serde_json::to_value(&resp).expect("serialização falhou");
91        assert_eq!(
92            json["source_db_path"],
93            "/home/user/.local/share/sqlite-graphrag/db.sqlite"
94        );
95        assert_eq!(json["dest_path"], "/tmp/backup.sqlite");
96        assert_eq!(json["bytes_copied"], 16384u64);
97        assert_eq!(json["status"], "ok");
98        assert_eq!(json["elapsed_ms"], 12u64);
99    }
100
101    #[test]
102    fn sync_safe_copy_rejeita_dest_igual_source() {
103        let db_path = std::path::PathBuf::from("/tmp/mesmo.sqlite");
104        let args = SyncSafeCopyArgs {
105            dest: db_path.clone(),
106            json: false,
107            format: None,
108            db: Some("/tmp/mesmo.sqlite".to_string()),
109        };
110        // Simula resolução manual do caminho — valida lógica de rejeição
111        let resultado = if args.dest == std::path::PathBuf::from(args.db.as_deref().unwrap_or("")) {
112            Err(AppError::Validation(
113                "destination path must differ from the source database path".to_string(),
114            ))
115        } else {
116            Ok(())
117        };
118        assert!(resultado.is_err(), "deve rejeitar dest igual ao source");
119        if let Err(AppError::Validation(msg)) = resultado {
120            assert!(msg.contains("destination path must differ"));
121        }
122    }
123
124    #[test]
125    fn sync_safe_copy_response_status_ok() {
126        let resp = SyncSafeCopyResponse {
127            source_db_path: "/data/db.sqlite".to_string(),
128            dest_path: "/backup/db.sqlite".to_string(),
129            bytes_copied: 0,
130            status: "ok".to_string(),
131            elapsed_ms: 0,
132        };
133        let json = serde_json::to_value(&resp).expect("serialização falhou");
134        assert_eq!(json["status"], "ok");
135    }
136
137    #[test]
138    fn sync_safe_copy_response_bytes_copied_zero_valido() {
139        let resp = SyncSafeCopyResponse {
140            source_db_path: "/data/db.sqlite".to_string(),
141            dest_path: "/backup/db.sqlite".to_string(),
142            bytes_copied: 0,
143            status: "ok".to_string(),
144            elapsed_ms: 1,
145        };
146        let json = serde_json::to_value(&resp).expect("serialização falhou");
147        assert_eq!(json["bytes_copied"], 0u64);
148        assert_eq!(json["elapsed_ms"], 1u64);
149    }
150}