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!("{} 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    // Get bucket name
207    print!("Enter S3 bucket name: ");
208    // Deliberately flush stdout to ensure prompt appears before user input
209    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    // Get region
218    print!("Enter AWS region (default: us-east-1): ");
219    // Deliberately flush stdout to ensure prompt appears before user input
220    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    // Get access key ID
231    print!("Enter AWS Access Key ID: ");
232    // Deliberately flush stdout to ensure prompt appears before user input
233    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    // Get secret access key (hidden input)
242    print!("Enter AWS Secret Access Key: ");
243    // Deliberately flush stdout to ensure prompt appears before password input
244    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    // Get optional endpoint URL
251    print!("Enter custom S3 endpoint URL (optional, for Backblaze/Cloudflare R2/etc., press Enter to skip): ");
252    // Deliberately flush stdout to ensure prompt appears before user input
253    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    // Create and save configuration
264    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        // Test that the config was created successfully
314        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        // Test adding provider
332        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        // Test getting provider
337        let retrieved = config.get_provider("s3");
338        assert!(retrieved.is_some());
339
340        // Test removing provider
341        assert!(config.remove_provider("s3"));
342        assert!(config.get_provider("s3").is_none());
343        assert_eq!(config.providers.len(), 0);
344    }
345}