165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
/**
|
||
* 检查缺失的本地视频文件
|
||
* 分析这些文件的 videoSourceUrl,判断是否需要从 MinIO 重新下载
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client'
|
||
import { access } from 'fs/promises'
|
||
import { constants as fsConstants } from 'fs'
|
||
import path from 'path'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
const S3_PUBLIC_DOMAIN = process.env.S3_PUBLIC_DOMAIN || ''
|
||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'
|
||
const VIDEO_TYPES = ['主图视频', '安装视频', '种草视频']
|
||
|
||
async function fileExists(fullPath: string): Promise<boolean> {
|
||
try {
|
||
await access(fullPath, fsConstants.F_OK)
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
console.log('==== 检查缺失的本地视频文件 ====\n')
|
||
|
||
// 查询所有视频类素材
|
||
const allVideos = await prisma.material.findMany({
|
||
where: {
|
||
deletedAt: null,
|
||
materialType: { in: VIDEO_TYPES } as any,
|
||
},
|
||
select: {
|
||
id: true,
|
||
materialType: true,
|
||
videoSourceUrl: true,
|
||
createdAt: true,
|
||
zipFileName: true,
|
||
zipFilePath: true,
|
||
},
|
||
orderBy: {
|
||
createdAt: 'desc',
|
||
},
|
||
})
|
||
|
||
// 找出所有本地路径但文件缺失的记录
|
||
const missingFiles: Array<{
|
||
id: string
|
||
materialType: string
|
||
videoSourceUrl: string
|
||
createdAt: Date
|
||
zipFileName: string | null
|
||
zipFilePath: string | null
|
||
}> = []
|
||
|
||
for (const m of allVideos) {
|
||
if (!m.videoSourceUrl) continue
|
||
|
||
const url = m.videoSourceUrl as string
|
||
if (!url.startsWith('/uploads/video-')) continue
|
||
|
||
const fileName = url.replace('/uploads/', '')
|
||
const fullPath = path.join(UPLOAD_DIR, fileName)
|
||
|
||
const exists = await fileExists(fullPath)
|
||
if (!exists) {
|
||
missingFiles.push({
|
||
id: m.id,
|
||
materialType: m.materialType,
|
||
videoSourceUrl: url,
|
||
createdAt: m.createdAt,
|
||
zipFileName: m.zipFileName,
|
||
zipFilePath: m.zipFilePath,
|
||
})
|
||
}
|
||
}
|
||
|
||
console.log(`共找到 ${missingFiles.length} 条本地路径但文件缺失的记录\n`)
|
||
|
||
if (missingFiles.length === 0) {
|
||
console.log('✅ 没有缺失的文件')
|
||
await prisma.$disconnect()
|
||
return
|
||
}
|
||
|
||
// 分析这些缺失文件
|
||
console.log('📊 分析缺失文件:\n')
|
||
|
||
// 按类型分组
|
||
const byType: Record<string, number> = {}
|
||
for (const m of missingFiles) {
|
||
byType[m.materialType] = (byType[m.materialType] || 0) + 1
|
||
}
|
||
|
||
console.log('按类型分布:')
|
||
for (const [type, count] of Object.entries(byType)) {
|
||
console.log(` ${type}: ${count} 条`)
|
||
}
|
||
console.log('')
|
||
|
||
// 检查是否有 zipFileName/zipFilePath 信息
|
||
const withZipInfo = missingFiles.filter((m) => m.zipFileName || m.zipFilePath)
|
||
const withoutZipInfo = missingFiles.filter((m) => !m.zipFileName && !m.zipFilePath)
|
||
|
||
console.log(`有 zipFileName/zipFilePath 信息: ${withZipInfo.length} 条`)
|
||
console.log(`无 zipFileName/zipFilePath 信息: ${withoutZipInfo.length} 条\n`)
|
||
|
||
// 检查这些文件的 videoSourceUrl 是否可能是从 S3 迁移过来的
|
||
// 如果文件名格式是 video-timestamp-random.mp4,可能是老的上传逻辑
|
||
// 如果文件名格式是 video-materialId-timestamp.mp4,可能是新的迁移逻辑
|
||
|
||
const oldFormat = missingFiles.filter((m) => {
|
||
const fileName = m.videoSourceUrl.replace('/uploads/video-', '')
|
||
// 老格式:video-timestamp-random.mp4(没有 materialId)
|
||
// 新格式:video-materialId-timestamp.mp4(有 materialId)
|
||
return !fileName.startsWith(m.id)
|
||
})
|
||
|
||
const newFormat = missingFiles.filter((m) => {
|
||
const fileName = m.videoSourceUrl.replace('/uploads/video-', '')
|
||
return fileName.startsWith(m.id)
|
||
})
|
||
|
||
console.log('按文件名格式分析:')
|
||
console.log(` 老格式 (video-timestamp-random.mp4): ${oldFormat.length} 条`)
|
||
console.log(` 新格式 (video-materialId-timestamp.mp4): ${newFormat.length} 条\n`)
|
||
|
||
// 显示前 20 条缺失文件的详细信息
|
||
console.log('缺失文件详情(前 20 条):\n')
|
||
missingFiles.slice(0, 20).forEach((m, index) => {
|
||
console.log(`${index + 1}. [${m.id}] ${m.materialType}`)
|
||
console.log(` videoSourceUrl: ${m.videoSourceUrl}`)
|
||
console.log(` 创建时间: ${m.createdAt.toISOString()}`)
|
||
console.log(` zipFileName: ${m.zipFileName || '(空)'}`)
|
||
console.log(` zipFilePath: ${m.zipFilePath || '(空)'}`)
|
||
console.log('')
|
||
})
|
||
|
||
if (missingFiles.length > 20) {
|
||
console.log(`... 还有 ${missingFiles.length - 20} 条未显示\n`)
|
||
}
|
||
|
||
// 检查这些文件是否可能在 MinIO 上
|
||
console.log('💡 建议:')
|
||
console.log(' 1. 这些文件的 videoSourceUrl 已经是本地路径格式,但本地文件不存在')
|
||
console.log(' 2. 可能的原因:')
|
||
console.log(' - 文件被手动删除')
|
||
console.log(' - 文件从未成功上传到本地(上传失败但数据库已更新)')
|
||
console.log(' - 文件路径变更或服务器迁移')
|
||
console.log(' 3. 如果这些文件还在 MinIO 上,需要:')
|
||
console.log(' - 根据 zipFileName/zipFilePath 或 videoSourceUrl 的文件名,')
|
||
console.log(' 在 MinIO 中查找对应的源文件')
|
||
console.log(' - 从 MinIO 下载到本地,并确保 videoSourceUrl 指向正确的本地路径')
|
||
|
||
await prisma.$disconnect()
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error('脚本执行出错:', error)
|
||
process.exit(1)
|
||
})
|
||
|