From ede8cb7031fb769039585185f625a3555f32dc7e Mon Sep 17 00:00:00 2001 From: YeomaiPC Date: Sat, 17 Oct 2026 03:29:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=87=E4=BB=BD=E5=AF=BC=E5=87=BA=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/utils/backup/full-export.ts | 49 +++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/server/utils/backup/full-export.ts b/server/utils/backup/full-export.ts index a5ece98..4074e35 100644 --- a/server/utils/backup/full-export.ts +++ b/server/utils/backup/full-export.ts @@ -1,8 +1,9 @@ import { createWriteStream, promises as fs } from 'node:fs' -import { join } from 'node:path' +import { basename, join } from 'node:path' import { pipeline } from 'node:stream/promises' import * as tar from 'tar' import { getImageTotals, listAllImageIndexRows } from '../image-index' +import type { ImageIndexRow } from '../image-index' import { listStorageBackendRows } from '../storage-backends' @@ -47,6 +48,41 @@ async function writeStreamToFile(stream: NodeJS.ReadableStream, filePath: string await pipeline(stream, createWriteStream(filePath)) } +async function streamToString(stream: NodeJS.ReadableStream): Promise { + const chunks: Buffer[] = [] + for await (const chunk of stream) { + chunks.push(Buffer.from(chunk as Uint8Array)) + } + return Buffer.concat(chunks).toString('utf8') +} + +/** + * 源文件已按设计删除(转码完成)时,导出 HLS 播放列表、分片与封面。 + * 恢复端会遍历 blobs 目录原样还原,因此这里只需把附属文件写进去。 + */ +async function exportVideoSidecars(row: ImageIndexRow, stagingRoot: string): Promise { + const keys: string[] = [] + if (row.thumb_key) keys.push(row.thumb_key) + + if (row.hls_key) { + keys.push(row.hls_key) + const dir = row.hls_key.slice(0, row.hls_key.lastIndexOf('/')) + const text = await streamToString(await createImageStream(row.hls_key)) + for (const line of text.split('\n')) { + const name = line.trim() + if (!name || name.startsWith('#')) continue + keys.push(`${dir}/${basename(name)}`) + } + } + + for (const key of keys) { + await writeStreamToFile( + await createImageStream(key), + join(stagingRoot, 'blobs', key) + ) + } +} + export async function runFullExportJob(jobId: string, appVersion: string): Promise { const stagingRoot = getBackupStagingDir(jobId) const archiveName = formatArchiveName(jobId) @@ -105,9 +141,14 @@ export async function runFullExportJob(jobId: string, appVersion: string): Promi message: `正在导出图片 ${processed}/${totals.count + 1}…` }) - const blobPath = join(stagingRoot, 'blobs', row.key) - const stream = await createImageStream(row.key) - await writeStreamToFile(stream, blobPath) + if (Number(row.source_deleted ?? 0) === 1) { + // 转码完成的视频只有 HLS 产物与封面 + await exportVideoSidecars(row, stagingRoot) + } else { + const blobPath = join(stagingRoot, 'blobs', row.key) + const stream = await createImageStream(row.key) + await writeStreamToFile(stream, blobPath) + } processed += 1 updateBackupJob(jobId, { progress: processed }) }