1use anyhow::Result;
4use colored::Colorize;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9
10#[derive(Debug, Serialize, Deserialize, Clone, Default)]
12pub struct SyncConfig {
13 pub providers: HashMap<String, ProviderConfig>,
14}
15
16#[derive(Debug, Serialize, Deserialize, Clone)]
18#[serde(tag = "type")]
19pub enum ProviderConfig {
20 #[serde(rename = "s3")]
21 S3 {
22 bucket_name: String,
23 region: String,
24 access_key_id: String,
25 secret_access_key: String,
26 endpoint_url: Option<String>,
27 },
28}
29
30impl SyncConfig {
31 pub fn load() -> Result<Self> {
33 let config_path = Self::config_file_path()?;
34
35 if config_path.exists() {
36 let content = fs::read_to_string(&config_path)?;
37 let config: SyncConfig = toml::from_str(&content)?;
38 Ok(config)
39 } else {
40 Ok(SyncConfig::default())
41 }
42 }
43
44 pub fn save(&self) -> Result<()> {
46 let config_path = Self::config_file_path()?;
47
48 if let Some(parent) = config_path.parent() {
50 fs::create_dir_all(parent)?;
51 }
52
53 let content = toml::to_string_pretty(self)?;
54 fs::write(&config_path, content)?;
55 Ok(())
56 }
57
58 fn config_file_path() -> Result<PathBuf> {
60 let config_dir = crate::config::Config::config_dir()?;
61 Ok(config_dir.join("sync.toml"))
62 }
63
64 pub fn set_provider(&mut self, name: String, config: ProviderConfig) {
66 self.providers.insert(name, config);
67 }
68
69 pub fn get_provider(&self, name: &str) -> Option<&ProviderConfig> {
71 self.providers.get(name)
72 }
73
74 pub fn remove_provider(&mut self, name: &str) -> bool {
76 self.providers.remove(name).is_some()
77 }
78}
79
80impl ProviderConfig {
81 pub fn new_s3(
83 bucket_name: String,
84 region: String,
85 access_key_id: String,
86 secret_access_key: String,
87 endpoint_url: Option<String>,
88 ) -> Self {
89 ProviderConfig::S3 {
90 bucket_name,
91 region,
92 access_key_id,
93 secret_access_key,
94 endpoint_url,
95 }
96 }
97
98 pub fn display(&self) -> String {
100 match self {
101 ProviderConfig::S3 {
102 bucket_name,
103 region,
104 access_key_id,
105 endpoint_url,
106 ..
107 } => {
108 let mut info = format!(
109 "S3 Configuration:\n Bucket: {}\n Region: {}\n Access Key: {}***",
110 bucket_name,
111 region,
112 &access_key_id[..access_key_id.len().min(8)]
113 );
114
115 if let Some(endpoint) = endpoint_url {
116 info.push_str(&format!("\n Endpoint: {}", endpoint));
117 }
118
119 info
120 }
121 }
122 }
123}
124
125pub async fn handle_sync_configure(
127 provider_name: &str,
128 command: Option<crate::cli::ConfigureCommands>,
129) -> Result<()> {
130 use crate::cli::ConfigureCommands;
131
132 match command {
133 Some(ConfigureCommands::Setup) | None => {
134 match provider_name.to_lowercase().as_str() {
136 "s3" | "amazon-s3" | "aws-s3" | "cloudflare" | "backblaze" => {
137 setup_s3_config(provider_name).await?;
138 }
139 _ => {
140 anyhow::bail!(
141 "Unsupported provider '{}'. Supported providers: s3, cloudflare, backblaze",
142 provider_name
143 );
144 }
145 }
146 }
147 Some(ConfigureCommands::Show) => {
148 let config = SyncConfig::load()?;
150
151 if let Some(provider_config) = config.get_provider(provider_name) {
152 println!(
153 "\n{}",
154 format!("Configuration for '{}':", provider_name)
155 .bold()
156 .blue()
157 );
158 println!("{}", provider_config.display());
159 } else {
160 println!(
161 "{} No configuration found for provider '{}'",
162 "ℹ️".blue(),
163 provider_name
164 );
165 println!(
166 "Run {} to set up configuration",
167 format!("lc sync configure {} setup", provider_name).dimmed()
168 );
169 }
170 }
171 Some(ConfigureCommands::Remove) => {
172 let mut config = SyncConfig::load()?;
174
175 if config.remove_provider(provider_name) {
176 config.save()?;
177 println!(
178 "{} Configuration for '{}' removed successfully",
179 "✓".green(),
180 provider_name
181 );
182 } else {
183 println!(
184 "{} No configuration found for provider '{}'",
185 "ℹ️".blue(),
186 provider_name
187 );
188 }
189 }
190 }
191
192 Ok(())
193}
194
195async fn setup_s3_config(provider_name: &str) -> Result<()> {
197 use std::io::{self, Write};
198
199 println!("{} Setting up S3 configuration for '{}'", "🔧".blue(), provider_name);
200 println!(
201 "{} This will be stored in your lc config directory",
202 "ℹ️".blue()
203 );
204 println!();
205
206 print!("Enter S3 bucket name: ");
208 io::stdout().flush()?;
210 let mut bucket_name = String::new();
211 io::stdin().read_line(&mut bucket_name)?;
212 let bucket_name = bucket_name.trim().to_string();
213 if bucket_name.is_empty() {
214 anyhow::bail!("Bucket name cannot be empty");
215 }
216
217 print!("Enter AWS region (default: us-east-1): ");
219 io::stdout().flush()?;
221 let mut region = String::new();
222 io::stdin().read_line(&mut region)?;
223 let region = region.trim().to_string();
224 let region = if region.is_empty() {
225 "us-east-1".to_string()
226 } else {
227 region
228 };
229
230 print!("Enter AWS Access Key ID: ");
232 io::stdout().flush()?;
234 let mut access_key_id = String::new();
235 io::stdin().read_line(&mut access_key_id)?;
236 let access_key_id = access_key_id.trim().to_string();
237 if access_key_id.is_empty() {
238 anyhow::bail!("Access Key ID cannot be empty");
239 }
240
241 print!("Enter AWS Secret Access Key: ");
243 io::stdout().flush()?;
245 let secret_access_key = rpassword::read_password()?;
246 if secret_access_key.is_empty() {
247 anyhow::bail!("Secret Access Key cannot be empty");
248 }
249
250 print!("Enter custom S3 endpoint URL (optional, for Backblaze/Cloudflare R2/etc., press Enter to skip): ");
252 io::stdout().flush()?;
254 let mut endpoint_url = String::new();
255 io::stdin().read_line(&mut endpoint_url)?;
256 let endpoint_url = endpoint_url.trim().to_string();
257 let endpoint_url = if endpoint_url.is_empty() {
258 None
259 } else {
260 Some(endpoint_url)
261 };
262
263 let provider_config = ProviderConfig::new_s3(
265 bucket_name.clone(),
266 region.clone(),
267 access_key_id.clone(),
268 secret_access_key,
269 endpoint_url.clone(),
270 );
271
272 let mut config = SyncConfig::load()?;
273 config.set_provider(provider_name.to_string(), provider_config);
274 config.save()?;
275
276 println!("\n{} S3 configuration for '{}' saved successfully!", "✓".green(), provider_name);
277 println!("{} Configuration details:", "📋".blue());
278 println!(" Bucket: {}", bucket_name);
279 println!(" Region: {}", region);
280 println!(
281 " Access Key: {}***",
282 &access_key_id[..access_key_id.len().min(8)]
283 );
284 if let Some(endpoint) = endpoint_url {
285 println!(" Endpoint: {}", endpoint);
286 }
287
288 println!("\n{} You can now use:", "💡".yellow());
289 println!(" {} - Sync to {}", format!("lc sync to {}", provider_name).dimmed(), provider_name);
290 println!(" {} - Sync from {}", format!("lc sync from {}", provider_name).dimmed(), provider_name);
291 println!(
292 " {} - View configuration",
293 format!("lc sync configure {} show", provider_name).dimmed()
294 );
295
296 Ok(())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_provider_config_creation() {
305 let config = ProviderConfig::new_s3(
306 "test-bucket".to_string(),
307 "us-east-1".to_string(),
308 "test-key".to_string(),
309 "test-secret".to_string(),
310 None,
311 );
312
313 assert!(matches!(config, ProviderConfig::S3 { .. }));
315 assert!(config.display().contains("test-bucket"));
316 assert!(config.display().contains("us-east-1"));
317 }
318
319 #[test]
320 fn test_sync_config_operations() {
321 let mut config = SyncConfig::default();
322
323 let provider_config = ProviderConfig::new_s3(
324 "test-bucket".to_string(),
325 "us-east-1".to_string(),
326 "test-key".to_string(),
327 "test-secret".to_string(),
328 None,
329 );
330
331 config.set_provider("s3".to_string(), provider_config);
333 assert!(config.get_provider("s3").is_some());
334 assert_eq!(config.providers.len(), 1);
335
336 let retrieved = config.get_provider("s3");
338 assert!(retrieved.is_some());
339
340 assert!(config.remove_provider("s3"));
342 assert!(config.get_provider("s3").is_none());
343 assert_eq!(config.providers.len(), 0);
344 }
345}