- 视频上传生成缩略图并提供占位封面和失败原因展示 - 支持 HLS 按需重新封装 MP4,并限制临时文件生命周期与访问权限 - 完善视频、文件 API 文档、搜索、批量删除和单次转码质量参数 - 修复备份恢复与跨后端迁移遗漏缩略图、HLS 清单和分片的问题 - 将备份任务、归档和下载权限按用户隔离 - 增加仅清理当前用户数据的高危存储清空流程和二次文字确认 - 修复 S3 中文文件名元数据及 CLI 数据库连接问题 - 补充备份恢复、用户隔离、媒体资源和转码相关测试
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
import { getDataDir } from '../utils/data-dir'
|
||
import { promises as fs } from 'node:fs'
|
||
import { tmpdir } from 'node:os'
|
||
import { join } from 'node:path'
|
||
import { runAutoDeleteCleanup } from '../utils/auto-delete'
|
||
import { migrateLocalImagesToIndex, purgeOrphanImageIndexRows, reconcileLegacyImageKeys, repairLocalImageIndexMetadata } from '../utils/image-index'
|
||
import { ensureDefaultBackends } from '../utils/storage-backends'
|
||
import {
|
||
cleanupTranscodedSources,
|
||
killOrphanFfmpeg,
|
||
purgeOrphanVideoAssets,
|
||
resumePendingTranscodes
|
||
} from '../utils/transcode'
|
||
import { logException, logInfo } from '../utils/logger'
|
||
|
||
export default defineNitroPlugin(() => {
|
||
const port = process.env.NITRO_PORT || process.env.PORT || '6892'
|
||
logInfo('PicHost starting', {
|
||
port,
|
||
dataDir: getDataDir(),
|
||
nodeEnv: process.env.NODE_ENV || 'development',
|
||
imageBaseUrl: process.env.IMAGE_BASE_URL || '(request origin)',
|
||
storageBackend: process.env.STORAGE_BACKEND || 'db',
|
||
logLevel: process.env.LOG_LEVEL || 'info'
|
||
})
|
||
|
||
ensureDefaultBackends()
|
||
|
||
/** 清理上次异常退出留下的转码临时目录 */
|
||
void (async () => {
|
||
try {
|
||
const entries = await fs.readdir(tmpdir(), { withFileTypes: true })
|
||
for (const entry of entries) {
|
||
if (
|
||
!entry.isDirectory()
|
||
|| !entry.name.startsWith('pichost-hls-')
|
||
|| entry.name.startsWith('pichost-hls-download-')
|
||
) continue
|
||
await fs.rm(join(tmpdir(), entry.name), { recursive: true, force: true })
|
||
.catch(() => {})
|
||
}
|
||
} catch {
|
||
// 清理失败不影响启动
|
||
}
|
||
})()
|
||
|
||
void (async () => {
|
||
const dataDir = getDataDir()
|
||
try {
|
||
logInfo('开始同步存储索引:扫描本地磁盘', {
|
||
path: `${dataDir}/images/, ${dataDir}/files/, ${dataDir}/videos/`
|
||
})
|
||
|
||
const inserted = await migrateLocalImagesToIndex()
|
||
if (inserted > 0) {
|
||
logInfo('磁盘扫描完成:已补全缺失索引', { inserted })
|
||
} else {
|
||
logInfo('磁盘扫描完成:索引与磁盘一致,无需补全')
|
||
}
|
||
|
||
logInfo('开始归一化遗留图片路径(如 blog/ → images/blog/)')
|
||
const reconciled = await reconcileLegacyImageKeys()
|
||
if (reconciled.updated > 0 || reconciled.removed > 0) {
|
||
logInfo('遗留路径归一化完成', reconciled)
|
||
} else {
|
||
logInfo('遗留路径归一化完成:无需调整')
|
||
}
|
||
|
||
logInfo('开始校验索引与磁盘一致性,清理无文件的孤儿记录')
|
||
const purged = await purgeOrphanImageIndexRows()
|
||
if (purged > 0) {
|
||
logInfo('孤儿索引清理完成', { removed: purged })
|
||
} else {
|
||
logInfo('孤儿索引清理完成:未发现孤儿记录')
|
||
}
|
||
|
||
logInfo('开始修复存量 Content-Type 元数据')
|
||
const repaired = repairLocalImageIndexMetadata()
|
||
if (repaired > 0) {
|
||
logInfo('Content-Type 修复完成', { repaired })
|
||
} else {
|
||
logInfo('Content-Type 修复完成:无需修复')
|
||
}
|
||
|
||
logInfo('图片索引同步结束')
|
||
|
||
logInfo('开始恢复未完成的视频转码任务')
|
||
await killOrphanFfmpeg()
|
||
const purgedAssets = await purgeOrphanVideoAssets()
|
||
if (purgedAssets > 0) {
|
||
logInfo('清理无主的视频附属文件', { removed: purgedAssets })
|
||
}
|
||
const removedSources = await cleanupTranscodedSources()
|
||
if (removedSources > 0) {
|
||
logInfo('清理转码完成后的源视频', { removed: removedSources })
|
||
}
|
||
resumePendingTranscodes()
|
||
} catch (error) {
|
||
logException('图片索引同步失败', error)
|
||
}
|
||
})()
|
||
|
||
void (async () => {
|
||
try {
|
||
logInfo('开始执行启动时自动删除检查')
|
||
const result = await runAutoDeleteCleanup()
|
||
if (result.skipped) {
|
||
logInfo('自动删除检查完成:策略未启用')
|
||
} else if (result.deleted > 0 || result.failed > 0) {
|
||
logInfo('自动删除检查完成', { ...result })
|
||
} else {
|
||
logInfo('自动删除检查完成:无到期图片')
|
||
}
|
||
} catch (error) {
|
||
logException('启动时自动删除失败', error)
|
||
}
|
||
})()
|
||
})
|