siyuan-note/siyuan · error

invalid history source path

Error message

invalid history source path

What it means

apply() snapshots a source asset into the history directory before relinking. After re-reading the file and confirming it is unchanged, it computes the file's path relative to the workspace data directory; if that relative path cannot be computed or escapes the data directory (a ".." prefix), it refuses to write history outside the workspace and throws this error.

Solutions

  1. Ensure all asset file paths passed into the relink batch are real paths under util.DataDir (use filepath.EvalSymlinks and relativize before queueing)
  2. Verify the process is using the same DataDir as when the file list was built
  3. Re-scan assets after moving or renaming the workspace so stale paths are refreshed

Example fix

// before
rel, relErr := filepath.Rel(util.DataDir, file.path) // file.path points outside DataDir
// after
abs, _ := filepath.EvalSymlinks(file.path)
rel, relErr := filepath.Rel(util.DataDir, abs)
if relErr != nil || strings.HasPrefix(rel, "..") { skip the file }
Defensive patterns

Strategy: validation

Validate before calling

rel, err := filepath.Rel(util.DataDir, path)
if err != nil || strings.HasPrefix(rel, "..") { /* skip file, do not queue for history */ }

Type guard

func inDataDir(p string) bool { rel, err := filepath.Rel(util.DataDir, p); return err == nil && !strings.HasPrefix(rel, "..") }

Prevention

When it happens

Trigger: Calling run() (or the test TestAssetRelinkSourceChangedDuringScan) with an asset file whose path is not inside util.DataDir, e.g. a symlinked or absolute path that resolves outside the data directory, or when filepath.Rel fails due to mismatched path bases.

Common situations: Workspace moved or data directory relocated so cached file paths point outside it; symlinked assets pointing to external locations; running the relink batch with paths collected from a different workspace.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/e2f3158133d3c9ed. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/asset_relink_batch.go:461

	if err != nil {
		return err
	}
	p.result.HistoryPath = filepath.ToSlash(historyDir)
	// 所有候选源先备份;备份失败属于请求级错误,此时尚未修改任何引用。
	for _, file := range files {
		if err = p.checkContext(); err != nil {
			return err
		}
		if file.before == nil {
			continue
		}
		current, readErr := filelock.ReadFile(file.path)
		if readErr != nil || !bytes.Equal(current, file.before) {
			return fmt.Errorf("source changed during scan: %s", file.path)
		}
		rel, relErr := filepath.Rel(util.DataDir, file.path)
		if relErr != nil || strings.HasPrefix(rel, "..") {
			return errors.New("invalid history source path")
		}
		dest := filepath.Join(historyDir, rel)
		if err = os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
			return err
		}
		if err = gulu.File.WriteFileSafer(dest, file.before, 0644); err != nil {
			return err
		}
	}
	indexHistoryDir(filepath.Base(historyDir), util.NewLute())
	changedViews, reload := map[string]bool{}, map[string]bool{}
	defer func() {
		for _, tree := range p.trees {
			viewChanged := false
			ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
				if entering && n.Type == ast.NodeAttributeView && changedViews[n.AttributeViewID] {
					viewChanged = true
				}

View on GitHub (pinned to 9f775e8a12)