gongxiangp2/lib/public-display.ts
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

143 lines
3.9 KiB
TypeScript

import { uploadFileToS3, uploadToS3 } from '@/lib/s3'
export const PUBLIC_DISPLAY_PREFIX = 'public-display'
function normalizeRelativePath(relativePath: string): string {
return String(relativePath || '')
.trim()
.replace(/\\/g, '/')
.replace(/^\/+/, '')
.replace(/\/{2,}/g, '/')
}
function normalizeS3PublicDomain(): string | null {
const rawDomain = String(process.env.S3_PUBLIC_DOMAIN || '').trim()
if (!rawDomain) {
return null
}
const normalized = rawDomain.match(/^https?:\/\//i) ? rawDomain : `https://${rawDomain}`
return normalized.replace(/\/+$/, '')
}
export function buildPublicDisplayObjectPath(relativePath: string): string {
const normalized = normalizeRelativePath(relativePath)
if (!normalized) {
return PUBLIC_DISPLAY_PREFIX
}
return `${PUBLIC_DISPLAY_PREFIX}/${normalized}`
}
function buildStorageObjectPath(objectPath: string): string {
const normalizedPrefix = normalizeRelativePath(process.env.S3_FILE_PATH || '')
const normalizedObjectPath = normalizeRelativePath(objectPath)
if (!normalizedPrefix) {
return normalizedObjectPath
}
return normalizeRelativePath(`${normalizedPrefix}/${normalizedObjectPath}`)
}
export function buildPublicDisplayUrl(relativePath: string): string | null {
const publicDomain = normalizeS3PublicDomain()
const objectPath = buildPublicDisplayObjectPath(relativePath)
if (!publicDomain || !objectPath) {
return null
}
return `${publicDomain}/${buildStorageObjectPath(objectPath)}`
}
export async function uploadPublicDisplayToS3(
file: Buffer,
relativePath: string,
contentType: string,
): Promise<string> {
return uploadToS3(file, buildPublicDisplayObjectPath(relativePath), contentType)
}
export async function uploadPublicDisplayFileToS3(
filePath: string,
relativePath: string,
contentType: string,
expectedSize?: number,
): Promise<string> {
return uploadFileToS3(
filePath,
buildPublicDisplayObjectPath(relativePath),
contentType,
expectedSize,
)
}
export function buildMaterialPublicDisplayPath(fileName: string): string {
return normalizeRelativePath(`materials/${fileName}`)
}
export function buildTaskDetailPublicDisplayPath(
materialId: string,
group: 'main' | 'sku' | 'detail',
fileName: string,
): string {
return normalizeRelativePath(`task-detail/${materialId}/${group}/${fileName}`)
}
export function buildCertificateReportPublicDisplayPath(
materialId: string,
group: 'pdf' | 'image' | 'preview',
fileName: string,
): string {
return normalizeRelativePath(`certificate-report/${materialId}/${group}/${fileName}`)
}
export function buildWhiteBackgroundPublicDisplayPath(
materialId: string,
skuId: string,
fileName: string,
): string {
return normalizeRelativePath(`white-background/${materialId}/${skuId}/${fileName}`)
}
export function buildWhiteBackgroundPublicDisplayPathFromAssetPath(
fileUrl?: string | null,
): string | null {
const normalizedFileUrl = String(fileUrl || '').trim().replace(/\\/g, '/')
if (!normalizedFileUrl || /^https?:\/\//i.test(normalizedFileUrl)) {
return null
}
const normalizedRelativePath = normalizedFileUrl.replace(/^\.?\//, '')
const segments = normalizedRelativePath.split('/').filter(Boolean)
const whiteBackgroundIndex = segments.indexOf('white-background')
if (
whiteBackgroundIndex < 0 ||
whiteBackgroundIndex < 2 ||
!segments[whiteBackgroundIndex - 1] ||
!segments[whiteBackgroundIndex + 1]
) {
return null
}
const materialId = segments[whiteBackgroundIndex - 1]
const skuId = segments[whiteBackgroundIndex + 1]
const fileSegments = segments.slice(whiteBackgroundIndex + 2)
if (fileSegments.length === 0) {
return null
}
if (fileSegments[0] === 'preview') {
fileSegments.shift()
}
if (fileSegments.length === 0) {
return null
}
return buildWhiteBackgroundPublicDisplayPath(materialId, skuId, fileSegments.join('/'))
}