lc/sync/
config.rs

1//! Sync configuration management for storing cloud provider settings
2
3use anyhow::Result;
4use colored::Colorize;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9
10/// Sync configuration for all providers
11#[derive(Debug, Serialize, Deserialize, Clone, Default)]
12pub struct SyncConfig {
13    pub providers: HashMap<String, ProviderConfig>,
14}
15
16/// Configuration for a specific cloud provider
17#[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    /// Load sync configuration from file
32    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    /// Save sync configuration to file
45    pub fn save(&self) -> Result<()> {
46        let config_path = Self::config_file_path()?;
47
48        // Ensure config directory exists
49        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    /// Get the path to the sync configuration file
59    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    /// Add or update a provider configuration
65    pub fn set_provider(&mut self, name: String, config: ProviderConfig) {
66        self.providers.insert(name, config);
67    }
68
69    /// Get a provider configuration
70    pub fn get_provider(&self, name: &str) -> Option<&ProviderConfig> {
71        self.providers.get(name)
72    }
73
74    /// Remove a provider configuration
75    pub fn remove_provider(&mut self, name: &str) -> bool {
76        self.providers.remove(name).is_some()
77    }
78}
79
80impl ProviderConfig {
81    /// Create a new S3 provider configuration
82    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    /// Display provider configuration (hiding sensitive data)
99    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
125/// Handle sync configure command
126pub 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            // Setup provider configuration
135            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            // Show provider configuration
149            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            // Remove provider configuration
173            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
195/// Setup S3 configuration interactively
196async fn setup_s3_config(provider_name: &str) -> Result<()> {
197    use std::io::{self, Write};
198
199    println!(
200        "{} Setting up S3 configuration for '{}'",
201        "🔧".blue(),
202        provider_name
203    );
204    println!(
205        "{} This will be stored in your lc config directory",
206        "ℹ️".blue()
207    );
208    println!();
209
210    // Get bucket name
211    print!("Enter S3 bucket name: ");
212    // Deliberately flush stdout to ensure prompt appears before user input
213    io::stdout().flush()?;
214    let mut bucket_name = String::new();
215    io::stdin().read_line(&mut bucket_name)?;
216    let bucket_name = bucket_name.trim().to_string();
217    if bucket_name.is_empty() {
218        anyhow::bail!("Bucket name cannot be empty");
219    }
220
221    // Get region
222    print!("Enter AWS region (default: us-east-1): ");
223    // Deliberately flush stdout to ensure prompt appears before user input
224    io::stdout().flush()?;
225    let mut region = String::new();
226    io::stdin().read_line(&mut region)?;
227    let region = region.trim().to_string();
228    let region = if region.is_empty() {
229        "us-east-1".to_string()
230    } else {
231        region
232    };
233
234    // Get access key ID
235    print!("Enter AWS Access Key ID: ");
236    // Deliberately flush stdout to ensure prompt appears before user input
237    io::stdout().flush()?;
238    let mut access_key_id = String::new();
239    io::stdin().read_line(&mut access_key_id)?;
240    let access_key_id = access_key_id.trim().to_string();
241    if access_key_id.is_empty() {
242        anyhow::bail!("Access Key ID cannot be empty");
243    }
244
245    // Get secret access key (hidden input)
246    print!("Enter AWS Secret Access Key: ");
247    // Deliberately flush stdout to ensure prompt appears before password input
248    io::stdout().flush()?;
249    let secret_access_key = rpassword::read_password()?;
250    if secret_access_key.is_empty() {
251        anyhow::bail!("Secret Access Key cannot be empty");
252    }
253
254    // Get optional endpoint URL
255    print!("Enter custom S3 endpoint URL (optional, for Backblaze/Cloudflare R2/etc., press Enter to skip): ");
256    // Deliberately flush stdout to ensure prompt appears before user input
257    io::stdout().flush()?;
258    let mut endpoint_url = String::new();
259    io::stdin().read_line(&mut endpoint_url)?;
260    let endpoint_url = endpoint_url.trim().to_string();
261    let endpoint_url = if endpoint_url.is_empty() {
262        None
263    } else {
264        Some(endpoint_url)
265    };
266
267    // Create and save configuration
268    let provider_config = ProviderConfig::new_s3(
269        bucket_name.clone(),
270        region.clone(),
271        access_key_id.clone(),
272        secret_access_key,
273        endpoint_url.clone(),
274    );
275
276    let mut config = SyncConfig::load()?;
277    config.set_provider(provider_name.to_string(), provider_config);
278    config.save()?;
279
280    println!(
281        "\n{} S3 configuration for '{}' saved successfully!",
282        "✓".green(),
283        provider_name
284    );
285    println!("{} Configuration details:", "📋".blue());
286    println!("  Bucket: {}", bucket_name);
287    println!("  Region: {}", region);
288    println!(
289        "  Access Key: {}***",
290        &access_key_id[..access_key_id.len().min(8)]
291    );
292    if let Some(endpoint) = endpoint_url {
293        println!("  Endpoint: {}", endpoint);
294    }
295
296    println!("\n{} You can now use:", "💡".yellow());
297    println!(
298        "  {} - Sync to {}",
299        format!("lc sync to {}", provider_name).dimmed(),
300        provider_name
301    );
302    println!(
303        "  {} - Sync from {}",
304        format!("lc sync from {}", provider_name).dimmed(),
305        provider_name
306    );
307    println!(
308        "  {} - View configuration",
309        format!("lc sync configure {} show", provider_name).dimmed()
310    );
311
312    Ok(())
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn test_provider_config_creation() {
321        let config = ProviderConfig::new_s3(
322            "test-bucket".to_string(),
323            "us-east-1".to_string(),
324            "test-key".to_string(),
325            "test-secret".to_string(),
326            None,
327        );
328
329        // Test that the config was created successfully
330        assert!(matches!(config, ProviderConfig::S3 { .. }));
331        assert!(config.display().contains("test-bucket"));
332        assert!(config.display().contains("us-east-1"));
333    }
334
335    #[test]
336    fn test_sync_config_operations() {
337        let mut config = SyncConfig::default();
338
339        let provider_config = ProviderConfig::new_s3(
340            "test-bucket".to_string(),
341            "us-east-1".to_string(),
342            "test-key".to_string(),
343            "test-secret".to_string(),
344            None,
345        );
346
347        // Test adding provider
348        config.set_provider("s3".to_string(), provider_config);
349        assert!(config.get_provider("s3").is_some());
350        assert_eq!(config.providers.len(), 1);
351
352        // Test getting provider
353        let retrieved = config.get_provider("s3");
354        assert!(retrieved.is_some());
355
356        // Test removing provider
357        assert!(config.remove_provider("s3"));
358        assert!(config.get_provider("s3").is_none());
359        assert_eq!(config.providers.len(), 0);
360    }
361}