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

  1. Only call RemoveBox with IDs returned by CreateBox/ListNotebooks; never craft IDs manually.
  2. If you maintain a reserved-name list mirror, check util.IsReservedFilename before invoking and reject.
  3. 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

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.