From 267c72bbe8d908de725fac08b144b4f2cb95a9f4 Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Mon, 27 Jul 2026 12:06:46 +0200 Subject: [PATCH] feat(worker): enable RAR ranged listing + format-aware destination reads --- worker/src/db/queries.ts | 4 ++- worker/src/provenance-backfill.ts | 43 ++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/worker/src/db/queries.ts b/worker/src/db/queries.ts index 3676648..3f7f63f 100644 --- a/worker/src/db/queries.ts +++ b/worker/src/db/queries.ts @@ -1015,6 +1015,7 @@ export async function createAutoGroup(input: { export interface PlaceholderCandidate { id: string; archiveType: string; + fileName: string; fileCount: number; fileSize: bigint; destMessageId: bigint | null; @@ -1046,7 +1047,7 @@ export async function findPlaceholderCandidates( ], }, select: { - id: true, archiveType: true, fileCount: true, fileSize: true, + id: true, archiveType: true, fileName: true, fileCount: true, fileSize: true, destMessageId: true, destMessageIds: true, destChannelId: true, }, orderBy: { indexedAt: "asc" }, @@ -1065,6 +1066,7 @@ export async function findPlaceholderCandidates( return rows.map((row) => ({ id: row.id, archiveType: row.archiveType, + fileName: row.fileName, fileCount: row.fileCount, fileSize: row.fileSize, destMessageId: row.destMessageId, diff --git a/worker/src/provenance-backfill.ts b/worker/src/provenance-backfill.ts index 5ada39a..313282a 100644 --- a/worker/src/provenance-backfill.ts +++ b/worker/src/provenance-backfill.ts @@ -12,6 +12,7 @@ import { } from "./db/queries.js"; import type { FileEntry } from "./archive/zip-reader.js"; import { readSevenZListingRanged, type RangedPart } from "./archive/ranged/sevenz-ranged.js"; +import { readRarListingRanged } from "./archive/ranged/rar-ranged.js"; import { tdlibRangeReader } from "./archive/ranged/range-reader.js"; import { fullDownloadListing } from "./archive/ranged/fallback.js"; import type { Client } from "tdl"; @@ -67,32 +68,40 @@ async function readScannedZipListing( return null; } -async function readZipListingFromDestination( +/** + * Resolve the destination copy's message(s) into ranged parts (file id + + * size + name), in order, so a multipart destination copy is reconstructed + * with correct per-part sizes and names (the last message carries the + * EOCD-bearing tail part for ZIP; multipart RAR needs correctly-suffixed + * `.partN.rar` names for `unrar` sibling discovery). Cheap-only: any TDLib + * failure here degrades the caller to name-size confidence rather than + * falling back to a full download. + */ +async function resolveDestParts( client: Client, destChatTelegramId: bigint, destMessageIds: bigint[], destMessageId: bigint | null, -): Promise { + fallbackFileName: string, +): Promise { const messageIds = destMessageIds.length > 0 ? destMessageIds : destMessageId ? [destMessageId] : []; if (messageIds.length === 0) return null; try { - // Resolve each destination message's document file id + size, in order, - // so a multipart destination copy is reconstructed with correct - // per-part sizes (the last message carries the EOCD-bearing tail part). - const parts: { fileId: string; fileSize: bigint }[] = []; + const parts: RangedPart[] = []; for (const msgId of messageIds) { const msg = (await invokeWithTimeout(client, { _: "getMessage", chat_id: Number(destChatTelegramId), message_id: Number(msgId), - })) as { content?: { document?: { document?: { id: number; size?: number } } } }; + })) as { content?: { document?: { document?: { id: number; size?: number }; file_name?: string } } }; const doc = msg?.content?.document?.document; if (!doc?.id) return null; - parts.push({ fileId: String(doc.id), fileSize: BigInt(doc.size ?? 0) }); + const fileName = msg?.content?.document?.file_name || fallbackFileName; + parts.push({ fileId: String(doc.id), fileSize: BigInt(doc.size ?? 0), fileName }); } - return await readScannedZipListing(client, parts); + return parts; } catch (err) { - log.warn({ err, destMessageIds: messageIds.map(Number) }, "destination ZIP listing read failed"); + log.warn({ err, destMessageIds: messageIds.map(Number) }, "destination archive part resolution failed"); return null; } } @@ -105,7 +114,7 @@ async function readScannedListingRanged( const read = tdlibRangeReader(client); if (archiveType === "ZIP") return readScannedZipListing(client, parts); if (archiveType === "SEVEN_Z") return readSevenZListingRanged(parts, read); - // RAR enabled in Task 8. + if (archiveType === "RAR") return readRarListingRanged(parts, read); return null; } @@ -125,12 +134,22 @@ async function resolveCandidateFingerprintEntries( })); const hasDestMessage = candidate.destMessageIds.length > 0 || candidate.destMessageId != null; if (!crcFingerprint(candidateEntries).complete && hasDestMessage && candidate.destChannel) { - const destEntries = await readZipListingFromDestination( + const destParts = await resolveDestParts( client, candidate.destChannel.telegramId, candidate.destMessageIds, candidate.destMessageId, + candidate.fileName, ); + let destEntries: FileEntry[] | null = null; + if (destParts) { + const read = tdlibRangeReader(client); + destEntries = + candidate.archiveType === "ZIP" ? await readScannedZipListing(client, destParts) + : candidate.archiveType === "SEVEN_Z" ? await readSevenZListingRanged(destParts, read) + : candidate.archiveType === "RAR" ? await readRarListingRanged(destParts, read) + : null; + } if (destEntries) { candidateEntries = destEntries; }