feat: 完成现有小程序接口的全部迁移至新后端
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
/** 仅接收成功上传并校验的预览对象;对象身份不包含 CDN 域名。 */
|
||||
export function buildAssetMappings(report, assets, config) {
|
||||
if (
|
||||
report.schemaVersion !== 1 ||
|
||||
report.mode !== 'apply' ||
|
||||
report.ok !== true ||
|
||||
!Array.isArray(report.files)
|
||||
)
|
||||
throw new Error('INVALID_SYNC_REPORT');
|
||||
const files = new Map();
|
||||
for (const file of report.files) {
|
||||
if (!file.relativePath?.startsWith('assets/previews/')) continue;
|
||||
const parts = file.relativePath.split('/');
|
||||
if (
|
||||
parts.some((part) => !part || part === '.' || part === '..') ||
|
||||
file.relativePath.includes('\\') ||
|
||||
!['uploaded', 'skipped'].includes(file.status) ||
|
||||
file.verifiedBy !== 'qiniu-etag-and-size' ||
|
||||
file.bucket !== config.publicBucket ||
|
||||
file.key !== `${config.prefix}${file.relativePath}` ||
|
||||
file.visibility !== 'public' ||
|
||||
!file.mimeType?.startsWith('image/') ||
|
||||
!Number.isSafeInteger(file.size) ||
|
||||
file.size < 0 ||
|
||||
!/^[a-f0-9]{64}$/.test(file.sha256) ||
|
||||
typeof file.etag !== 'string' ||
|
||||
files.has(file.relativePath)
|
||||
)
|
||||
throw new Error('INVALID_REPORT_FILE');
|
||||
files.set(file.relativePath, file);
|
||||
}
|
||||
const mappings = [];
|
||||
for (const asset of assets) {
|
||||
if (
|
||||
asset.purpose !== 'preview' ||
|
||||
!asset.sourceRef?.startsWith('cloud://')
|
||||
)
|
||||
continue;
|
||||
const url = new URL(asset.sourceRef);
|
||||
if (
|
||||
!url.hostname ||
|
||||
url.search ||
|
||||
url.hash ||
|
||||
asset.sourceRefHash !==
|
||||
createHash('sha256').update(asset.sourceRef).digest('hex')
|
||||
)
|
||||
throw new Error('INVALID_SOURCE_REFERENCE');
|
||||
const path = decodeURIComponent(url.pathname.slice(1));
|
||||
const file = files.get(path);
|
||||
if (
|
||||
!file ||
|
||||
asset.visibility !== 'public' ||
|
||||
asset.ownerUserId !== null ||
|
||||
!['pending', 'ready'].includes(asset.status)
|
||||
)
|
||||
throw new Error('ASSET_MAPPING_MISSING_OR_INELIGIBLE');
|
||||
const data = {
|
||||
provider: 'qiniu',
|
||||
bucket: file.bucket,
|
||||
objectKey: file.key,
|
||||
objectRefHash: createHash('sha256')
|
||||
.update(JSON.stringify(['qiniu', file.bucket, file.key]))
|
||||
.digest('hex'),
|
||||
externalUrl: null,
|
||||
mimeType: file.mimeType,
|
||||
sizeBytes: BigInt(file.size),
|
||||
checksum: file.sha256,
|
||||
status: 'ready',
|
||||
lastError: null,
|
||||
};
|
||||
const unchanged = Object.entries(data).every(
|
||||
([key, value]) => asset[key] === value,
|
||||
);
|
||||
// 已就绪对象只能重复确认同一映射,禁止工具悄悄覆盖之后的编辑。
|
||||
if (asset.status === 'ready' && !unchanged)
|
||||
throw new Error('READY_ASSET_CONFLICT');
|
||||
mappings.push({ asset, file, data, unchanged });
|
||||
}
|
||||
if (!mappings.length) throw new Error('NO_MATCHING_ASSETS');
|
||||
return mappings;
|
||||
}
|
||||
Reference in New Issue
Block a user