ci / ci (22, ubuntu-latest) (push) Canceled after 0s
文件床 - 新增 files/ 存储前缀与 media_type 索引列,图片、视频、文件三类资源独立 - 新增文件上传、列表、删除、批量删除接口,禁止 html/htm/svg - 支持公开直链与 Token 访问两种模式,后台可切换,直链强制附件下载 视频床 - 新增 videos/ 存储前缀与视频上传接口,单文件上限 500MB - 接入 ffmpeg 转码队列,并发数与单任务核心数可配,默认串行 - 五档转码质量:直接封装 / 速度 / 平衡 / 质量 / 压缩(压缩档可调码率比例) - 码率控制避免转码后体积反超源文件,音频跟随源音轨不放大不压缩 - 转码进度实时上报,成功后删除源文件,失败时保留源文件与可用链接 - 上传时自动抽帧生成封面,视频与封面各有独立链接 - 删除视频时中断转码并清理切片、封面与源文件 媒体库 - 图库、视频库、文件库三套独立统计,含趋势、占用与来源分布 - 统一筛选、多选、批量删除、批量标签、网格/列表切换,显示方式按类型分别记忆 - 视频详情对齐图片详情,视频库支持在线播放(hls.js,兼容回退原文件) 修复 - 清理无主的切片、封面与开发期遗留的 ffmpeg 进程 - 源文件按设计删除后,索引不再被启动孤儿清理误删 - 视频统计口径与列表一致,转码中的视频不计入统计 - 转码失败时回收已写入的部分切片,避免残留 - 设置页停用检查更新(与上游版本号已不对应) 其他 - Docker 镜像安装 ffmpeg - git remote 指向内网仓库
115 lines
4.0 KiB
TypeScript
115 lines
4.0 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-')) 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)
|
||
}
|
||
})()
|
||
})
|