{"record":{"id":"30034b21850f9617","repo":"siyuan-note/siyuan","slug":"duplicate-oldpath-s","errorCode":null,"errorMessage":"duplicate oldPath: %s","messagePattern":"duplicate oldPath: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/asset_relink_batch.go","lineNumber":83,"sourceCode":"\tp := &assetRelinkPlan{ctx: ctx, batch: batch, lookup: lookup, assetRelinker: &assetRelinker{\n\t\tcollectOnly: true, routes: map[string]*assetRelinker{},\n\t\tresult: apicontract.AssetReferencesData{References: []apicontract.AssetReference{}, SkippedNotebooks: []string{}, DryRun: dryRun},\n\t}}\n\tif len(mappings) == 0 {\n\t\treturn p, errors.New(\"at least one asset path or mapping is required\")\n\t}\n\tseen := map[string]bool{}\n\tvar validationErr error\n\tfor _, mapping := range mappings {\n\t\trule, err := newAssetRelinker(mapping.OldPath, mapping.NewPath)\n\t\tif !lookup && mapping.NewPath == \"\" {\n\t\t\terr = errors.New(\"newPath is required\")\n\t\t}\n\t\titem := &assetRelinkItem{input: mapping, rule: rule}\n\t\tp.items = append(p.items, item)\n\t\tif rule.oldPath != \"\" {\n\t\t\tif seen[rule.oldPath] {\n\t\t\t\tvalidationErr = fmt.Errorf(\"duplicate oldPath: %s\", mapping.OldPath)\n\t\t\t}\n\t\t\tseen[rule.oldPath] = true\n\t\t}\n\t\tif err != nil {\n\t\t\titem.inputError = true\n\t\t\tp.fail(item, err.Error())\n\t\t\tcontinue\n\t\t}\n\t\tp.routes[rule.oldPath] = rule\n\t}\n\tif validationErr != nil {\n\t\treturn p, validationErr\n\t}\n\tfor _, item := range p.items {\n\t\tif item.reason == \"\" && item.rule.newPath != item.rule.oldPath && seen[item.rule.newPath] {\n\t\t\treturn p, errors.New(\"chained or cyclic asset mappings are not supported\")\n\t\t}\n\t}","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/asset_relink_batch.go#L65-L101","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Deduplicate mappings by OldPath before constructing the batch (keep the last/newest rule)","Detect duplicates in the UI when a row is added and merge or reject it there","Normalize paths (case, separators, leading 'assets/') before dedup so equivalent spellings collapse"],"exampleFix":"// before\nmappings := append(perNotebookA, perNotebookB...) // may contain dup oldPath\nmodel.RelinkAssets(ctx, mappings, false)\n// after\nseen := map[string]bool{}\nvar unique []apicontract.AssetRelinkMapping\nfor _, m := range mappings {\n\tif seen[m.OldPath] { continue }\n\tseen[m.OldPath] = true\n\tunique = append(unique, m)\n}\nmodel.RelinkAssets(ctx, unique, false)","handlingStrategy":"validation","validationCode":"seen := map[string]bool{}\nfor _, m := range mappings {\n\tif seen[m.OldPath] { return fmt.Errorf(\"duplicate oldPath %q\", m.OldPath) }\n\tseen[m.OldPath] = true\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Deduplicate mappings by normalized oldPath before batching","Merge duplicate UI rows on add","Normalize separators/case before comparing paths"],"tags":["go","validation","duplicate-input"],"backgroundTag":"conflicting-config-options","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}