Files
doodle-store/apps/server/scripts/asset-mapping.mjs
T

84 lines
3.1 KiB
JavaScript

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;
}