fix: 迁移图片资源保留原始dat

This commit is contained in:
cc
2026-04-25 22:55:28 +08:00
parent 8e2a6ec933
commit 32aab8d490

View File

@@ -7,7 +7,6 @@ import * as tar from 'tar'
import { ConfigService } from './config' import { ConfigService } from './config'
import { wcdbService } from './wcdbService' import { wcdbService } from './wcdbService'
import { expandHomePath } from '../utils/pathUtils' import { expandHomePath } from '../utils/pathUtils'
import { decryptDatViaNative, encryptDatViaNative, type NativeDatMeta } from './nativeImageDecrypt'
type BackupDbKind = 'session' | 'contact' | 'emoticon' | 'message' | 'media' | 'sns' | 'hardlink' type BackupDbKind = 'session' | 'contact' | 'emoticon' | 'message' | 'media' | 'sns' | 'hardlink'
type BackupPhase = 'preparing' | 'scanning' | 'exporting' | 'packing' | 'inspecting' | 'restoring' | 'done' | 'failed' type BackupPhase = 'preparing' | 'scanning' | 'exporting' | 'packing' | 'inspecting' | 'restoring' | 'done' | 'failed'
@@ -48,7 +47,6 @@ interface BackupResourceEntry {
targetRelativePath: string targetRelativePath: string
ext?: string ext?: string
size?: number size?: number
datMeta?: NativeDatMeta
} }
interface BackupManifest { interface BackupManifest {
@@ -290,27 +288,6 @@ export class BackupService {
return suffixMatch ? suffixMatch[1] : trimmed return suffixMatch ? suffixMatch[1] : trimmed
} }
private parseImageXorKey(value: unknown): number {
if (typeof value === 'number') return value
const text = String(value ?? '').trim()
if (!text) return Number.NaN
return text.toLowerCase().startsWith('0x') ? parseInt(text, 16) : parseInt(text, 10)
}
private getImageKeysForWxid(wxid: string): { xorKey: number; aesKey?: string } | null {
const wxidConfigs = this.configService.get('wxidConfigs') || {}
const candidates = this.buildWxidCandidates(wxid)
const matchedKey = Object.keys(wxidConfigs).find((key) => {
const cleanKey = this.cleanAccountDirName(key).toLowerCase()
return candidates.some(candidate => cleanKey === candidate.toLowerCase())
})
const cfg = matchedKey ? wxidConfigs[matchedKey] : undefined
const xorKey = this.parseImageXorKey(cfg?.imageXorKey ?? this.configService.get('imageXorKey'))
if (!Number.isFinite(xorKey)) return null
const aesKey = String(cfg?.imageAesKey ?? this.configService.get('imageAesKey') ?? '').trim()
return { xorKey, aesKey: aesKey || undefined }
}
private async listFilesForArchive(root: string, rel = '', state = { visited: 0 }): Promise<string[]> { private async listFilesForArchive(root: string, rel = '', state = { visited: 0 }): Promise<string[]> {
const dir = join(root, rel) const dir = join(root, rel)
const files: string[] = [] const files: string[] = []
@@ -626,11 +603,9 @@ export class BackupService {
manifest: BackupManifest manifest: BackupManifest
): Promise<void> { ): Promise<void> {
const accountDir = dirname(connected.dbStorage) const accountDir = dirname(connected.dbStorage)
const keys = this.getImageKeysForWxid(connected.wxid)
const imagesDir = join(stagingDir, 'resources', 'images') const imagesDir = join(stagingDir, 'resources', 'images')
const imagePaths = await this.listChatImageDatFiles(accountDir) const imagePaths = await this.listChatImageDatFiles(accountDir)
if (imagePaths.length === 0) return if (imagePaths.length === 0) return
if (!keys) throw new Error('存在图片资源,但未配置图片解密密钥')
mkdirSync(imagesDir, { recursive: true }) mkdirSync(imagesDir, { recursive: true })
const resources: BackupResourceEntry[] = [] const resources: BackupResourceEntry[] = []
@@ -641,18 +616,16 @@ export class BackupService {
if (!relativeTarget) continue if (!relativeTarget) continue
emitImageProgress({ emitImageProgress({
phase: 'exporting', phase: 'exporting',
message: '正在解密图片资源', message: '正在打包图片资源',
current: index + 1, current: index + 1,
total: imagePaths.length, total: imagePaths.length,
detail: relativeTarget detail: relativeTarget
}) })
const decrypted = decryptDatViaNative(sourcePath, keys.xorKey, keys.aesKey) const archivePath = toArchivePath(join('resources', 'images', relativeTarget))
if (!decrypted) continue
const archivePath = toArchivePath(join('resources', 'images', `${relativeTarget}${decrypted.ext || '.bin'}`))
const outputPath = join(stagingDir, archivePath) const outputPath = join(stagingDir, archivePath)
mkdirSync(dirname(outputPath), { recursive: true }) await this.stagePlainResource(sourcePath, outputPath)
await writeFile(outputPath, decrypted.data)
const stem = basename(sourcePath).replace(/\.dat$/i, '').toLowerCase() const stem = basename(sourcePath).replace(/\.dat$/i, '').toLowerCase()
const stat = statSync(sourcePath)
resources.push({ resources.push({
kind: 'image', kind: 'image',
id: relativeTarget, id: relativeTarget,
@@ -660,9 +633,7 @@ export class BackupService {
sourceFileName: basename(sourcePath), sourceFileName: basename(sourcePath),
archivePath, archivePath,
targetRelativePath: relativeTarget, targetRelativePath: relativeTarget,
ext: decrypted.ext || undefined, size: stat.size
size: decrypted.data.length,
datMeta: decrypted.meta
}) })
if (index % 20 === 0) await delay() if (index % 20 === 0) await delay()
} }
@@ -930,13 +901,6 @@ export class BackupService {
} }
const accountDir = dirname(connected.dbStorage) const accountDir = dirname(connected.dbStorage)
const imageKeys = imageByPath.size > 0
? this.getImageKeysForWxid(connected.wxid || String(manifest.source?.wxid || '').trim())
: null
if (imageByPath.size > 0 && !imageKeys) {
throw new Error('备份包包含图片资源,但目标账号未配置图片加密密钥')
}
let current = startCurrent let current = startCurrent
let skipped = 0 let skipped = 0
const pending: Promise<void>[] = [] const pending: Promise<void>[] = []
@@ -957,35 +921,26 @@ export class BackupService {
const image = imageByPath.get(entryPath) const image = imageByPath.get(entryPath)
if (image) { if (image) {
const tempPath = this.resolveStagingPath(extractDir, entryPath)
const targetPath = this.resolveTargetResourcePath(accountDir, image.targetRelativePath) const targetPath = this.resolveTargetResourcePath(accountDir, image.targetRelativePath)
if (!tempPath || !targetPath) { if (!targetPath) {
skipped += 1 skipped += 1
entry.resume() entry.resume()
return return
} }
const task = this.writeTarEntryToFile(entry, tempPath).then(async () => { current += 1
current += 1 emitRestoreProgress({
emitRestoreProgress({ phase: 'restoring',
phase: 'restoring', message: '正在写回图片资源',
message: '正在加密并写回图片资源', current,
current, total,
total, detail: image.md5 || image.targetRelativePath
detail: image.md5 || image.targetRelativePath
})
if (existsSync(targetPath)) {
skipped += 1
return
}
const encrypted = encryptDatViaNative(tempPath, imageKeys!.xorKey, imageKeys!.aesKey, image.datMeta)
if (!encrypted) {
skipped += 1
return
}
mkdirSync(dirname(targetPath), { recursive: true })
await writeFile(targetPath, encrypted)
}) })
pending.push(task) if (existsSync(targetPath)) {
skipped += 1
entry.resume()
return
}
pending.push(this.writeTarEntryToFile(entry, targetPath))
return return
} }