{"record":{"id":"bffcaf5608363640","repo":"farion1231/cc-switch","slug":"skill-backup-is-not-a-directory","errorCode":null,"errorMessage":"Skill backup is not a directory: {}","messagePattern":"Skill backup is not a directory: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":1745,"sourceCode":"                    skill: metadata.skill,\n                }),\n                Err(err) => {\n                    log::warn!(\"解析 Skill 备份失败 {}: {err:#}\", path.display());\n                }\n            }\n        }\n\n        entries.sort_by_key(|entry| std::cmp::Reverse(entry.created_at));\n        Ok(entries)\n    }\n\n    pub fn delete_backup(backup_id: &str) -> Result<()> {\n        let backup_path = Self::backup_path_for_id(backup_id)?;\n        let metadata = fs::symlink_metadata(&backup_path)\n            .with_context(|| format!(\"failed to access {}\", backup_path.display()))?;\n\n        if !metadata.is_dir() {\n            return Err(anyhow!(\n                \"Skill backup is not a directory: {}\",\n                backup_path.display()\n            ));\n        }\n\n        fs::remove_dir_all(&backup_path)\n            .with_context(|| format!(\"failed to delete {}\", backup_path.display()))?;\n\n        log::info!(\"Skill 备份已删除: {}\", backup_path.display());\n        Ok(())\n    }\n\n    pub fn restore_from_backup(\n        db: &Arc<Database>,\n        backup_id: &str,\n        current_app: &AppType,\n    ) -> Result<InstalledSkill> {\n        let _state_guard = skill_state_write_guard();","sourceCodeStart":1727,"sourceCodeEnd":1763,"githubUrl":"https://github.com/farion1231/cc-switch/blob/0b5da510168914b251481654a568c3ffacd62cf4/src-tauri/src/services/skill.rs#L1727-L1763","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the exact path printed in the error message under the skill backups directory","If it is a stray file/symlink, remove it manually with the file manager or rm","Check whether a cloud-sync client is managing the backups folder and exclude it"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const backups = await invoke<Array<{ id: string; created_at: number }>>('list_skill_backups');\nconst target = backups.find(b => b.id === backupId);\nif (!target) { notify('Backup no longer listed — refresh'); return; }","typeGuard":null,"tryCatchPattern":"try {\n  await invoke('delete_skill_backup', { backupId });\n} catch (e) {\n  const msg = String(e);\n  if (msg.includes('not a directory')) {\n    notify(`Remove the stray entry manually: ${msg}`); // path is in the message\n    return;\n  }\n  throw e;\n}","preventionTips":["Exclude the backups directory from cloud-sync clients","Do not hand-edit the backups folder structure","Treat 'not a directory' as filesystem tampering — inspect before deleting anything manually"],"tags":["backup","filesystem","validation","skill-manager"],"backgroundTag":"filesystem-entry-unexpected-type","analyzedSha":"0b5da510168914b251481654a568c3ffacd62cf4","analyzedAt":"2026-08-20T14:29:00.113Z","contentChangedAt":"2026-08-20T14:29:00.113Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}