farion1231/cc-switch · error · anyhow::Error

Restore target already exists: {}

Error message

Restore target already exists: {}

What it means

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.

Source

Thrown at src-tauri/src/services/skill.rs:1795

                skill
                    .directory
                    .eq_ignore_ascii_case(&metadata.skill.directory)
            })
        {
            return Err(anyhow!(
                "Skill already exists, please uninstall the current one first: {}",
                metadata.skill.directory
            ));
        }

        // meta.json 是文件内容(可能来自手工放置或不可信备份),directory 此前
        // 未经任何校验就直接 join——可穿越出 SSOT 目录写任意位置。必须先校验。
        let directory = Self::require_valid_directory(&metadata.skill.directory)?;

        let ssot_dir = Self::get_ssot_dir()?;
        let restore_path = ssot_dir.join(&directory);
        if restore_path.exists() || Self::is_symlink(&restore_path) {
            return Err(anyhow!(
                "Restore target already exists: {}",
                restore_path.display()
            ));
        }

        let mut restored_skill = metadata.skill;
        restored_skill.directory = directory;
        restored_skill.installed_at = Utc::now().timestamp();
        restored_skill.apps = SkillApps::only(current_app);
        restored_skill.updated_at = 0;

        Self::copy_dir_recursive(&backup_skill_dir, &restore_path)?;

        // 重新计算内容哈希
        restored_skill.content_hash = Self::compute_dir_hash(&restore_path).ok();

        if let Err(err) = db.save_skill(&restored_skill) {
            let _ = fs::remove_dir_all(&restore_path);

View on GitHub (pinned to 0b5da51016)

Solutions

  1. Inspect the exact path in the error message under the SSOT skills directory
  2. If it is a leftover you do not need, move it aside or delete it manually, then retry the restore
  3. If unsure of its origin, rename it (e.g. add .bak) instead of deleting, then restore
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await invoke('restore_skill_backup', { backupId, currentApp });
} catch (e) {
  const msg = String(e);
  if (msg.includes('Restore target already exists')) {
    notify(`Leftover files at the target — inspect and move aside: ${msg}`);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: Earlier failed/partial uninstalls; users hand-managing the SSOT directory; sync tools recreating folders.

Related errors


AI-assisted analysis of farion1231/cc-switch@0b5da51016 (2026-08-20). Data as JSON: /api/errors/78c594122a0fc999. Report an issue: GitHub.