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!(
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 print!("Enter S3 bucket name: ");
212 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 print!("Enter AWS region (default: us-east-1): ");
223 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 print!("Enter AWS Access Key ID: ");
236 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 print!("Enter AWS Secret Access Key: ");
247 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 print!("Enter custom S3 endpoint URL (optional, for Backblaze/Cloudflare R2/etc., press Enter to skip): ");
256 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 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 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 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 let retrieved = config.get_provider("s3");
354 assert!(retrieved.is_some());
355
356 assert!(config.remove_provider("s3"));
358 assert!(config.get_provider("s3").is_none());
359 assert_eq!(config.providers.len(), 0);
360 }
361}