siyuan-note/siyuan · error
can not remove [%s] caused by it is not a dir
Error message
can not remove [%s] caused by it is not a dir
What it means
Returned by RemoveBox (kernel/model/mount.go:215) when the notebook path exists on disk (filelock.IsExist) but is not a directory (gulu.File.IsDir returns false). SiYuan notebooks must be directories under util.DataDir; a regular file at the notebook path is treated as an invalid state and removal is refused.
Source
Thrown at kernel/model/mount.go:215
return fmt.Errorf("can not remove [%s] caused by it is a reserved file", boxID)
}
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) {
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 isEncrypted {
// 加密索引先持有生命周期租约再获取索引锁,因此删除也必须先结束生命周期,保持锁顺序一致。
unmount0(boxID)
}
databaseIndexDataLock.Lock()
defer databaseIndexDataLock.Unlock()
createDocLock.Lock()
defer createDocLock.Unlock()
if !isEncrypted {
unmount0(boxID)
}
ClearRichClipboardBox(boxID)
if !isEncrypted {View on GitHub (pinned to 251596fc0d)
Solutions
- Inspect util.DataDir/<boxID> on disk; if it is a stray file, move or delete it manually (after confirming it is not a notebook dir that lost its directory bit), then retry the remove.
- If it is a symlink that got mangled, restore or remove the link and retry.
- Avoid naming unrelated files with 22-character base32 IDs that match IsNodeIDPattern to prevent future collisions.
Example fix
# before: file blocking removal
$ ls data/<boxID> # regular file
# after: remove the stray file, then retry the API
$ rm data/<boxID>
# retry: POST /api/notebook/removeNotebook {"notebook":"<boxID>"} Defensive patterns
Strategy: validation
Validate before calling
// Pre-check the path before calling RemoveBox.
p := filepath.Join(util.DataDir, boxID)
if info, err := os.Stat(p); err == nil && !info.IsDir() {
return fmt.Errorf("%s is not a directory; resolve on disk first", p)
}
return model.RemoveBox(boxID) Try / catch
if err := model.RemoveBox(boxID); err != nil {
if strings.Contains(err.Error(), "it is not a dir") {
// filesystem-level repair required, not a retry candidate
log.Warn("notebook path is not a directory; manual cleanup needed: %s", boxID)
}
return err
} Prevention
- Never create files under data/ that share a notebook ID format.
- Treat this error as a data-integrity signal — do not auto-retry without inspecting the path.
When it happens
Trigger: Calling /api/notebook/removeNotebook where <workspace>/data/<boxID> resolves to a file (someone created a file with the same name as a notebook ID), or a symlink/pipe/device was placed there manually.
Common situations: Manual file-system tampering, a partial/corrupted workspace, or another tool that wrote a file named like a notebook ID under data/.
Related errors
- Remove notebook [%s] path [%s] failed: %s
- invalid notebook ID
- can not remove [%s] caused by it is a reserved file
- import path is not sub path of import dir
- invalid box id
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/ff97fbe904c81512.
Report an issue: GitHub.