From f6381e31787838fed8bdf911b2fab73f82a9304a Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Fri, 21 Aug 2026 16:50:41 +0200 Subject: [PATCH] fix(worker): recognize native 7z multipart volumes (.7z.001, .7z.002, ...) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detectArchive() had multipart patterns for ZIP and RAR but none for 7z's own volume-split naming, so files like "Name.7z.001" matched nothing and were dropped before ever reaching grouping or the skipped-package bookkeeping — no Package row, no SkippedPackage row, no log line. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01JMm2E4ecmATJo8HuBx92NP --- worker/src/archive/detect.test.ts | 39 +++++++++++++++++++++++++++++++ worker/src/archive/detect.ts | 10 +++++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 worker/src/archive/detect.test.ts diff --git a/worker/src/archive/detect.test.ts b/worker/src/archive/detect.test.ts new file mode 100644 index 0000000..49e627e --- /dev/null +++ b/worker/src/archive/detect.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { detectArchive, isArchiveAttachment } from "./detect.js"; + +describe("detectArchive — 7z numbered multipart (pack.7z.001, pack.7z.002, ...)", () => { + it("recognizes a 7z multipart part as an archive attachment", () => { + expect(isArchiveAttachment("Lost Adventures Vol2.7z.001")).toBe(true); + }); + + it("extracts format, baseName, and partNumber", () => { + const info = detectArchive("Lost Adventures Vol2.7z.001"); + expect(info).toEqual({ + baseName: "Lost Adventures Vol2.7z", + partNumber: 1, + format: "7Z", + pattern: "SEVENZ_NUMBERED", + }); + }); + + it("groups multiple parts under the same baseName + format key regardless of part number", () => { + const part1 = detectArchive("Lost Adventures Vol2.7z.001"); + const part2 = detectArchive("Lost Adventures Vol2.7z.010"); + expect(part1?.baseName).toBe(part2?.baseName); + expect(part1?.format).toBe(part2?.format); + expect(part2?.partNumber).toBe(10); + }); + + it("is case-insensitive on the .7z extension", () => { + expect(detectArchive("Archive.7Z.002")?.format).toBe("7Z"); + }); + + it("still recognizes a standalone single .7z file", () => { + expect(detectArchive("Single Pack.7z")).toEqual({ + baseName: "Single Pack", + partNumber: -1, + format: "7Z", + pattern: "SINGLE", + }); + }); +}); diff --git a/worker/src/archive/detect.ts b/worker/src/archive/detect.ts index f4a0c7a..2fe5853 100644 --- a/worker/src/archive/detect.ts +++ b/worker/src/archive/detect.ts @@ -4,7 +4,7 @@ export interface MultipartInfo { baseName: string; partNumber: number; format: ArchiveFormat; - pattern: "ZIP_NUMBERED" | "ZIP_LEGACY" | "RAR_PART" | "RAR_LEGACY" | "SINGLE"; + pattern: "ZIP_NUMBERED" | "ZIP_LEGACY" | "RAR_PART" | "RAR_LEGACY" | "SEVENZ_NUMBERED" | "SINGLE"; } const patterns: { @@ -46,6 +46,14 @@ const patterns: { getBaseName: (m) => m[1], getPartNumber: (m) => parseInt(m[2], 10), }, + // pack.7z.001, pack.7z.002 (native 7z volume split) + { + regex: /^(.+\.7z)\.(\d{3,})$/i, + format: "7Z", + pattern: "SEVENZ_NUMBERED", + getBaseName: (m) => m[1], + getPartNumber: (m) => parseInt(m[2], 10), + }, ]; /** Extensions we recognize as fetchable documents (archives + standalone files) */