{"record":{"id":"ff97fbe904c81512","repo":"siyuan-note/siyuan","slug":"can-not-remove-s-caused-by-it-is-not-a-dir","errorCode":null,"errorMessage":"can not remove [%s] caused by it is not a dir","messagePattern":"can not remove \\[(.+?)\\] caused by it is not a dir","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/mount.go","lineNumber":297,"sourceCode":"\t}\n\n\tFlushTxQueue()\n\tsql.FlushQueue()\n\t// 索引和笔记本目录删除后无法再读取 custom-avs，需提前收集；实际删除成功后再清理绑定行。\n\tdeletedAttrViewBlockIDs, err := collectBoxDeletedAttributeViewBlocks(boxID)\n\tif nil != err {\n\t\treturn fmt.Errorf(\"query database-bound blocks in notebook [%s] failed: %w\", boxID, err)\n\t}\n\tisUserGuide := IsUserGuide(boxID)\n\tlocalPath := filepath.Join(util.DataDir, boxID)\n\tif !filelock.IsExist(localPath) {\n\t\tremoveHPathRefreshBox(boxID)\n\t\tforgetRuntimeNormalBox(boxID)\n\t\tremoveMasterPasswordMigrationBox(boxID)\n\t\treturn\n\t}\n\tif !gulu.File.IsDir(localPath) {\n\t\treturn fmt.Errorf(\"can not remove [%s] caused by it is not a dir\", boxID)\n\t}\n\n\t// 删目录前固定加密状态，确保后续历史、资源和索引清理始终使用同一个安全边界。\n\tisEncrypted := IsEncryptedBox(boxID)\n\tif !isUserGuide {\n\t\tif err = EnsureAssetPrefixLocal(localPath); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\tif isEncrypted {\n\t\t// 加密索引先持有生命周期租约再获取索引锁，因此删除也必须先结束生命周期，保持锁顺序一致。\n\t\tunmount0(boxID)\n\t}\n\n\tdatabaseIndexDataLock.Lock()\n\tdefer databaseIndexDataLock.Unlock()\n\tcreateDocLock.Lock()\n\tdefer createDocLock.Unlock()","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/mount.go#L279-L315","documentation":"RemoveBox refuses to delete a notebook whose on-disk workspace path (data/<boxID>) is not a directory. The notebook ID resolves to a file, symlink to a file, or a missing/irregular filesystem entry, so a recursive directory removal would be unsafe. The kernel aborts the removal and returns this error instead of deleting anything.","triggerScenarios":"Calling RemoveBox(boxID) (HTTP /api/notebook/removeNotebook) when data/<boxID> exists but is a regular file, a broken symlink, or another non-directory entry; the ID passes earlier checks (removed from conf, runtime, migration lists) but fails gulu.File.IsDir(localPath).","commonSituations":"Someone extracted a .sy archive or copied a file directly into the data/ folder so it looks like a notebook ID; a sync/restore left a file where the notebook directory used to be; a stale or corrupted notebook entry in conf after the directory was replaced by a file; manual workspace manipulation outside the app.","solutions":["Check the on-disk path: open <workspace>/data/<boxID> and confirm whether it is a file or directory (ls -la).","If it is a stray file, back it up and delete or move it manually, then retry the notebook removal.","If the ID should be a real notebook, re-create/restore the directory (e.g. re-sync or restore from backup) instead of removing it.","Verify boxID is a real notebook ID (20-digit NodeID) and not a wrong value pointing at an unrelated file."],"exampleFix":"// before: removing an ID whose data path is a plain file\nawait fetchPost(\"/api/notebook/removeNotebook\", { notebook: \"20240101120000-abcd123\" });\n// -> can not remove [...] caused by it is not a dir\n\n// after: guard on the client before calling\nconst localPath = path.join(workspaceDir, \"data\", boxID);\nif (fs.statSync(localPath).isDirectory()) {\n  await fetchPost(\"/api/notebook/removeNotebook\", { notebook: boxID });\n} else {\n  fs.rmSync(localPath); // remove the stray file first\n}","handlingStrategy":"validation","validationCode":"import fs from \"fs\";\nimport path from \"path\";\n\nfunction canRemoveBox(workspaceDir: string, boxID: string): boolean {\n  const p = path.join(workspaceDir, \"data\", boxID);\n  try {\n    return fs.statSync(p).isDirectory();\n  } catch {\n    return false;\n  }\n}","typeGuard":"function isDirectory(p: string): p is string & { __dir: true } {\n  try { return require(\"fs\").statSync(p).isDirectory(); } catch { return false; }\n}","tryCatchPattern":"try {\n  await fetchPost(\"/api/notebook/removeNotebook\", { notebook: boxID });\n} catch (e) {\n  if (String(e).includes(\"is not a dir\")) {\n    // inspect/clean the stray file at data/<boxID>, then retry or abort\n  }\n}","preventionTips":["Never place loose files named like node IDs directly in the data/ folder","Inspect data/<boxID> with ls -la before programmatic removal","Restore workspaces from backups fully rather than partially"],"tags":["filesystem","notebook","workspace","validation"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}