fix: preserve shared derived file references in purge lookup
This commit is contained in:
parent
c27d920527
commit
a1ffe3b106
11
Dockerfile
11
Dockerfile
@ -48,13 +48,13 @@ CMD ["npm", "run", "dev", "--", "--hostname", "0.0.0.0"]
|
||||
# 构建阶段
|
||||
FROM base AS builder
|
||||
ARG ALPINE_MIRROR
|
||||
ARG VCS_REF
|
||||
# 安装构建依赖
|
||||
RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk/repositories && \
|
||||
apk add --no-cache libc6-compat python3 make g++ cairo-dev jpeg-dev pango-dev giflib-dev ffmpeg openssl
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ARG VCS_REF
|
||||
|
||||
# The source revision is immutable image metadata, not a runtime environment
|
||||
# assertion. Production builds must provide the exact 40-character Git SHA.
|
||||
@ -146,9 +146,7 @@ RUN echo "清理 standalone 目录..." && \
|
||||
# identity, exact source SHA and a hashed backup receipt before --apply works.
|
||||
FROM base AS migrator
|
||||
ARG ALPINE_MIRROR
|
||||
ARG VCS_REF
|
||||
WORKDIR /app
|
||||
LABEL org.opencontainers.image.revision=$VCS_REF
|
||||
ENV NODE_ENV=production
|
||||
RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk/repositories && \
|
||||
apk add --no-cache openssl postgresql-client && \
|
||||
@ -156,6 +154,8 @@ RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk
|
||||
command -v pg_dump >/dev/null && \
|
||||
command -v pg_restore >/dev/null && \
|
||||
command -v psql >/dev/null
|
||||
ARG VCS_REF
|
||||
LABEL org.opencontainers.image.revision=$VCS_REF
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/scripts/safe-production-migrate.mjs ./scripts/safe-production-migrate.mjs
|
||||
@ -174,11 +174,8 @@ ENTRYPOINT ["node", "scripts/safe-production-migrate.mjs"]
|
||||
|
||||
# 生产运行阶段
|
||||
FROM base AS runner
|
||||
ARG VCS_REF
|
||||
WORKDIR /app
|
||||
|
||||
LABEL org.opencontainers.image.revision=$VCS_REF
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
@ -189,6 +186,8 @@ RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk
|
||||
flock --exclusive --timeout 1 --conflict-exit-code 75 --no-fork \
|
||||
/tmp/material-upload-flock-probe.lock sh -c 'true' && \
|
||||
rm -f /tmp/material-upload-flock-probe.lock
|
||||
ARG VCS_REF
|
||||
LABEL org.opencontainers.image.revision=$VCS_REF
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs && \
|
||||
adduser --system --uid 1001 nextjs
|
||||
|
||||
@ -12,10 +12,13 @@
|
||||
|
||||
## 验证
|
||||
|
||||
- 21 个相关 Jest 测试,包括入队与空队列检查交错时不会丢失唤醒。
|
||||
- 23 个相关 Jest 测试,包括入队与空队列检查交错时不会丢失唤醒和进行中状态的 500ms 刷新。
|
||||
- 14 个真实 PostgreSQL/文件系统/MinIO 集成验证组;包括幂等、事务回滚、租约接管、真实删除、共享保护、直接 SQL 更新、队列状态变化、特殊地址和失败续跑。
|
||||
- 独立数据库 400,000 条额外资产:引用检查 59ms,提交 24ms,4 个文件从提交到实际删除及审计完成 122ms;资产查询计划实际使用 `material_assets_purge_references_idx`。
|
||||
- 已启动 worker 收到提交后 60ms 完成,不等待 5 秒轮询。
|
||||
- 独立数据库 400,000 条额外资产:引用检查 67ms,提交 31ms,4 个文件从提交到实际删除及审计完成 141ms;资产查询计划实际使用 `material_assets_purge_references_idx`。
|
||||
- 已启动 worker 收到提交后 92ms 完成,不等待 5 秒轮询。
|
||||
- 白底派生地址中的原始查询、片段和点段与原生 URL 解析一致;真实 MinIO 集成验证不会漏掉共享文件。
|
||||
- 类型检查、存储边界检查和 14 个安全迁移测试通过。迁移审计核对九个索引的表达式、状态谓词、有效性及三个函数的完整定义。
|
||||
|
||||
生产验收只读核对原先 18 个任务的 1,276 个文件和现存引用样本,不由自动化提交真实素材删除。隔离测试耗时不等于生产完整物理清理耗时;生产查询耗时和发布凭据另存于发布回执。
|
||||
|
||||
镜像构建将提交 SHA 参数放在固定系统依赖层之后,避免每次提交使编译器安装缓存失效;镜像标签、提交文件和发布时的精确 SHA 校验保持有效。
|
||||
|
||||
@ -89,6 +89,11 @@ BEGIN
|
||||
IF tail[1] = 'preview' THEN tail := tail[2:array_length(tail, 1)]; END IF;
|
||||
IF cardinality(tail) > 0 THEN
|
||||
derived := 'white-background/' || segments[wb - 1] || '/' || segments[wb + 1] || '/' || array_to_string(tail, '/');
|
||||
IF derived ~ E'[\\t\\r\\n]' OR derived ~* '/(\.|%2e)' THEN
|
||||
result := array_append(result, 'url:resolve');
|
||||
END IF;
|
||||
-- 派生地址会经过 URL 解析;原始 ?/# 是查询/片段,编码形式仍是文件名。
|
||||
derived := regexp_replace(derived, '[?#].*$', '');
|
||||
FOR round IN 0..6 LOOP
|
||||
result := array_append(result, 'derived:' || md5(derived));
|
||||
decoded := purge_decode_reference(derived);
|
||||
|
||||
@ -223,7 +223,7 @@ async function main() {
|
||||
await processNextMaterialPurge()
|
||||
await client.send(new HeadObjectCommand({ Bucket, Key: encodedKey }))
|
||||
const localWhiteBackground = `uploads/skus/${objectKeeper.id}/white-background/${run}/preview/shared-display.png`
|
||||
await prisma.materialAsset.create({ data: { materialId: objectKeeper.id, assetType: 'test', fileUrl: localWhiteBackground } })
|
||||
const whiteAsset = await prisma.materialAsset.create({ data: { materialId: objectKeeper.id, assetType: 'test', fileUrl: localWhiteBackground } })
|
||||
const { buildPublicDisplayUrl, buildWhiteBackgroundPublicDisplayPathFromAssetPath } = await import('../lib/public-display')
|
||||
const displaySource = buildPublicDisplayUrl(buildWhiteBackgroundPublicDisplayPathFromAssetPath(localWhiteBackground)!)!
|
||||
const { getTrustedS3MutationKey } = await import('../lib/s3')
|
||||
@ -233,6 +233,18 @@ async function main() {
|
||||
await enqueueMaterialPurge([display.id], admin.id)
|
||||
await processNextMaterialPurge()
|
||||
await client.send(new HeadObjectCommand({ Bucket, Key: displayKey }))
|
||||
const displayTarget = [{ ...identifyPurgeFile(displaySource)!, outcome: 'pending' as const }]
|
||||
for (const fileUrl of [localWhiteBackground + '#preview', localWhiteBackground + '?cache=1',
|
||||
localWhiteBackground.replace('/preview/shared-display', '/preview/./shared-display'),
|
||||
localWhiteBackground.replace('/preview/shared-display', '/preview/sub/../shared-display'),
|
||||
localWhiteBackground.replace('/preview/shared-display', '/preview/%2e/shared-display')]) {
|
||||
await prisma.materialAsset.update({ where: { id: whiteAsset.id }, data: { fileUrl } })
|
||||
const full = await findPurgeFileReferences(prisma)
|
||||
const next = await findPurgeFileReferences(prisma, displayTarget)
|
||||
assert(full.has(purgeFileIdentity(displayTarget[0])), '白底派生引用确实存在')
|
||||
assert(next.has(purgeFileIdentity(displayTarget[0])), `不得漏掉派生 URL 引用 ${fileUrl}`)
|
||||
}
|
||||
await prisma.materialAsset.update({ where: { id: whiteAsset.id }, data: { fileUrl: localWhiteBackground } })
|
||||
const referenceTarget = [{ ...identifyPurgeFile(source(sharedKey))!, outcome: 'pending' as const }]
|
||||
for (const previewUrl of [
|
||||
source(sharedKey).replace('shared', '%73hared'), source(sharedKey).replace('shared', '%2573hared'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user