Files
FileHost/server/plugins/startup.ts
T
Yeomai 89ea2fb25d
ci / ci (22, ubuntu-latest) (push) Canceled after 0s
feat: 新增视频床与文件床,视频支持 HLS 转码
文件床
- 新增 files/ 存储前缀与 media_type 索引列,图片、视频、文件三类资源独立
- 新增文件上传、列表、删除、批量删除接口,禁止 html/htm/svg
- 支持公开直链与 Token 访问两种模式,后台可切换,直链强制附件下载

视频床
- 新增 videos/ 存储前缀与视频上传接口,单文件上限 500MB
- 接入 ffmpeg 转码队列,并发数与单任务核心数可配,默认串行
- 五档转码质量:直接封装 / 速度 / 平衡 / 质量 / 压缩(压缩档可调码率比例)
- 码率控制避免转码后体积反超源文件,音频跟随源音轨不放大不压缩
- 转码进度实时上报,成功后删除源文件,失败时保留源文件与可用链接
- 上传时自动抽帧生成封面,视频与封面各有独立链接
- 删除视频时中断转码并清理切片、封面与源文件

媒体库
- 图库、视频库、文件库三套独立统计,含趋势、占用与来源分布
- 统一筛选、多选、批量删除、批量标签、网格/列表切换,显示方式按类型分别记忆
- 视频详情对齐图片详情,视频库支持在线播放(hls.js,兼容回退原文件)

修复
- 清理无主的切片、封面与开发期遗留的 ffmpeg 进程
- 源文件按设计删除后,索引不再被启动孤儿清理误删
- 视频统计口径与列表一致,转码中的视频不计入统计
- 转码失败时回收已写入的部分切片,避免残留
- 设置页停用检查更新(与上游版本号已不对应)

其他
- Docker 镜像安装 ffmpeg
- git remote 指向内网仓库
2026-10-17 03:14:57 +08:00

115 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
})()
})