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

  1. 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.
  2. If it is a symlink that got mangled, restore or remove the link and retry.
  3. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/ff97fbe904c81512. Report an issue: GitHub.