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

  1. Check the on-disk path: open <workspace>/data/<boxID> and confirm whether it is a file or directory (ls -la).
  2. If it is a stray file, back it up and delete or move it manually, then retry the notebook removal.
  3. 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.
  4. 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

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


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)