siyuan-note/siyuan · error
can not remove [%s] caused by it is a reserved file
Error message
can not remove [%s] caused by it is a reserved file
What it means
RemoveBox refuses to delete a notebook whose ID collides with a reserved filename: util.IsReservedFilename(boxID) is true, returning fmt.Errorf('can not remove [%s] caused by it is a reserved file', boxID). This protects SiYuan's own data-directory layout (names like data, temp, conf, .siyuan, assets, storage, etc.) from being removed via the notebook API.
Source
Thrown at kernel/model/mount.go:197
boundAVIDs, err := sql.QueryBoundBlockAVIDsInBox(nil, rootIDs, boxID)
if nil != err {
return nil, err
}
return groupDeletedAttributeViewBlocks(boundAVIDs), nil
}
func RemoveBox(boxID string) (err error) {
if !ast.IsNodeIDPattern(boxID) {
return errors.New("invalid notebook ID")
}
if _, loaded := boxLock.LoadOrStore(boxID, true); loaded {
err = errors.New(Conf.language(239))
return
}
defer boxLock.Delete(boxID)
if util.IsReservedFilename(boxID) {
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)View on GitHub (pinned to 251596fc0d)
Solutions
- Only call RemoveBox with IDs returned by CreateBox/ListNotebooks; never craft IDs manually.
- If you maintain a reserved-name list mirror, check util.IsReservedFilename before invoking and reject.
- Treat this error as a programming bug, not a user action — fix the caller to use the real notebook ID.
Example fix
// before
model.RemoveBox("conf") // reserved filename -> rejected
// after: use the real notebook ID from ListNotebooks
for _, nb := range mustListNotebooks() {
if nb.ID == targetID { model.RemoveBox(targetID); break }
} Defensive patterns
Strategy: validation
Validate before calling
if util.IsReservedFilename(boxID) {
return fmt.Errorf("%q is a reserved name; pass a real notebook ID", boxID)
} Type guard
null
Try / catch
null
Prevention
- Never craft notebook IDs manually; use IDs from CreateBox/ListNotebooks.
- Mirror the reserved-name check at API trust boundaries.
When it happens
Trigger: Calling RemoveBox with an ID equal to a reserved filename. Normally unreachable because real notebook IDs are generated by ast.NewNodeID and never match reserved names, but a crafted or mistaken ID that passed IsNodeIDPattern (797) yet equals a reserved name would hit this. It is a defense-in-depth guard before any destructive action.
Common situations: Direct/malformed API calls with hand-crafted IDs; a future reserved name added that collides with an existing ID (extremely unlikely given the ID format); integration tests using IDs that happen to match reserved names.
Related errors
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/0ba9ad4ce1d7b2d2.
Report an issue: GitHub.