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

Skill backup is not a directory: {}

Error message

Skill backup is not a directory: {}

What it means

delete_backup resolves the backup id to a path under the backup directory (rejecting traversal) and stats it with symlink_metadata to avoid following links. If the entry exists but is not a directory (regular file, symlink, or other), it refuses remove_dir_all semantics and aborts. This is a safety guard against deleting through unexpected filesystem entries.

Source

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

                    skill: metadata.skill,
                }),
                Err(err) => {
                    log::warn!("解析 Skill 备份失败 {}: {err:#}", path.display());
                }
            }
        }

        entries.sort_by_key(|entry| std::cmp::Reverse(entry.created_at));
        Ok(entries)
    }

    pub fn delete_backup(backup_id: &str) -> Result<()> {
        let backup_path = Self::backup_path_for_id(backup_id)?;
        let metadata = fs::symlink_metadata(&backup_path)
            .with_context(|| format!("failed to access {}", backup_path.display()))?;

        if !metadata.is_dir() {
            return Err(anyhow!(
                "Skill backup is not a directory: {}",
                backup_path.display()
            ));
        }

        fs::remove_dir_all(&backup_path)
            .with_context(|| format!("failed to delete {}", backup_path.display()))?;

        log::info!("Skill 备份已删除: {}", backup_path.display());
        Ok(())
    }

    pub fn restore_from_backup(
        db: &Arc<Database>,
        backup_id: &str,
        current_app: &AppType,
    ) -> Result<InstalledSkill> {
        let _state_guard = skill_state_write_guard();

View on GitHub (pinned to 0b5da51016)

Solutions

  1. Inspect the exact path printed in the error message under the skill backups directory
  2. If it is a stray file/symlink, remove it manually with the file manager or rm
  3. Check whether a cloud-sync client is managing the backups folder and exclude it
Defensive patterns

Strategy: validation

Validate before calling

const backups = await invoke<Array<{ id: string; created_at: number }>>('list_skill_backups');
const target = backups.find(b => b.id === backupId);
if (!target) { notify('Backup no longer listed — refresh'); return; }

Try / catch

try {
  await invoke('delete_skill_backup', { backupId });
} catch (e) {
  const msg = String(e);
  if (msg.includes('not a directory')) {
    notify(`Remove the stray entry manually: ${msg}`); // path is in the message
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: A backup 'directory' replaced by a regular file or symlink (manual tampering, partial backup creation, copy tools that flatten structure); a previous backup write interrupted at directory-creation time.

Common situations: Users hand-editing the backups folder; backup tools or sync clients (Dropbox/OneDrive) replacing directories with placeholder files or links; interrupted backup writes after disk-full.

Related errors


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