feat(worker): enable RAR ranged listing + format-aware destination reads

This commit is contained in:
2026-07-27 12:06:46 +02:00
parent 497c4876a6
commit 267c72bbe8
2 changed files with 34 additions and 13 deletions
+3 -1
View File
@@ -1015,6 +1015,7 @@ export async function createAutoGroup(input: {
export interface PlaceholderCandidate { export interface PlaceholderCandidate {
id: string; id: string;
archiveType: string; archiveType: string;
fileName: string;
fileCount: number; fileCount: number;
fileSize: bigint; fileSize: bigint;
destMessageId: bigint | null; destMessageId: bigint | null;
@@ -1046,7 +1047,7 @@ export async function findPlaceholderCandidates(
], ],
}, },
select: { 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, destMessageId: true, destMessageIds: true, destChannelId: true,
}, },
orderBy: { indexedAt: "asc" }, orderBy: { indexedAt: "asc" },
@@ -1065,6 +1066,7 @@ export async function findPlaceholderCandidates(
return rows.map((row) => ({ return rows.map((row) => ({
id: row.id, id: row.id,
archiveType: row.archiveType, archiveType: row.archiveType,
fileName: row.fileName,
fileCount: row.fileCount, fileCount: row.fileCount,
fileSize: row.fileSize, fileSize: row.fileSize,
destMessageId: row.destMessageId, destMessageId: row.destMessageId,
+31 -12
View File
@@ -12,6 +12,7 @@ import {
} from "./db/queries.js"; } from "./db/queries.js";
import type { FileEntry } from "./archive/zip-reader.js"; import type { FileEntry } from "./archive/zip-reader.js";
import { readSevenZListingRanged, type RangedPart } from "./archive/ranged/sevenz-ranged.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 { tdlibRangeReader } from "./archive/ranged/range-reader.js";
import { fullDownloadListing } from "./archive/ranged/fallback.js"; import { fullDownloadListing } from "./archive/ranged/fallback.js";
import type { Client } from "tdl"; import type { Client } from "tdl";
@@ -67,32 +68,40 @@ async function readScannedZipListing(
return null; 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, client: Client,
destChatTelegramId: bigint, destChatTelegramId: bigint,
destMessageIds: bigint[], destMessageIds: bigint[],
destMessageId: bigint | null, destMessageId: bigint | null,
): Promise<FileEntry[] | null> { fallbackFileName: string,
): Promise<RangedPart[] | null> {
const messageIds = destMessageIds.length > 0 ? destMessageIds : destMessageId ? [destMessageId] : []; const messageIds = destMessageIds.length > 0 ? destMessageIds : destMessageId ? [destMessageId] : [];
if (messageIds.length === 0) return null; if (messageIds.length === 0) return null;
try { try {
// Resolve each destination message's document file id + size, in order, const parts: RangedPart[] = [];
// 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 }[] = [];
for (const msgId of messageIds) { for (const msgId of messageIds) {
const msg = (await invokeWithTimeout(client, { const msg = (await invokeWithTimeout(client, {
_: "getMessage", _: "getMessage",
chat_id: Number(destChatTelegramId), chat_id: Number(destChatTelegramId),
message_id: Number(msgId), 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; const doc = msg?.content?.document?.document;
if (!doc?.id) return null; 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) { } 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; return null;
} }
} }
@@ -105,7 +114,7 @@ async function readScannedListingRanged(
const read = tdlibRangeReader(client); const read = tdlibRangeReader(client);
if (archiveType === "ZIP") return readScannedZipListing(client, parts); if (archiveType === "ZIP") return readScannedZipListing(client, parts);
if (archiveType === "SEVEN_Z") return readSevenZListingRanged(parts, read); if (archiveType === "SEVEN_Z") return readSevenZListingRanged(parts, read);
// RAR enabled in Task 8. if (archiveType === "RAR") return readRarListingRanged(parts, read);
return null; return null;
} }
@@ -125,12 +134,22 @@ async function resolveCandidateFingerprintEntries(
})); }));
const hasDestMessage = candidate.destMessageIds.length > 0 || candidate.destMessageId != null; const hasDestMessage = candidate.destMessageIds.length > 0 || candidate.destMessageId != null;
if (!crcFingerprint(candidateEntries).complete && hasDestMessage && candidate.destChannel) { if (!crcFingerprint(candidateEntries).complete && hasDestMessage && candidate.destChannel) {
const destEntries = await readZipListingFromDestination( const destParts = await resolveDestParts(
client, client,
candidate.destChannel.telegramId, candidate.destChannel.telegramId,
candidate.destMessageIds, candidate.destMessageIds,
candidate.destMessageId, 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) { if (destEntries) {
candidateEntries = destEntries; candidateEntries = destEntries;
} }