Sfoglia il codice sorgente

feat: 导入路径重映射 + read_backup_manifest 命令

- ImportParams 新增 project_path_overrides 字段,支持 Full 策略下按项目 ID 重映射目标路径

- 新增 is_path_root_accessible 辅助函数检查路径根(盘符/根目录)可达性

- 新增 ProjectManifestEntry 结构体与 do_read_backup_manifest 核心函数

- 新增 read_backup_manifest Tauri 命令,供前端导入前预检项目路径

- 在 lib.rs invoke_handler 注册 read_backup_manifest 命令

- 新增 3 个单元测试覆盖路径可达性检查逻辑
Mochocyang 2 mesi fa
parent
commit
6c6f148372
2 ha cambiato i file con 101 aggiunte e 1 eliminazioni
  1. 100 1
      src-tauri/src/commands/backup.rs
  2. 1 0
      src-tauri/src/lib.rs

+ 100 - 1
src-tauri/src/commands/backup.rs

@@ -58,6 +58,7 @@ pub struct ImportParams {
     pub zip_path: String,
     pub strategy: ImportStrategy,
     pub projects: Option<Vec<ProjectRestoreInfo>>,
+    pub project_path_overrides: Option<std::collections::HashMap<String, String>>,
 }
 
 #[derive(Debug, Clone, Serialize)]
@@ -89,6 +90,15 @@ pub struct BackupManifest {
     pub projects: Vec<ProjectBackupInfo>,
 }
 
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProjectManifestEntry {
+    pub id: String,
+    pub path: String,
+    pub name: String,
+    pub path_accessible: bool,
+}
+
 #[derive(Debug, Clone, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct BackupProgressPayload {
@@ -290,6 +300,29 @@ fn extract_dir_from_zip(
     Ok(count)
 }
 
+/// 检查项目路径的根(盘符/根目录)是否可达。
+/// Windows: 检查盘符是否存在(如 "D:\" 存在)。
+/// Unix: 根目录 "/" 始终可达。
+fn is_path_root_accessible(path: &str) -> bool {
+    let p = Path::new(path);
+    match p.components().next() {
+        Some(std::path::Component::Prefix(prefix)) => {
+            // Windows 驱动器前缀(如 "C:" "D:")
+            let prefix_path = prefix.as_os_str();
+            let root = if prefix_path.to_string_lossy().len() == 2 {
+                // "D:" → "D:\"
+                format!("{}\\", prefix_path.to_string_lossy())
+            } else {
+                // UNC 路径等,直接检查
+                prefix_path.to_string_lossy().to_string()
+            };
+            Path::new(&root).exists()
+        }
+        Some(std::path::Component::RootDir) => true,
+        _ => true,
+    }
+}
+
 // ── Core logic (Tauri-agnostic) ──────────────────────────────────
 
 /// Core export backup logic.
@@ -588,7 +621,15 @@ pub fn do_import_backup<F: Fn(&BackupProgressPayload)>(
             ImportStrategy::Full => {
                 manifest_projects
                     .iter()
-                    .map(|p| (p.id.clone(), p.path.clone(), p.name.clone()))
+                    .map(|p| {
+                        let path = params
+                            .project_path_overrides
+                            .as_ref()
+                            .and_then(|m| m.get(&p.id))
+                            .cloned()
+                            .unwrap_or_else(|| p.path.clone());
+                        (p.id.clone(), path, p.name.clone())
+                    })
                     .collect()
             }
             ImportStrategy::Selective => params
@@ -679,6 +720,38 @@ pub fn do_import_backup<F: Fn(&BackupProgressPayload)>(
     })
 }
 
+/// 读取备份文件中的 manifest.json,返回项目列表及路径可达性。
+/// 不做实际解压,供前端在导入前检查路径。
+pub fn do_read_backup_manifest(zip_path: &Path) -> Result<Vec<ProjectManifestEntry>, String> {
+    if !zip_path.exists() {
+        return Err("备份文件不存在".to_string());
+    }
+
+    let file = fs::File::open(zip_path)
+        .map_err(|e| format!("打开备份文件失败: {e}"))?;
+    let mut archive = zip::ZipArchive::new(file)
+        .map_err(|e| format!("读取备份文件失败,可能已损坏: {e}"))?;
+
+    let manifest_bytes = extract_file_from_zip(&mut archive, "manifest.json")?
+        .ok_or_else(|| "备份文件缺少 manifest.json".to_string())?;
+
+    let manifest: BackupManifest = serde_json::from_slice(&manifest_bytes)
+        .map_err(|e| format!("解析 manifest.json 失败: {e}"))?;
+
+    let entries = manifest
+        .projects
+        .iter()
+        .map(|p| ProjectManifestEntry {
+            id: p.id.clone(),
+            path: p.path.clone(),
+            name: p.name.clone(),
+            path_accessible: is_path_root_accessible(&p.path),
+        })
+        .collect();
+
+    Ok(entries)
+}
+
 // ── Tauri commands ───────────────────────────────────────────────
 
 #[tauri::command]
@@ -739,6 +812,14 @@ pub async fn import_backup(
     })
 }
 
+#[tauri::command]
+pub async fn read_backup_manifest(zip_path: String) -> Result<Vec<ProjectManifestEntry>, String> {
+    run_guarded("read_backup_manifest", || {
+        let path = Path::new(&zip_path);
+        do_read_backup_manifest(path)
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -806,4 +887,22 @@ mod tests {
             ".trash 应包含在 PROJECT_SUBDIRS 中"
         );
     }
+
+    #[test]
+    fn test_is_path_root_accessible_windows_drive() {
+        // C 盘在 Windows 上始终存在
+        assert!(is_path_root_accessible("C:\\some\\path"));
+    }
+
+    #[test]
+    fn test_is_path_root_accessible_unix_root() {
+        // Unix 根目录始终可达
+        assert!(is_path_root_accessible("/home/user/project"));
+    }
+
+    #[test]
+    fn test_is_path_root_accessible_nonexistent_drive() {
+        // Z 盘大概率不存在
+        assert!(!is_path_root_accessible("Z:\\nonexistent\\path"));
+    }
 }

+ 1 - 0
src-tauri/src/lib.rs

@@ -119,6 +119,7 @@ pub fn run() {
             commands::file_sync::ignore_file_change_task,
             commands::backup::export_backup,
             commands::backup::import_backup,
+            commands::backup::read_backup_manifest,
             set_proxy_env,
         ])
         .on_window_event(|window, event| {