siyuan-note/siyuan · error

history path [%s] is not under history directory

Error message

history path [%s] is not under history directory

What it means

After confirming the path is inside the workspace and exists, validateHistoryPath computes its path relative to util.HistoryDir and rejects it if the relative path escapes the history directory (starts with '..') or Rel fails. Only files genuinely under data/history may be used as history paths, preventing misuse of arbitrary workspace files as history sources.

Source

Thrown at kernel/model/history.go:597

	}
	IncSync()
	util.PushMsg(Conf.Language(102), 3000)
	return nil
}

// validateHistoryPath 校验历史路径是否位于工作区内且属于历史目录。
// 拒绝路径穿越攻击(..、绝对路径等)。返回规范化的绝对路径。
func validateHistoryPath(historyPath string) (string, error) {
	p := filepath.Join(util.WorkspaceDir, historyPath)
	if !gulu.File.IsSubPath(util.WorkspaceDir, p) {
		return "", fmt.Errorf("history path [%s] is not in workspace", historyPath)
	}
	if !gulu.File.IsExist(p) {
		return "", fmt.Errorf("history path [%s] not exist", historyPath)
	}
	rel, err := filepath.Rel(util.HistoryDir, p)
	if err != nil || strings.HasPrefix(rel, "..") {
		return "", fmt.Errorf("history path [%s] is not under history directory", historyPath)
	}
	return p, nil
}

// IsEncryptedHistoryPath 判断历史路径是否明确属于加密笔记本。
func IsEncryptedHistoryPath(absPath string) bool {
	boxID := ExtractBoxIDFromHistoryPath(absPath)
	if boxID == "" {
		return false
	}
	if IsEncryptedBox(boxID) {
		return true
	}
	rel, err := filepath.Rel(util.HistoryDir, absPath)
	if err != nil {
		return false
	}
	parts := strings.SplitN(filepath.ToSlash(rel), "/", 3)

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Use only paths whose prefix is the workspace data/history directory as returned by history listing APIs
  2. Do not substitute live-file paths for history paths; locate the corresponding snapshot under data/history
  3. Rebuild/refresh the history index if the expected snapshot path resolves elsewhere

Example fix

// before: live path, not a history snapshot
rollbackAttributeViewHistory("data/storage/av/view.json")
// after: history snapshot under data/history
rollbackAttributeViewHistory("data/history/20240101120000-update/<boxID>/storage/av/view.json")
Defensive patterns

Strategy: validation

Validate before calling

function isUnderHistoryDir(historyPath) {
  const normalized = historyPath.replace(/\\/g, "/");
  return normalized.startsWith("history/") || normalized.includes("/history/");
}

Type guard

null

Try / catch

try { await rollbackDocHistory(p); } catch (e) { if (String(e.msg).includes("not under history directory")) { /* switch to the appropriate live-file API or locate the snapshot */ } else { throw e; } }

Prevention

When it happens

Trigger: Passing a workspace path that exists but is not under data/history — e.g. data/documents, data/assets, data/storage/av files directly, or a path resolving via symlink outside the history tree, to any of the history APIs.

Common situations: Confusing the live document path with its history snapshot; passing a storage/av live JSON instead of its history copy; scripting rollbacks with workspace-relative doc paths; attempting to 'restore' an asset by pointing at the live assets directory.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/ba18f766b1825196. Report an issue: GitHub.