180 lines
6.0 KiB
TypeScript
180 lines
6.0 KiB
TypeScript
/**
|
||
* 检查 video-source-* 格式的文件
|
||
* 可能这些文件还在,但数据库中的 videoSourceUrl 已经更新为 video-* 格式了
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client'
|
||
import { readdir, stat } from 'fs/promises'
|
||
import { existsSync } from 'fs'
|
||
import path from 'path'
|
||
|
||
const prisma = new PrismaClient()
|
||
|
||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'
|
||
const VIDEO_TYPES = ['主图视频', '安装视频', '种草视频']
|
||
|
||
async function main() {
|
||
console.log('==== 检查 video-source-* 格式的文件 ====\n')
|
||
|
||
// 1. 列出本地 uploads 目录中所有 video-source-*.mp4 文件
|
||
console.log('1. 检查本地 uploads 目录中的 video-source-*.mp4 文件:\n')
|
||
|
||
let videoSourceFiles: string[] = []
|
||
try {
|
||
const files = await readdir(UPLOAD_DIR)
|
||
videoSourceFiles = files.filter((f) => f.startsWith('video-source-') && f.endsWith('.mp4'))
|
||
console.log(`找到 ${videoSourceFiles.length} 个 video-source-*.mp4 文件\n`)
|
||
|
||
if (videoSourceFiles.length > 0) {
|
||
console.log('前 20 个文件:')
|
||
for (const file of videoSourceFiles.slice(0, 20)) {
|
||
const filePath = path.join(UPLOAD_DIR, file)
|
||
const stats = await stat(filePath)
|
||
console.log(` - ${file} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`)
|
||
}
|
||
if (videoSourceFiles.length > 20) {
|
||
console.log(` ... 还有 ${videoSourceFiles.length - 20} 个文件\n`)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log(`无法读取 uploads 目录: ${error}\n`)
|
||
}
|
||
|
||
// 2. 查询所有 videoSourceUrl 是本地路径但文件不存在的视频素材
|
||
console.log('\n2. 查询数据库中 videoSourceUrl 为本地路径但文件缺失的记录:\n')
|
||
|
||
const allVideos = await prisma.material.findMany({
|
||
where: {
|
||
deletedAt: null,
|
||
materialType: { in: VIDEO_TYPES } as any,
|
||
videoSourceUrl: {
|
||
not: null,
|
||
startsWith: '/uploads/video-',
|
||
} as any,
|
||
},
|
||
select: {
|
||
id: true,
|
||
materialType: true,
|
||
videoSourceUrl: true,
|
||
zipFileName: true,
|
||
createdAt: true,
|
||
},
|
||
})
|
||
|
||
// 找出缺失的文件
|
||
const missingFiles: typeof allVideos = []
|
||
for (const m of allVideos) {
|
||
const fileName = (m.videoSourceUrl as string).replace('/uploads/', '')
|
||
const localPath = path.join(UPLOAD_DIR, fileName)
|
||
if (!existsSync(localPath)) {
|
||
missingFiles.push(m)
|
||
}
|
||
}
|
||
|
||
console.log(`共找到 ${missingFiles.length} 条 videoSourceUrl 为本地路径但文件缺失的记录\n`)
|
||
|
||
// 3. 尝试匹配:检查这些缺失的文件是否对应 video-source-* 格式的文件
|
||
console.log('3. 尝试匹配:检查缺失的文件是否对应 video-source-* 格式的文件:\n')
|
||
|
||
let matchedCount = 0
|
||
const matchedFiles: Array<{
|
||
materialId: string
|
||
materialType: string
|
||
videoSourceUrl: string
|
||
videoSourceFile: string
|
||
}> = []
|
||
|
||
for (const material of missingFiles) {
|
||
const videoUrl = material.videoSourceUrl as string
|
||
const expectedFileName = videoUrl.replace('/uploads/video-', '')
|
||
|
||
// 尝试匹配 video-source-{materialId}-{timestamp}.mp4 格式
|
||
// 或者 video-source-{其他格式}.mp4
|
||
const possibleMatches = videoSourceFiles.filter((f) => {
|
||
// 如果文件名包含 materialId,可能是匹配的
|
||
if (f.includes(material.id)) {
|
||
return true
|
||
}
|
||
// 或者检查时间戳是否接近
|
||
const urlTimestamp = expectedFileName.match(/^([^-]+)/)?.[1]
|
||
const fileTimestamp = f.match(/video-source-[^-]+-(\d+)/)?.[1]
|
||
if (urlTimestamp && fileTimestamp) {
|
||
// 时间戳接近(相差不超过 1 小时)
|
||
const urlTime = parseInt(urlTimestamp)
|
||
const fileTime = parseInt(fileTimestamp)
|
||
const diff = Math.abs(urlTime - fileTime)
|
||
return diff < 3600000 // 1小时
|
||
}
|
||
return false
|
||
})
|
||
|
||
if (possibleMatches.length > 0) {
|
||
matchedCount++
|
||
matchedFiles.push({
|
||
materialId: material.id,
|
||
materialType: material.materialType,
|
||
videoSourceUrl: videoUrl,
|
||
videoSourceFile: possibleMatches[0],
|
||
})
|
||
|
||
if (matchedCount <= 10) {
|
||
console.log(`✅ 匹配到: [${material.id}] ${material.materialType}`)
|
||
console.log(` 数据库: ${videoUrl}`)
|
||
console.log(` 实际文件: ${possibleMatches[0]}\n`)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (matchedCount > 10) {
|
||
console.log(`... 还有 ${matchedCount - 10} 个匹配\n`)
|
||
}
|
||
|
||
console.log(`\n匹配结果:`)
|
||
console.log(` 找到 ${matchedCount} 个可能的匹配`)
|
||
console.log(` 还有 ${missingFiles.length - matchedCount} 个未匹配\n`)
|
||
|
||
// 4. 检查是否有 video-source-* 文件但数据库中没有对应记录
|
||
console.log('4. 检查是否有 video-source-* 文件但数据库中没有对应记录:\n')
|
||
|
||
const orphanedFiles: string[] = []
|
||
for (const file of videoSourceFiles) {
|
||
// 尝试从文件名提取 materialId
|
||
const materialIdMatch = file.match(/video-source-([a-f0-9-]{36})/)
|
||
if (materialIdMatch) {
|
||
const materialId = materialIdMatch[1]
|
||
const exists = allVideos.some((m) => m.id === materialId)
|
||
if (!exists) {
|
||
orphanedFiles.push(file)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (orphanedFiles.length > 0) {
|
||
console.log(`找到 ${orphanedFiles.length} 个孤立文件(数据库中没有对应记录):`)
|
||
orphanedFiles.slice(0, 10).forEach((f) => {
|
||
console.log(` - ${f}`)
|
||
})
|
||
if (orphanedFiles.length > 10) {
|
||
console.log(` ... 还有 ${orphanedFiles.length - 10} 个`)
|
||
}
|
||
} else {
|
||
console.log('没有找到孤立文件')
|
||
}
|
||
|
||
console.log('\n==== 检查完成 ====')
|
||
console.log(`\n💡 建议:`)
|
||
if (matchedCount > 0) {
|
||
console.log(`1. 发现 ${matchedCount} 个文件可能是 video-source-* 格式,需要重命名为 video-* 格式`)
|
||
console.log(`2. 或者更新数据库中的 videoSourceUrl 指向正确的文件名`)
|
||
}
|
||
console.log(`3. 检查 MinIO 中是否还有这些源文件(我们之前说过确认后再删除)`)
|
||
|
||
await prisma.$disconnect()
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error('脚本执行出错:', error)
|
||
process.exit(1)
|
||
})
|
||
|