siyuan-note/siyuan · error

The top-level notebook document cannot be removed or moved

Error message

The top-level notebook document cannot be removed or moved

What it means

Thrown by MoveDocs (kernel/model/file.go:1757) when one of the fromPaths resolves to the notebook's own top-level (box) document. IsBoxDocPath extracts the tree ID from the path via util.GetTreeID and compares it with the notebook ID (kernel/model/box_doc.go:317-324); equality means the path is /<boxID>.sy, the special root document of the notebook. Moving that document would be equivalent to moving or destroying the notebook itself, so it is rejected with Conf.Language(341).

Solutions

  1. Filter out paths whose basename without .sy equals the notebook ID before calling moveDocs (see exampleFix)
  2. If the goal is to move the whole notebook, there is no move API — export/import the notebook or recreate it, or use renameNotebook if only the name matters
  3. If the goal is to move all children of the notebook, list its child docs (path depth 1) and move those instead of the root doc

Example fix

// before
fetchPost('/api/filetree/moveDocs', {fromPaths: allSelectedPaths, toNotebook, toPath})

// after
const movable = allSelectedPaths.filter(p => {
  const base = p.split('/').pop().replace(/\.sy$/, '')
  return base !== fromNotebookId // skip the notebook's own root doc
})
if (movable.length) fetchPost('/api/filetree/moveDocs', {fromPaths: movable, toNotebook, toPath})
Defensive patterns

Strategy: validation

Validate before calling

// Strip notebook root docs (/<boxID>.sy) from a move batch
func filterBoxDocPaths(paths []string, boxes map[string]*Box) (ret []string) {
	for _, p := range paths {
		if box := boxes[p]; nil != box && !IsBoxDocPath(box.ID, p) {
			ret = append(ret, p)
		}
	}
	return
}

Type guard

func isMovableDocPath(boxID, p string) bool {
	return "" != util.GetTreeID(p) && util.GetTreeID(p) != boxID
}

Prevention

When it happens

Trigger: POST /api/filetree/moveDocs where fromPaths contains "/<notebookID>.sy" or a path whose base filename (minus .sy) equals the notebook ID. Typically happens when a client selects all documents in a notebook (the root doc is included) or computes parent paths and accidentally feeds the box doc path back in.

Common situations: Select-all + drag in the doc tree; bulk-move scripts that build paths by walking data/<box>/ and include the top-level <boxID>.sy file; tools that map a block ID back to a path without excluding the box document; new users trying to relocate the first top-level doc like a normal document.

Related errors


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/e4d2e6816d1ee98e. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/file.go:1757

		return
	}
	toPath = normalizeBoxDocTarget(toBoxID, toPath)
	pathsBoxes, err := getBoxesByPathsStrict(fromPaths)
	if err != nil {
		return
	}

	fromPaths = util.FilterMoveDocFromPaths(fromPaths, toPath)
	if 1 > len(fromPaths) {
		return
	}

	fromPaths = orderMoveDocPaths(fromPaths, pathsBoxes)

	for _, fromPath := range fromPaths {
		fromBox := pathsBoxes[fromPath]
		if nil != fromBox && IsBoxDocPath(fromBox.ID, fromPath) {
			return errors.New(Conf.Language(341))
		}
	}

	if 1 == len(fromPaths) {
		// 移动到自己的父文档下的情况相当于不移动,直接返回
		if fromBox := pathsBoxes[fromPaths[0]]; nil != fromBox && fromBox.ID == toBoxID {
			parentDir := path.Dir(fromPaths[0])
			if ("/" == toPath && "/" == parentDir) || (parentDir+".sy" == toPath) {
				return
			}
		}
	}

	// 检查路径深度是否超过限制
	for _, fromPath := range fromPaths {
		fromBox := pathsBoxes[fromPath]
		childDepth := util.GetChildDocDepth(filepath.Join(util.DataDir, fromBox.ID, fromPath))
		if depth := strings.Count(toPath, "/") + childDepth; 6 < depth && !Conf.FileTree.AllowCreateDeeper {

View on GitHub (pinned to afa823b6b4)