Files
bt/server/api/torrent/[infohash]/preview.get.ts
T

92 lines
3.4 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 { loadSettings } from '../../../utils/appconfig'
import { withSwarm } from '../../../utils/swarm'
const IMAGE_TYPES: Record<string, string> = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
bmp: 'image/bmp',
avif: 'image/avif'
}
/** 超过这个大小就不做在线预览了,从 swarm 拉一张大图的代价太高 */
const MAX_PREVIEW_BYTES = 12 * 1024 * 1024
/** 单张图片的下载时间上限 */
const DOWNLOAD_TIMEOUT_MS = 25_000
function normPath(value: string): string {
return value.replace(/\\/g, '/').replace(/^\.\//, '')
}
/**
* 从 swarm 里只把某一张图片的字节拉下来用于预览。
* 只下载该文件覆盖到的分片,不落库、不长期占用磁盘。
*/
export default defineEventHandler(async (event) => {
const infoHash = String(getRouterParam(event, 'infohash') || '').toLowerCase()
if (!/^[a-f0-9]{40}$/.test(infoHash)) {
throw createError({ statusCode: 400, statusMessage: 'infoHash 格式不正确' })
}
const query = getQuery(event)
const magnet = String(query.magnet || '')
const filePath = String(query.path || '')
if (!/^magnet:\?/i.test(magnet)) {
throw createError({ statusCode: 400, statusMessage: '缺少可用的磁力链,无法定位做种节点' })
}
if (!filePath) {
throw createError({ statusCode: 400, statusMessage: '缺少 path 参数' })
}
const ext = filePath.slice(filePath.lastIndexOf('.') + 1).toLowerCase()
const mime = IMAGE_TYPES[ext]
if (!mime) {
throw createError({ statusCode: 400, statusMessage: '只支持预览图片文件(jpg / png / gif / webp / bmp / avif)' })
}
const settings = await loadSettings()
try {
const bytes = await withSwarm(infoHash, magnet, settings.metadataTimeout, async (torrent) => {
const target = (torrent.files || []).find(
(f: any) => normPath(String(f.path || f.name || '')) === normPath(filePath)
)
if (!target) {
throw createError({ statusCode: 404, statusMessage: '这个种子里没有该文件,文件清单可能已过期' })
}
if (target.length > MAX_PREVIEW_BYTES) {
throw createError({
statusCode: 413,
statusMessage: `这张图 ${Math.round(target.length / 1024 / 1024)} MB,超过在线预览上限 ${MAX_PREVIEW_BYTES / 1024 / 1024} MB`
})
}
return Promise.race<ArrayBuffer>([
target.arrayBuffer(),
new Promise<never>((_, reject) => {
setTimeout(
() => reject(new Error('下载图片超时:暂时没有可用的做种节点,稍后再试')),
DOWNLOAD_TIMEOUT_MS
)
})
])
})
setHeader(event, 'content-type', mime)
setHeader(event, 'content-length', String(bytes.byteLength))
setHeader(
event,
'content-disposition',
`inline; filename="${encodeURIComponent(filePath.slice(filePath.lastIndexOf('/') + 1))}"`
)
setHeader(event, 'cache-control', 'private, max-age=3600')
return Buffer.from(bytes)
} catch (err) {
// 把 withSwarm 抛出的原始错误(如 webtorrent 无法加载)转成可读的 HTTP 错误,
// 避免前端看到 Nitro 的通用 Server Error 页面。
if (err && typeof err === 'object' && 'statusCode' in err) throw err
const message = err instanceof Error ? err.message : '从做种节点获取图片失败'
throw createError({ statusCode: 503, statusMessage: message })
}
})