|
1 | | -use crate::config::{Config, ConfigManager}; |
| 1 | +use crate::config::{ |
| 2 | + Config, ConfigManager, ExportBundle, ExportOptions as ExportServiceOptions, ExportService, |
| 3 | + ImportOptions as ImportServiceOptions, ImportService, ValidationResult, |
| 4 | +}; |
2 | 5 | use crate::models::AppType; |
3 | 6 | use serde::{Deserialize, Serialize}; |
4 | 7 | use std::path::PathBuf; |
@@ -356,3 +359,217 @@ pub fn get_config_paths() -> Result<ConfigPathInfo, String> { |
356 | 359 | json_exists: json_path.exists(), |
357 | 360 | }) |
358 | 361 | } |
| 362 | + |
| 363 | +// ============ Enhanced Export/Import Commands (using ExportService/ImportService) ============ |
| 364 | + |
| 365 | +/// 统一导出选项 |
| 366 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 367 | +pub struct UnifiedExportOptions { |
| 368 | + /// 是否包含配置 |
| 369 | + pub include_config: bool, |
| 370 | + /// 是否包含凭证 |
| 371 | + pub include_credentials: bool, |
| 372 | + /// 是否脱敏敏感信息 |
| 373 | + pub redact_secrets: bool, |
| 374 | +} |
| 375 | + |
| 376 | +/// 统一导出结果 |
| 377 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 378 | +pub struct UnifiedExportResult { |
| 379 | + /// 导出包内容(JSON 格式) |
| 380 | + pub content: String, |
| 381 | + /// 建议的文件名 |
| 382 | + pub suggested_filename: String, |
| 383 | + /// 是否已脱敏 |
| 384 | + pub redacted: bool, |
| 385 | + /// 是否包含配置 |
| 386 | + pub has_config: bool, |
| 387 | + /// 是否包含凭证 |
| 388 | + pub has_credentials: bool, |
| 389 | +} |
| 390 | + |
| 391 | +/// 导出完整的配置和凭证包 |
| 392 | +/// |
| 393 | +/// # Arguments |
| 394 | +/// * `config` - 当前配置 |
| 395 | +/// * `options` - 导出选项 |
| 396 | +/// |
| 397 | +/// # Requirements: 3.1, 3.2 |
| 398 | +#[tauri::command] |
| 399 | +pub fn export_bundle( |
| 400 | + config: Config, |
| 401 | + options: UnifiedExportOptions, |
| 402 | +) -> Result<UnifiedExportResult, String> { |
| 403 | + let export_options = ExportServiceOptions { |
| 404 | + include_config: options.include_config, |
| 405 | + include_credentials: options.include_credentials, |
| 406 | + redact_secrets: options.redact_secrets, |
| 407 | + }; |
| 408 | + |
| 409 | + // 获取应用版本 |
| 410 | + let app_version = env!("CARGO_PKG_VERSION").to_string(); |
| 411 | + |
| 412 | + let bundle = |
| 413 | + ExportService::export(&config, &export_options, &app_version).map_err(|e| e.to_string())?; |
| 414 | + |
| 415 | + let content = bundle.to_json().map_err(|e| e.to_string())?; |
| 416 | + |
| 417 | + // 生成带时间戳的文件名 |
| 418 | + let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); |
| 419 | + let suffix = if options.redact_secrets { |
| 420 | + "_redacted" |
| 421 | + } else { |
| 422 | + "" |
| 423 | + }; |
| 424 | + let scope = match (options.include_config, options.include_credentials) { |
| 425 | + (true, true) => "full", |
| 426 | + (true, false) => "config", |
| 427 | + (false, true) => "credentials", |
| 428 | + (false, false) => "empty", |
| 429 | + }; |
| 430 | + let suggested_filename = format!("proxycast_{}_{}{}.json", scope, timestamp, suffix); |
| 431 | + |
| 432 | + Ok(UnifiedExportResult { |
| 433 | + content, |
| 434 | + suggested_filename, |
| 435 | + redacted: bundle.redacted, |
| 436 | + has_config: bundle.has_config(), |
| 437 | + has_credentials: bundle.has_credentials(), |
| 438 | + }) |
| 439 | +} |
| 440 | + |
| 441 | +/// 仅导出配置为 YAML |
| 442 | +/// |
| 443 | +/// # Arguments |
| 444 | +/// * `config` - 当前配置 |
| 445 | +/// * `redact_secrets` - 是否脱敏敏感信息 |
| 446 | +/// |
| 447 | +/// # Requirements: 3.1, 5.1 |
| 448 | +#[tauri::command] |
| 449 | +pub fn export_config_yaml(config: Config, redact_secrets: bool) -> Result<ExportResult, String> { |
| 450 | + let content = ExportService::export_yaml(&config, redact_secrets).map_err(|e| e.to_string())?; |
| 451 | + |
| 452 | + // 生成带时间戳的文件名 |
| 453 | + let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); |
| 454 | + let suffix = if redact_secrets { "_redacted" } else { "" }; |
| 455 | + let suggested_filename = format!("proxycast_config_{}{}.yaml", timestamp, suffix); |
| 456 | + |
| 457 | + Ok(ExportResult { |
| 458 | + content, |
| 459 | + suggested_filename, |
| 460 | + }) |
| 461 | +} |
| 462 | + |
| 463 | +/// 验证导入内容 |
| 464 | +/// |
| 465 | +/// # Arguments |
| 466 | +/// * `content` - 导入内容(JSON 导出包或 YAML 配置) |
| 467 | +/// |
| 468 | +/// # Requirements: 4.1, 4.2 |
| 469 | +#[tauri::command] |
| 470 | +pub fn validate_import(content: String) -> Result<ValidationResult, String> { |
| 471 | + Ok(ImportService::validate(&content)) |
| 472 | +} |
| 473 | + |
| 474 | +/// 导入完整的导出包 |
| 475 | +/// |
| 476 | +/// # Arguments |
| 477 | +/// * `current_config` - 当前配置 |
| 478 | +/// * `content` - 导出包内容(JSON 格式) |
| 479 | +/// * `merge` - 是否合并到现有配置 |
| 480 | +/// |
| 481 | +/// # Requirements: 4.1, 4.3 |
| 482 | +#[tauri::command] |
| 483 | +pub fn import_bundle( |
| 484 | + current_config: Config, |
| 485 | + content: String, |
| 486 | + merge: bool, |
| 487 | +) -> Result<ImportResult, String> { |
| 488 | + // 首先尝试解析为 ExportBundle |
| 489 | + if let Ok(bundle) = ExportBundle::from_json(&content) { |
| 490 | + let options = ImportServiceOptions { merge }; |
| 491 | + let result = |
| 492 | + ImportService::import(&bundle, ¤t_config, &options, ¤t_config.auth_dir) |
| 493 | + .map_err(|e| e.to_string())?; |
| 494 | + |
| 495 | + return Ok(ImportResult { |
| 496 | + success: result.success, |
| 497 | + config: result.config, |
| 498 | + warnings: result.warnings, |
| 499 | + }); |
| 500 | + } |
| 501 | + |
| 502 | + // 尝试解析为 YAML 配置 |
| 503 | + let options = ImportServiceOptions { merge }; |
| 504 | + let result = ImportService::import_yaml(&content, ¤t_config, &options) |
| 505 | + .map_err(|e| e.to_string())?; |
| 506 | + |
| 507 | + Ok(ImportResult { |
| 508 | + success: result.success, |
| 509 | + config: result.config, |
| 510 | + warnings: result.warnings, |
| 511 | + }) |
| 512 | +} |
| 513 | + |
| 514 | +// ============ Path Utility Commands ============ |
| 515 | + |
| 516 | +/// 展开路径中的 tilde (~) 为用户主目录 |
| 517 | +/// |
| 518 | +/// # Arguments |
| 519 | +/// * `path` - 要展开的路径字符串 |
| 520 | +/// |
| 521 | +/// # Returns |
| 522 | +/// 展开后的完整路径字符串 |
| 523 | +/// |
| 524 | +/// # Requirements: 2.3 |
| 525 | +#[tauri::command] |
| 526 | +pub fn expand_path(path: String) -> Result<String, String> { |
| 527 | + use crate::config::expand_tilde; |
| 528 | + |
| 529 | + let expanded = expand_tilde(&path); |
| 530 | + Ok(expanded.to_string_lossy().to_string()) |
| 531 | +} |
| 532 | + |
| 533 | +/// 打开认证目录 |
| 534 | +/// |
| 535 | +/// # Arguments |
| 536 | +/// * `path` - 认证目录路径(支持 tilde 展开) |
| 537 | +/// |
| 538 | +/// # Requirements: 2.2 |
| 539 | +#[tauri::command] |
| 540 | +pub async fn open_auth_dir(path: String) -> Result<bool, String> { |
| 541 | + use crate::config::expand_tilde; |
| 542 | + |
| 543 | + let expanded = expand_tilde(&path); |
| 544 | + |
| 545 | + // 确保目录存在 |
| 546 | + if !expanded.exists() { |
| 547 | + std::fs::create_dir_all(&expanded).map_err(|e| e.to_string())?; |
| 548 | + } |
| 549 | + |
| 550 | + #[cfg(target_os = "macos")] |
| 551 | + { |
| 552 | + std::process::Command::new("open") |
| 553 | + .arg(&expanded) |
| 554 | + .spawn() |
| 555 | + .map_err(|e| e.to_string())?; |
| 556 | + } |
| 557 | + |
| 558 | + #[cfg(target_os = "windows")] |
| 559 | + { |
| 560 | + std::process::Command::new("explorer") |
| 561 | + .arg(&expanded) |
| 562 | + .spawn() |
| 563 | + .map_err(|e| e.to_string())?; |
| 564 | + } |
| 565 | + |
| 566 | + #[cfg(target_os = "linux")] |
| 567 | + { |
| 568 | + std::process::Command::new("xdg-open") |
| 569 | + .arg(&expanded) |
| 570 | + .spawn() |
| 571 | + .map_err(|e| e.to_string())?; |
| 572 | + } |
| 573 | + |
| 574 | + Ok(true) |
| 575 | +} |
0 commit comments