1use anyhow::Result;
4use colored::*;
5
6#[derive(Debug, Clone)]
8pub struct ConfigFile {
9 pub name: String,
10 pub content: Vec<u8>,
11}
12
13pub fn encrypt_files(config_files: &[ConfigFile]) -> Result<Vec<ConfigFile>> {
15 use super::encryption::{derive_key_from_password, encrypt_data};
16
17 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
37pub fn decrypt_files(encrypted_files: &[ConfigFile]) -> Result<Vec<ConfigFile>> {
39 use super::encryption::{decrypt_data, derive_key_from_password};
40
41 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
61pub 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
76fn 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
88pub 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_sync_provider(provider)?;
101
102 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 let mut config_files = Vec::new();
113
114 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 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 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 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 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 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
214pub 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_sync_provider(provider)?;
227
228 let config_dir = dirs::config_dir()
230 .ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?
231 .join("lc");
232
233 if !config_dir.exists() {
235 fs::create_dir_all(&config_dir)?;
236 }
237
238 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 let files_to_save = if _encrypted {
265 println!("🔓 Decrypting configuration files...");
266 decrypt_files(&_downloaded_files)?
267 } else {
268 _downloaded_files
269 };
270
271 for file in files_to_save {
273 let file_path = config_dir.join(&file.name);
274
275 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}