跳至內容
Astera Technology

保留相對附件連結,搬移後為何卻指錯檔案?

先依舊基準解原連結,對回實際物件,再按新位置建立參考。

下載完整個案資料及參考答案 ↓

先核來源與已訂規則

本例舊 docs/record 的 ../files/a,物件搬到 target/assets/a.bin,新紀錄卻在 target/pages。照留原相對字樣,會錯指 target/files/a。內部參考須有已附物件對應,外部來源按此規則仍作外部。

已填假設輸入
紀錄輸入 JSON
原例輸入{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]}
下載可編輯 CSV ↓

比較兩條實際路徑

更正新參考 ../assets/a.bin,才解到已對應檔案。建立相對參考時,目標的查詢參數及片段也須照留。所有 example.invalid 網址只在本地解路徑,沒有發請求。

錯誤與更正路徑的輸出
路徑輸出 JSON
錯誤路徑{"accepted":true,"reference":"../files/a","resolved":"https://files.example.invalid/target/files/a"}
更正路徑{"accepted":true,"reference":"../assets/a.bin","resolved":"https://files.example.invalid/target/assets/a.bin"}
下載可編輯 CSV ↓
本地 Node.js 程序:22.23.3 或 24.14.1
import {DatabaseSync} from 'node:sqlite';
import {TextDecoder} from 'node:util';
import {inflateSync} from 'node:zlib';
import {posix} from 'node:path';
import {createHash} from 'node:crypto';
const rows=(db,sql)=>db.prepare(sql).all().map(r=>({...r}));
const failed=(action)=>{try{action();return null;}catch(e){return {code:e.errcode??e.code??e.name};}};

import assert from 'node:assert/strict';
const run=function run(i,corrected=true){
    if(!corrected)return {accepted:true,reference:i.reference,resolved:new URL(i.reference,i.newBase).href};
    const old=new URL(i.reference,i.oldBase);const oldBase=new URL(i.oldBase);
    if(old.origin!==oldBase.origin)return {accepted:true,reference:i.reference,resolved:old.href};
    const target=i.mapping.find(m=>m.source===old.href);
    if(!target)return {accepted:false,error:'unmapped-internal-reference',original:i.reference,resolvedSource:old.href};
    const dest=new URL(target.destination);const newBase=new URL(i.newBase);
    const reference=dest.origin===newBase.origin?posix.relative(posix.dirname(newBase.pathname),dest.pathname)+dest.search+dest.hash:dest.href;
    return {accepted:true,reference,resolved:new URL(reference,i.newBase).href};
  };
const cases=[{"label":"misleading base","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]},"expected":{"accepted":true,"reference":"../files/a","resolved":"https://files.example.invalid/target/files/a"},"corrected":false},{"label":"corrected base","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin"}]},"expected":{"accepted":true,"reference":"../assets/a.bin","resolved":"https://files.example.invalid/target/assets/a.bin"},"corrected":true},{"label":"external absolute reference retained","input":{"reference":"https://outside.example.invalid/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]},"expected":{"accepted":true,"reference":"https://outside.example.invalid/a","resolved":"https://outside.example.invalid/a"},"corrected":true},{"label":"unmapped internal object held","input":{"reference":"../files/missing","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]},"expected":{"accepted":false,"error":"unmapped-internal-reference","original":"../files/missing","resolvedSource":"https://files.example.invalid/pack/files/missing"},"corrected":true},{"label":"mapped query and fragment retained","input":{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin?revision=2#part"}]},"expected":{"accepted":true,"reference":"../assets/a.bin?revision=2#part","resolved":"https://files.example.invalid/target/assets/a.bin?revision=2#part"},"corrected":true}];
for(const test of cases){
  const actual=run(test.input,test.corrected);
  assert.deepEqual(actual,test.expected,test.label);
  console.log(test.label,actual);
}
下載可執行例子 ↓

下次載入前核邊界

外部絕對參考照留;內部未有對應,連已解舊目標一起暫停。已對應的 ?revision=2#part,須在新參考及解出的網址都保留。

可編輯邊界紀錄及預期輸出
情況輸入 JSON預期輸出 JSON
外部絕對參考{"reference":"https://outside.example.invalid/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]}{"accepted":true,"reference":"https://outside.example.invalid/a","resolved":"https://outside.example.invalid/a"}
內部物件尚無對應{"reference":"../files/missing","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[]}{"accepted":false,"error":"unmapped-internal-reference","original":"../files/missing","resolvedSource":"https://files.example.invalid/pack/files/missing"}
對應目標的查詢參數及片段{"reference":"../files/a","oldBase":"https://files.example.invalid/pack/docs/record","newBase":"https://files.example.invalid/target/pages/record","mapping":[{"source":"https://files.example.invalid/pack/files/a","destination":"https://files.example.invalid/target/assets/a.bin?revision=2#part"}]}{"accepted":true,"reference":"../assets/a.bin?revision=2#part","resolved":"https://files.example.invalid/target/assets/a.bin?revision=2#part"}
下載可編輯 CSV ↓

新紀錄可否直接沿用 ../files/a?

查看參考答案

不可,新基準會改它的目標。須用已對應的 ../assets/a.bin。

相關資源