Skip to main content

lc/sync/
sync.rs

1//! Sync command handlers for configuration synchronization
2
3use anyhow::Result;
4use colored::*;
5
6/// Configuration file structure for sync operations
7#[derive(Debug, Clone)]
8pub struct ConfigFile {
9    pub name: String,
10    pub content: Vec<u8>,
11}
12
13/// Encrypt multiple configuration files
14pub fn encrypt_files(config_files: &[ConfigFile]) -> Result<Vec<ConfigFile>> {
15    use super::encryption::{derive_key_from_password, encrypt_data};
16
17    // Get encryption password from environment or prompt
18    let password = std::env::var("LC_SYNC_PASSWORD").unwrap_or_else(|_| {
19        rpassword::prompt_password("Enter sync encryption password: ")
20            .expect("Failed to read password")
21    });
22
23    let key = derive_key_from_password(&password)?;
24
25    let mut encrypted_files = Vec::new();
26    for file in config_files {
27        let encrypted_content = encrypt_data(&file.content, &key)?;
28        encrypted_files.push(ConfigFile {
29            name: file.name.clone(),
30            content: encrypted_content,
31        });
32    }
33
34    Ok(encrypted_files)
35}
36
37/// Decrypt multiple configuration files
38pub fn decrypt_files(encrypted_files: &[ConfigFile]) -> Result<Vec<ConfigFile>> {
39    use super::encryption::{decrypt_data, derive_key_from_password};
40
41    // Get encryption password from environment or prompt
42    let password = std::env::var("LC_SYNC_PASSWORD").unwrap_or_else(|_| {
43        rpassword::prompt_password("Enter sync decryption password: ")
44            .expect("Failed to read password")
45    });
46
47    let key = derive_key_from_password(&password)?;
48
49    let mut decrypted_files = Vec::new();
50    for file in encrypted_files {
51        let decrypted_content = decrypt_data(&file.content, &key)?;
52        decrypted_files.push(ConfigFile {
53            name: file.name.clone(),
54            content: decrypted_content,
55        });
56    }
57
58    Ok(decrypted_files)
59}
60
61/// List available sync providers
62pub async fn handle_sync_providers() -> Result<()> {
63    println!("{}", "Available sync providers:".bold());
64    println!("  • {} - Amazon S3 and S3-compatible storage", "s3".cyan());
65    println!("  • {} - Amazon S3", "amazon-s3".cyan());
66    println!("  • {} - AWS S3", "aws-s3".cyan());
67    println!("  • {} - Cloudflare R2", "cloudflare".cyan());
68    println!("  • {} - Backblaze B2", "backblaze".cyan());
69    println!(
70        "\n{}",
71        "Configure a provider with: lc sync configure <provider>".italic()
72    );
73    Ok(())
74}
75
76/// Sync configuration files to cloud storage
77
78/// Validate sync provider name
79fn validate_sync_provider(provider: &str) -> Result<()> {
80    match provider.to_lowercase().as_str() {
81        "s3" | "amazon-s3" | "aws-s3" | "cloudflare" | "backblaze" => Ok(()),
82        _ => {
83            anyhow::bail!("Unsupported sync provider: {}", provider);
84        }
85    }
86}
87
88/// Sync configuration files to cloud storage
89pub async fn handle_sync_to(provider: &str, encrypted: bool, yes: bool) -> Result<()> {
90    use std::fs;
91    use std::io::{self, Write};
92
93    println!(
94        "📤 {} configuration to {}...",
95        "Syncing".cyan(),
96        provider.bold()
97    );
98
99    // Validate provider early
100    validate_sync_provider(provider)?;
101
102    // Get lc config directory
103    let config_dir = dirs::config_dir()
104        .ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?
105        .join("lc");
106
107    if !config_dir.exists() {
108        anyhow::bail!("Configuration directory does not exist: {:?}", config_dir);
109    }
110
111    // Collect all configuration files
112    let mut config_files = Vec::new();
113
114    // First, collect all .toml and .db files from the main config directory
115    for entry in fs::read_dir(&config_dir)? {
116        let entry = entry?;
117        let path = entry.path();
118        
119        if path.is_file() {
120            let file_name = path.file_name()
121                .and_then(|n| n.to_str())
122                .unwrap_or("unknown");
123            let extension = path.extension().and_then(|e| e.to_str());
124            
125            // Include all .toml files and .db files (logs.db, embeddings.db, etc.)
126            let should_include = extension.map(|e| e == "toml" || e == "db").unwrap_or(false);
127            
128            if should_include {
129                let content = fs::read(&path)?;
130                config_files.push(ConfigFile {
131                    name: file_name.to_string(),
132                    content,
133                });
134            }
135        }
136    }
137
138    // Collect provider configs from providers/ subdirectory
139    let providers_dir = config_dir.join("providers");
140    if providers_dir.exists() {
141        for entry in fs::read_dir(&providers_dir)? {
142            let entry = entry?;
143            let path = entry.path();
144            if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("toml") {
145                let content = fs::read(&path)?;
146                let name = format!("providers/{}", path.file_name().unwrap().to_string_lossy());
147                config_files.push(ConfigFile { name, content });
148            }
149        }
150    }
151
152    // Check for embeddings directory and include any database files there
153    let embeddings_dir = config_dir.join("embeddings");
154    if embeddings_dir.exists() {
155        for entry in fs::read_dir(&embeddings_dir)? {
156            let entry = entry?;
157            let path = entry.path();
158            if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("db") {
159                let content = fs::read(&path)?;
160                let name = format!("embeddings/{}", path.file_name().unwrap().to_string_lossy());
161                config_files.push(ConfigFile { name, content });
162            }
163        }
164    }
165
166    if config_files.is_empty() {
167        println!("{} No configuration files found to sync", "ℹ️".blue());
168        return Ok(());
169    }
170
171    println!("Found {} configuration files", config_files.len());
172
173    // Show files to be synced and confirm
174    if !yes {
175        println!("\nFiles to sync:");
176        for file in &config_files {
177            println!("  • {}", file.name);
178        }
179
180        print!("\nContinue with sync? [y/N]: ");
181        io::stdout().flush()?;
182
183        let mut input = String::new();
184        io::stdin().read_line(&mut input)?;
185
186        if !input.trim().eq_ignore_ascii_case("y") {
187            println!("Sync cancelled.");
188            return Ok(());
189        }
190    }
191
192    // Encrypt files if requested
193    let _files_to_upload = if encrypted {
194        println!("🔐 Encrypting configuration files...");
195        encrypt_files(&config_files)?
196    } else {
197        config_files
198    };
199
200    #[cfg(feature = "s3-sync")]
201    {
202        use super::s3::upload_to_s3_provider;
203        upload_to_s3_provider(&_files_to_upload, provider, encrypted).await?;
204        println!("{} Configuration synced successfully!", "✅".green());
205        return Ok(());
206    }
207
208    #[cfg(not(feature = "s3-sync"))]
209    {
210        anyhow::bail!("S3 sync feature not enabled. Build with --features s3-sync");
211    }
212}
213
214/// Sync configuration files from cloud storage
215pub async fn handle_sync_from(provider: &str, _encrypted: bool, yes: bool) -> Result<()> {
216    use std::fs;
217    use std::io::{self, Write};
218
219    println!(
220        "📥 {} configuration from {}...",
221        "Syncing".cyan(),
222        provider.bold()
223    );
224
225    // Validate provider early
226    validate_sync_provider(provider)?;
227
228    // Get lc config directory
229    let config_dir = dirs::config_dir()
230        .ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?
231        .join("lc");
232
233    // Create config directory if it doesn't exist
234    if !config_dir.exists() {
235        fs::create_dir_all(&config_dir)?;
236    }
237
238    // Confirm before syncing
239    if !yes {
240        println!(
241            "\n⚠️  {} This will overwrite local configuration files!",
242            "Warning:".yellow()
243        );
244        print!("Continue with sync? [y/N]: ");
245        io::stdout().flush()?;
246
247        let mut input = String::new();
248        io::stdin().read_line(&mut input)?;
249
250        if !input.trim().eq_ignore_ascii_case("y") {
251            println!("Sync cancelled.");
252            return Ok(());
253        }
254    }
255
256    #[cfg(feature = "s3-sync")]
257    {
258        use super::s3::download_from_s3_provider;
259        let _downloaded_files: Vec<ConfigFile> = download_from_s3_provider(provider, _encrypted).await?;
260
261        println!("Downloaded {} configuration files", _downloaded_files.len());
262
263        // Decrypt files if they were encrypted
264        let files_to_save = if _encrypted {
265            println!("🔓 Decrypting configuration files...");
266            decrypt_files(&_downloaded_files)?
267        } else {
268            _downloaded_files
269        };
270
271        // Save files to config directory
272        for file in files_to_save {
273            let file_path = config_dir.join(&file.name);
274
275            // Ensure parent directory exists
276            if let Some(parent) = file_path.parent() {
277                fs::create_dir_all(parent)?;
278            }
279
280            fs::write(&file_path, &file.content)?;
281            println!("  ✓ Saved {}", file.name);
282        }
283
284        println!("{} Configuration synced successfully!", "✅".green());
285        return Ok(());
286    }
287
288    #[cfg(not(feature = "s3-sync"))]
289    {
290        anyhow::bail!("S3 sync feature not enabled. Build with --features s3-sync");
291    }
292}