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
- Filter out paths whose basename without .sy equals the notebook ID before calling moveDocs (see exampleFix)
- 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
- 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 building 'all documents' batches, exclude paths whose basename minus .sy equals the notebook ID
- Remember RenameDoc routes box docs to RenameBox — for moves there is no notebook-move equivalent; plan UX accordingly
- Log any filtered root-doc paths so silent no-ops are visible in tooling
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
- The current settings do not allow the creation of…
- 106
- 199
- block operation context
- Cannot create a file starting with .
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)