Skip to main content

cc_switch_lib/cli/commands/
config_webdav.rs

1use clap::Subcommand;
2
3use crate::cli::ui::{highlight, info, success, warning};
4use crate::error::AppError;
5use crate::{
6    get_webdav_sync_settings, set_webdav_sync_settings, webdav_jianguoyun_preset,
7    WebDavSyncService, WebDavSyncSettings,
8};
9
10#[derive(Subcommand, Debug, Clone)]
11pub enum WebDavCommand {
12    /// Show current WebDAV sync settings
13    Show,
14
15    /// Create or update WebDAV sync settings
16    Set {
17        #[arg(long)]
18        base_url: Option<String>,
19
20        #[arg(long)]
21        remote_root: Option<String>,
22
23        #[arg(long)]
24        profile: Option<String>,
25
26        #[arg(long)]
27        username: Option<String>,
28
29        #[arg(long)]
30        password: Option<String>,
31
32        #[arg(long, conflicts_with = "disable")]
33        enable: bool,
34
35        #[arg(long, conflicts_with = "enable")]
36        disable: bool,
37
38        #[arg(long, conflicts_with = "no_auto_sync")]
39        auto_sync: bool,
40
41        #[arg(long, conflicts_with = "auto_sync")]
42        no_auto_sync: bool,
43    },
44
45    /// Clear stored WebDAV sync settings
46    Clear,
47
48    /// Apply Jianguoyun preset settings
49    Jianguoyun {
50        #[arg(long)]
51        username: String,
52
53        #[arg(long)]
54        password: String,
55
56        #[arg(long)]
57        remote_root: Option<String>,
58
59        #[arg(long)]
60        profile: Option<String>,
61
62        #[arg(long, conflicts_with = "no_auto_sync")]
63        auto_sync: bool,
64
65        #[arg(long, conflicts_with = "auto_sync")]
66        no_auto_sync: bool,
67    },
68
69    /// Check whether the current WebDAV settings can connect successfully
70    CheckConnection,
71
72    /// Upload the current local snapshot to WebDAV
73    Upload,
74
75    /// Download the current remote snapshot from WebDAV
76    Download,
77
78    /// Migrate legacy V1 remote data to V2 protocol
79    MigrateV1ToV2,
80}
81
82pub fn execute(cmd: WebDavCommand) -> Result<(), AppError> {
83    match cmd {
84        WebDavCommand::Show => show(),
85        WebDavCommand::Set {
86            base_url,
87            remote_root,
88            profile,
89            username,
90            password,
91            enable,
92            disable,
93            auto_sync,
94            no_auto_sync,
95        } => set(
96            base_url,
97            remote_root,
98            profile,
99            username,
100            password,
101            enable,
102            disable,
103            auto_sync,
104            no_auto_sync,
105        ),
106        WebDavCommand::Clear => clear(),
107        WebDavCommand::Jianguoyun {
108            username,
109            password,
110            remote_root,
111            profile,
112            auto_sync,
113            no_auto_sync,
114        } => jianguoyun(
115            username,
116            password,
117            remote_root,
118            profile,
119            auto_sync,
120            no_auto_sync,
121        ),
122        WebDavCommand::CheckConnection => check_connection(),
123        WebDavCommand::Upload => upload(),
124        WebDavCommand::Download => download(),
125        WebDavCommand::MigrateV1ToV2 => migrate_v1_to_v2(),
126    }
127}
128
129fn show() -> Result<(), AppError> {
130    let Some(settings) = get_webdav_sync_settings() else {
131        println!(
132            "{}",
133            info(crate::t!(
134                "WebDAV sync is not configured.",
135                "WebDAV 同步尚未配置。"
136            ))
137        );
138        return Ok(());
139    };
140
141    println!("{}", highlight(crate::t!("WebDAV Sync", "WebDAV 同步")));
142    println!("{}", "═".repeat(60));
143    println!("Enabled:      {}", yes_no(settings.enabled));
144    println!("Base URL:     {}", settings.base_url);
145    println!("Remote Root:  {}", settings.remote_root);
146    println!("Profile:      {}", settings.profile);
147    println!("Username:     {}", blank_as_na(&settings.username));
148    println!("Password:     {}", masked_secret(&settings.password));
149    println!("Auto Sync:    {}", yes_no(settings.auto_sync));
150    println!(
151        "Last Sync:    {}",
152        settings
153            .status
154            .last_sync_at
155            .map(|value| value.to_string())
156            .unwrap_or_else(|| "N/A".to_string())
157    );
158    println!(
159        "Last Error:   {}",
160        settings
161            .status
162            .last_error
163            .clone()
164            .filter(|value| !value.trim().is_empty())
165            .unwrap_or_else(|| "N/A".to_string())
166    );
167
168    Ok(())
169}
170
171#[allow(clippy::too_many_arguments)]
172fn set(
173    base_url: Option<String>,
174    remote_root: Option<String>,
175    profile: Option<String>,
176    username: Option<String>,
177    password: Option<String>,
178    enable: bool,
179    disable: bool,
180    auto_sync: bool,
181    no_auto_sync: bool,
182) -> Result<(), AppError> {
183    let mut settings = merged_settings(
184        get_webdav_sync_settings(),
185        base_url,
186        remote_root,
187        profile,
188        username,
189        password,
190        enable,
191        disable,
192        auto_sync,
193        no_auto_sync,
194    );
195    settings.normalize();
196    set_webdav_sync_settings(Some(settings))?;
197    println!(
198        "{}",
199        success(crate::t!(
200            "✓ WebDAV settings saved.",
201            "✓ WebDAV 设置已保存。"
202        ))
203    );
204    Ok(())
205}
206
207fn clear() -> Result<(), AppError> {
208    set_webdav_sync_settings(None)?;
209    println!(
210        "{}",
211        success(crate::t!(
212            "✓ WebDAV settings cleared.",
213            "✓ WebDAV 设置已清空。"
214        ))
215    );
216    Ok(())
217}
218
219fn jianguoyun(
220    username: String,
221    password: String,
222    remote_root: Option<String>,
223    profile: Option<String>,
224    auto_sync: bool,
225    no_auto_sync: bool,
226) -> Result<(), AppError> {
227    let mut settings = webdav_jianguoyun_preset(&username, &password);
228    if let Some(remote_root) = remote_root {
229        settings.remote_root = remote_root;
230    }
231    if let Some(profile) = profile {
232        settings.profile = profile;
233    }
234    if auto_sync {
235        settings.auto_sync = true;
236    }
237    if no_auto_sync {
238        settings.auto_sync = false;
239    }
240    settings.normalize();
241    set_webdav_sync_settings(Some(settings))?;
242    WebDavSyncService::check_connection()?;
243    println!(
244        "{}",
245        success(crate::t!(
246            "✓ Jianguoyun WebDAV preset applied.",
247            "✓ 已应用坚果云 WebDAV 预设。"
248        ))
249    );
250    Ok(())
251}
252
253fn check_connection() -> Result<(), AppError> {
254    WebDavSyncService::check_connection()?;
255    println!(
256        "{}",
257        success(crate::t!(
258            "✓ WebDAV connection succeeded.",
259            "✓ WebDAV 连接成功。"
260        ))
261    );
262    Ok(())
263}
264
265fn upload() -> Result<(), AppError> {
266    let summary = WebDavSyncService::upload()?;
267    println!("{}", success(&summary.message));
268    Ok(())
269}
270
271fn download() -> Result<(), AppError> {
272    let summary = WebDavSyncService::download()?;
273    sync_live_config_after_webdav();
274    println!("{}", success(&summary.message));
275    Ok(())
276}
277
278fn migrate_v1_to_v2() -> Result<(), AppError> {
279    let summary = WebDavSyncService::migrate_v1_to_v2()?;
280    sync_live_config_after_webdav();
281    println!("{}", success(&summary.message));
282    Ok(())
283}
284
285fn sync_live_config_after_webdav() {
286    let Ok(state) = crate::AppState::try_new() else {
287        return;
288    };
289
290    if let Err(err) = crate::services::ProviderService::sync_current_to_live(&state) {
291        let en = format!("Live config sync after WebDAV operation failed: {err}");
292        let zh = format!("WebDAV 操作后同步 live 配置失败: {err}");
293        println!("{}", warning(crate::t!(&en, &zh)));
294    }
295}
296
297#[allow(clippy::too_many_arguments)]
298fn merged_settings(
299    current: Option<WebDavSyncSettings>,
300    base_url: Option<String>,
301    remote_root: Option<String>,
302    profile: Option<String>,
303    username: Option<String>,
304    password: Option<String>,
305    enable: bool,
306    disable: bool,
307    auto_sync: bool,
308    no_auto_sync: bool,
309) -> WebDavSyncSettings {
310    let mut settings = current.unwrap_or_default();
311
312    if let Some(base_url) = base_url {
313        settings.base_url = base_url;
314    }
315    if let Some(remote_root) = remote_root {
316        settings.remote_root = remote_root;
317    }
318    if let Some(profile) = profile {
319        settings.profile = profile;
320    }
321    if let Some(username) = username {
322        settings.username = username;
323    }
324    if let Some(password) = password {
325        settings.password = password;
326    }
327    if enable {
328        settings.enabled = true;
329    }
330    if disable {
331        settings.enabled = false;
332    }
333    if auto_sync {
334        settings.auto_sync = true;
335    }
336    if no_auto_sync {
337        settings.auto_sync = false;
338    }
339
340    settings
341}
342
343fn yes_no(value: bool) -> &'static str {
344    if value {
345        "yes"
346    } else {
347        "no"
348    }
349}
350
351fn blank_as_na(value: &str) -> &str {
352    let trimmed = value.trim();
353    if trimmed.is_empty() {
354        "N/A"
355    } else {
356        trimmed
357    }
358}
359
360fn masked_secret(value: &str) -> String {
361    if value.trim().is_empty() {
362        return "N/A".to_string();
363    }
364    "********".to_string()
365}
366
367#[cfg(test)]
368mod tests {
369    use super::merged_settings;
370    use crate::{WebDavSyncSettings, WebDavSyncStatus};
371
372    #[test]
373    fn merged_settings_updates_selected_fields_only() {
374        let current = WebDavSyncSettings {
375            enabled: true,
376            base_url: "https://dav.example.com/root".to_string(),
377            remote_root: "sync-root".to_string(),
378            profile: "default".to_string(),
379            username: "demo".to_string(),
380            password: "secret".to_string(),
381            auto_sync: false,
382            status: WebDavSyncStatus {
383                last_error: Some("boom".to_string()),
384                ..WebDavSyncStatus::default()
385            },
386        };
387
388        let merged = merged_settings(
389            Some(current),
390            None,
391            Some("next-root".to_string()),
392            None,
393            None,
394            None,
395            false,
396            false,
397            true,
398            false,
399        );
400
401        assert!(merged.enabled);
402        assert_eq!(merged.base_url, "https://dav.example.com/root");
403        assert_eq!(merged.remote_root, "next-root");
404        assert_eq!(merged.profile, "default");
405        assert_eq!(merged.username, "demo");
406        assert_eq!(merged.password, "secret");
407        assert!(merged.auto_sync);
408        assert_eq!(merged.status.last_error.as_deref(), Some("boom"));
409    }
410}