flnxi-Wu/lib/material-storage-cleanup.ts

121 lines
3.3 KiB
TypeScript

import { unlink } from 'fs/promises'
import { isAbsolute } from 'path'
import { deleteFromS3, isS3Url } from '@/lib/s3'
export interface MaterialStorageLike {
zipFilePath?: string | null
previewUrl?: string | null
previewUrls?: string | null
displayImages?: string | null
videoSourceUrl?: string | null
videoPreview540Url?: string | null
videoPreviewClipUrl?: string | null
videoCoverUrl?: string | null
}
export interface MaterialStorageCleanupFailure {
fileUrl: string
reason: string
}
export interface MaterialStorageCleanupResult {
deletedFiles: number
skippedFiles: number
failedFiles: number
failures: MaterialStorageCleanupFailure[]
}
function parseUrlList(value?: string | null): string[] {
if (!value) return []
try {
const parsed = JSON.parse(value)
if (Array.isArray(parsed)) {
return parsed.filter((url): url is string => typeof url === 'string' && url.trim().length > 0)
}
} catch {
// 兼容历史单值字段。
}
return value.trim() ? [value] : []
}
export function collectMaterialStorageUrls(material: MaterialStorageLike): string[] {
const urls = [
material.zipFilePath,
material.videoSourceUrl && material.videoSourceUrl !== material.zipFilePath ? material.videoSourceUrl : null,
material.previewUrl,
material.videoPreview540Url,
material.videoPreviewClipUrl,
material.videoCoverUrl,
...parseUrlList(material.previewUrls),
...parseUrlList(material.displayImages),
]
return Array.from(new Set(urls.filter((url): url is string => Boolean(url?.trim()))))
}
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message
if (typeof error === 'string') return error
return '未知错误'
}
function isExplicitLocalPath(fileUrl: string): boolean {
return fileUrl.startsWith('file://') ||
isAbsolute(fileUrl) ||
/^[a-zA-Z]:[\\/]/.test(fileUrl) ||
fileUrl.startsWith('./') ||
fileUrl.startsWith('../')
}
function shouldDeleteFromS3(fileUrl: string): boolean {
if (isS3Url(fileUrl)) return true
const storageProvider = (process.env.STORAGE_PROVIDER || '').trim().toLowerCase()
return storageProvider === 's3' && !isExplicitLocalPath(fileUrl)
}
export async function deleteMaterialStorageFiles(
fileUrls: string[],
options: { logPrefix?: string } = {}
): Promise<MaterialStorageCleanupResult> {
const logPrefix = options.logPrefix || '[MaterialStorageCleanup]'
const uniqueFiles = Array.from(new Set(fileUrls.filter((url) => url && url.trim())))
const result: MaterialStorageCleanupResult = {
deletedFiles: 0,
skippedFiles: 0,
failedFiles: 0,
failures: [],
}
for (const fileUrl of uniqueFiles) {
try {
if (shouldDeleteFromS3(fileUrl)) {
await deleteFromS3(fileUrl)
result.deletedFiles++
continue
}
try {
await unlink(fileUrl)
result.deletedFiles++
} catch (fileError: any) {
if (fileError?.code === 'ENOENT') {
result.skippedFiles++
continue
}
throw fileError
}
} catch (error) {
const reason = getErrorMessage(error)
console.error(`${logPrefix} Failed to delete file: ${fileUrl}`, error)
result.failedFiles++
result.failures.push({ fileUrl, reason })
}
}
return result
}