{"record":{"id":"78c594122a0fc999","repo":"farion1231/cc-switch","slug":"restore-target-already-exists","errorCode":null,"errorMessage":"Restore target already exists: {}","messagePattern":"Restore target already exists: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":1795,"sourceCode":"                skill\n                    .directory\n                    .eq_ignore_ascii_case(&metadata.skill.directory)\n            })\n        {\n            return Err(anyhow!(\n                \"Skill already exists, please uninstall the current one first: {}\",\n                metadata.skill.directory\n            ));\n        }\n\n        // meta.json 是文件内容（可能来自手工放置或不可信备份），directory 此前\n        // 未经任何校验就直接 join——可穿越出 SSOT 目录写任意位置。必须先校验。\n        let directory = Self::require_valid_directory(&metadata.skill.directory)?;\n\n        let ssot_dir = Self::get_ssot_dir()?;\n        let restore_path = ssot_dir.join(&directory);\n        if restore_path.exists() || Self::is_symlink(&restore_path) {\n            return Err(anyhow!(\n                \"Restore target already exists: {}\",\n                restore_path.display()\n            ));\n        }\n\n        let mut restored_skill = metadata.skill;\n        restored_skill.directory = directory;\n        restored_skill.installed_at = Utc::now().timestamp();\n        restored_skill.apps = SkillApps::only(current_app);\n        restored_skill.updated_at = 0;\n\n        Self::copy_dir_recursive(&backup_skill_dir, &restore_path)?;\n\n        // 重新计算内容哈希\n        restored_skill.content_hash = Self::compute_dir_hash(&restore_path).ok();\n\n        if let Err(err) = db.save_skill(&restored_skill) {\n            let _ = fs::remove_dir_all(&restore_path);","sourceCodeStart":1777,"sourceCodeEnd":1813,"githubUrl":"https://github.com/farion1231/cc-switch/blob/0b5da510168914b251481654a568c3ffacd62cf4/src-tauri/src/services/skill.rs#L1777-L1813","documentation":"The database has no conflicting skill (id or directory), but the target path under the SSOT directory already exists on disk — as a file, directory, or symlink. Restore never overwrites existing filesystem state, even orphaned state no DB row references.","triggerScenarios":"Leftover directory from an uninstall whose DB delete succeeded but file removal failed or was skipped; a manual copy of the skill into the SSOT folder; a symlink planted at the target path.","commonSituations":"Earlier failed/partial uninstalls; users hand-managing the SSOT directory; sync tools recreating folders.","solutions":["Inspect the exact path in the error message under the SSOT skills directory","If it is a leftover you do not need, move it aside or delete it manually, then retry the restore","If unsure of its origin, rename it (e.g. add .bak) instead of deleting, then restore"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await invoke('restore_skill_backup', { backupId, currentApp });\n} catch (e) {\n  const msg = String(e);\n  if (msg.includes('Restore target already exists')) {\n    notify(`Leftover files at the target — inspect and move aside: ${msg}`);\n    return;\n  }\n  throw e;\n}","preventionTips":["After failed uninstalls, check for leftover directories in the SSOT skills folder","Do not manually place folders into the SSOT directory","Rename leftovers to *.bak rather than deleting when origin is unclear"],"tags":["restore","filesystem","path-exists","leftover-state","skill-manager"],"backgroundTag":"filesystem-path-already-exists","analyzedSha":"0b5da510168914b251481654a568c3ffacd62cf4","analyzedAt":"2026-08-20T14:29:00.113Z","contentChangedAt":"2026-08-20T14:29:00.113Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}