siyuan-note/siyuan · error

duplicate oldPath

Error message

duplicate oldPath: %s

What it means

The same oldPath may appear only once in a batch of mappings. Duplicate oldPath values would make routing ambiguous (two different targets for one source), so the plan constructor collects the error and rejects the whole batch with this message.

Solutions

  1. Deduplicate mappings by OldPath before constructing the batch (keep the last/newest rule)
  2. Detect duplicates in the UI when a row is added and merge or reject it there
  3. Normalize paths (case, separators, leading 'assets/') before dedup so equivalent spellings collapse

Example fix

// before
mappings := append(perNotebookA, perNotebookB...) // may contain dup oldPath
model.RelinkAssets(ctx, mappings, false)
// after
seen := map[string]bool{}
var unique []apicontract.AssetRelinkMapping
for _, m := range mappings {
	if seen[m.OldPath] { continue }
	seen[m.OldPath] = true
	unique = append(unique, m)
}
model.RelinkAssets(ctx, unique, false)
Defensive patterns

Strategy: validation

Validate before calling

seen := map[string]bool{}
for _, m := range mappings {
	if seen[m.OldPath] { return fmt.Errorf("duplicate oldPath %q", m.OldPath) }
	seen[m.OldPath] = true
}

Prevention

When it happens

Trigger: Calling RelinkAssets or FindAssetReferencesBatch with two mappings whose normalized OldPath is identical, e.g. [{a.png -> b.png}, {a.png -> c.png}], or duplicated entries from concatenating request lists.

Common situations: Batch UI allowing the user to add the same source row twice; scripts merging per-notebook mapping lists without dedup; copy-paste of mapping JSON blocks.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


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

Appendix: source

Thrown at kernel/model/asset_relink_batch.go:83

	p := &assetRelinkPlan{ctx: ctx, batch: batch, lookup: lookup, assetRelinker: &assetRelinker{
		collectOnly: true, routes: map[string]*assetRelinker{},
		result: apicontract.AssetReferencesData{References: []apicontract.AssetReference{}, SkippedNotebooks: []string{}, DryRun: dryRun},
	}}
	if len(mappings) == 0 {
		return p, errors.New("at least one asset path or mapping is required")
	}
	seen := map[string]bool{}
	var validationErr error
	for _, mapping := range mappings {
		rule, err := newAssetRelinker(mapping.OldPath, mapping.NewPath)
		if !lookup && mapping.NewPath == "" {
			err = errors.New("newPath is required")
		}
		item := &assetRelinkItem{input: mapping, rule: rule}
		p.items = append(p.items, item)
		if rule.oldPath != "" {
			if seen[rule.oldPath] {
				validationErr = fmt.Errorf("duplicate oldPath: %s", mapping.OldPath)
			}
			seen[rule.oldPath] = true
		}
		if err != nil {
			item.inputError = true
			p.fail(item, err.Error())
			continue
		}
		p.routes[rule.oldPath] = rule
	}
	if validationErr != nil {
		return p, validationErr
	}
	for _, item := range p.items {
		if item.reason == "" && item.rule.newPath != item.rule.oldPath && seen[item.rule.newPath] {
			return p, errors.New("chained or cyclic asset mappings are not supported")
		}
	}

View on GitHub (pinned to 9f775e8a12)