siyuan-note/siyuan · error
can not remove [ ] caused by it is not a dir
Error message
can not remove [%s] caused by it is not a dir
What it means
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.
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.
Example fix
// before: removing an ID whose data path is a plain file
await fetchPost("/api/notebook/removeNotebook", { notebook: "20240101120000-abcd123" });
// -> can not remove [...] caused by it is not a dir
// after: guard on the client before calling
const localPath = path.join(workspaceDir, "data", boxID);
if (fs.statSync(localPath).isDirectory()) {
await fetchPost("/api/notebook/removeNotebook", { notebook: boxID });
} else {
fs.rmSync(localPath); // remove the stray file first
} Defensive patterns
Strategy: validation
Validate before calling
import fs from "fs";
import path from "path";
function canRemoveBox(workspaceDir: string, boxID: string): boolean {
const p = path.join(workspaceDir, "data", boxID);
try {
return fs.statSync(p).isDirectory();
} catch {
return false;
}
} Type guard
function isDirectory(p: string): p is string & { __dir: true } {
try { return require("fs").statSync(p).isDirectory(); } catch { return false; }
} Try / catch
try {
await fetchPost("/api/notebook/removeNotebook", { notebook: boxID });
} catch (e) {
if (String(e).includes("is not a dir")) {
// inspect/clean the stray file at data/<boxID>, then retry or abort
}
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- can not open file, just support open folder only
- 106
- asset must be a regular file
- asset path does not belong to a notebook
- asset path must be a file
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/ff97fbe904c81512.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/mount.go:297
}
FlushTxQueue()
sql.FlushQueue()
// 索引和笔记本目录删除后无法再读取 custom-avs,需提前收集;实际删除成功后再清理绑定行。
deletedAttrViewBlockIDs, err := collectBoxDeletedAttributeViewBlocks(boxID)
if nil != err {
return fmt.Errorf("query database-bound blocks in notebook [%s] failed: %w", boxID, err)
}
isUserGuide := IsUserGuide(boxID)
localPath := filepath.Join(util.DataDir, boxID)
if !filelock.IsExist(localPath) {
removeHPathRefreshBox(boxID)
forgetRuntimeNormalBox(boxID)
removeMasterPasswordMigrationBox(boxID)
return
}
if !gulu.File.IsDir(localPath) {
return fmt.Errorf("can not remove [%s] caused by it is not a dir", boxID)
}
// 删目录前固定加密状态,确保后续历史、资源和索引清理始终使用同一个安全边界。
isEncrypted := IsEncryptedBox(boxID)
if !isUserGuide {
if err = EnsureAssetPrefixLocal(localPath); err != nil {
return
}
}
if isEncrypted {
// 加密索引先持有生命周期租约再获取索引锁,因此删除也必须先结束生命周期,保持锁顺序一致。
unmount0(boxID)
}
databaseIndexDataLock.Lock()
defer databaseIndexDataLock.Unlock()
createDocLock.Lock()
defer createDocLock.Unlock()View on GitHub (pinned to 9f775e8a12)