siyuan-note/siyuan · error

Move notebook [%s] file [%s] failed: %s

Error message

Move notebook [%s] file [%s] failed: %s

What it means

In Box.Move, after both oldPath and newPath pass validateBoxPath, filelock.Rename(fromPath, toPath) is attempted; on failure it logs and returns errors.New(fmt.Sprintf(Conf.Language(5), box.Name, fromPath, err)) — 'Move notebook [%s] file [%s] failed: %s'. On success it also removes a now-empty ID-named source directory. Rename across filesystems can return EXDEV (cross-device link), and a busy/locked source produces EBUSY on Windows.

Source

Thrown at kernel/model/box.go:465

	IncSync()
	return nil
}

func (box *Box) Move(oldPath, newPath string) error {
	if _, err := box.validateBoxPath(oldPath); err != nil {
		return err
	}
	if _, err := box.validateBoxPath(newPath); err != nil {
		return err
	}
	boxLocalPath := filepath.Join(util.DataDir, box.ID)
	fromPath := filepath.Join(boxLocalPath, oldPath)
	toPath := filepath.Join(boxLocalPath, newPath)

	if err := filelock.Rename(fromPath, toPath); err != nil {
		msg := fmt.Sprintf(Conf.Language(5), box.Name, fromPath, err)
		logging.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, box.Name, err)
		return errors.New(msg)
	}

	if oldDir := path.Dir(oldPath); ast.IsNodeIDPattern(path.Base(oldDir)) {
		fromDir := filepath.Join(boxLocalPath, oldDir)
		if util.IsEmptyDir(fromDir) {
			filelock.Remove(fromDir)
		}
	}
	IncSync()
	return nil
}

func (box *Box) Remove(path string) error {
	if _, err := box.validateBoxPath(path); err != nil {
		return err
	}
	boxLocalPath := filepath.Join(util.DataDir, box.ID)
	filePath := filepath.Join(boxLocalPath, path)

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Ensure source and destination are on the same filesystem (no symlinks splitting the notebook across volumes).
  2. Check the target does not already exist, or overwrite intentionally first.
  3. Close other tools locking the source file (sync clients, editors, AV) and retry.
  4. On EXDEV, fall back to copy-then-delete if a cross-volume move is genuinely required.

Example fix

// before: bare rename across possible volume boundary
if err := box.Move(oldPath, newPath); err != nil { return err }

// after: detect EXDEV and copy+remove as a fallback
if err := box.Move(oldPath, newPath); err != nil {
    if errors.Is(err, syscall.EXDEV) {
        return copyThenRemove(box, oldPath, newPath)
    }
    return err
}
Defensive patterns

Strategy: fallback

Validate before calling

// Ensure source and destination share a filesystem before renaming.
sameFS, _ := sameDevice(fromPath, toPath)
if !sameFS { /* use copy+remove instead */ }

Try / catch

// Fall back to copy-then-remove on cross-device rename.
if err := box.Move(oldPath, newPath); err != nil {
    if errors.Is(err, syscall.EXDEV) { return copyThenRemove(box, oldPath, newPath) }
    return err
}

Prevention

When it happens

Trigger: POST /api/filetree/moveDocs (and renameDocByID heading2doc/li2doc reshuffles) moving a doc/folder where the target exists, the source is open/locked by another process, the two paths sit on different filesystems (EXDEV), or permissions deny the rename.

Common situations: Workspace on one volume with a notebook symlinked to another; target filename already present; the source .sy is held open by a sync/AV process on Windows; moving into a path that was just removed by another client.

Related errors


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