Ouvrir rapidement un très grand livre et continuer la lecture sans attendre le chargement complet sur très mauvaise connexion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
4.7 KiB
TypeScript
76 lines
4.7 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import AdmZip from "adm-zip";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { ArchiveReadingSourceAdapter } from "./archive-reading-source.adapter.js";
|
|
|
|
const directories: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const directory of directories.splice(0)) rmSync(directory, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("ArchiveReadingSourceAdapter", () => {
|
|
it("builds an ordered EPUB spine and serves only opaque resource ids", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "readabook-reading-"));
|
|
directories.push(directory);
|
|
const filePath = join(directory, "book.epub");
|
|
const zip = new AdmZip();
|
|
zip.addFile("META-INF/container.xml", Buffer.from(`<?xml version="1.0"?><container><rootfiles><rootfile full-path="OPS/package.opf"/></rootfiles></container>`));
|
|
zip.addFile("OPS/package.opf", Buffer.from(`<package><manifest><item id="one" href="chapter-1.xhtml" media-type="application/xhtml+xml"/><item id="pic" href="images/cover.jpg" media-type="image/jpeg"/><item id="two" href="chapter-2.xhtml" media-type="application/xhtml+xml"/></manifest><spine><itemref idref="one"/><itemref idref="two"/></spine></package>`));
|
|
zip.addFile("OPS/chapter-1.xhtml", Buffer.from('<html><head><link href="styles/book.css" rel="stylesheet"/></head><body><img src="images/cover.jpg"/><p>First</p></body></html>'));
|
|
zip.addFile("OPS/chapter-2.xhtml", Buffer.from("<p>Second</p>"));
|
|
zip.addFile("OPS/images/cover.jpg", Buffer.from([1, 2, 3]));
|
|
zip.addFile("OPS/styles/book.css", Buffer.from("body { color: black; }"));
|
|
zip.writeZip(filePath);
|
|
const stat = await import("node:fs").then(({ statSync }) => statSync(filePath));
|
|
const source = new ArchiveReadingSourceAdapter();
|
|
const book = { id: 7, format: "epub", filePath, fileSize: stat.size, fileMtime: stat.mtime.toISOString() };
|
|
|
|
const manifest = await source.manifest(book);
|
|
|
|
expect(manifest.spine).toHaveLength(2);
|
|
expect(manifest).toMatchObject({
|
|
containerPath: "META-INF/container.xml",
|
|
opfPath: "OPS/package.opf",
|
|
basePath: "OPS/",
|
|
revision: expect.any(String)
|
|
});
|
|
expect(JSON.stringify(manifest)).not.toContain(filePath);
|
|
expect(JSON.stringify(manifest)).not.toContain(`/books/${book.id}/file`);
|
|
expect(manifest.resources).toHaveLength(6);
|
|
expect(manifest.resources.map((resource) => resource.resourceId)).not.toContain("OPS/chapter-1.xhtml");
|
|
expect(manifest.resources).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ href: "META-INF/container.xml", mediaType: "application/xml", size: expect.any(Number), url: expect.stringContaining("/reading-resources/") }),
|
|
expect.objectContaining({ href: "OPS/package.opf", mediaType: "application/oebps-package+xml" }),
|
|
expect.objectContaining({ href: "OPS/images/cover.jpg", mediaType: "image/jpeg", size: 3 })
|
|
]));
|
|
expect(manifest.spine[0]).toMatchObject({ index: 0, href: "OPS/chapter-1.xhtml", mediaType: "application/xhtml+xml", size: expect.any(Number) });
|
|
const firstResource = manifest.resources.find((resource) => resource.href === "OPS/chapter-1.xhtml");
|
|
const coverResource = manifest.resources.find((resource) => resource.href === "OPS/images/cover.jpg");
|
|
expect(firstResource?.references).toContain(coverResource?.resourceId);
|
|
const chapter = await source.resource(book, manifest.spine[0].resourceId);
|
|
expect(chapter.contentType).toBe("application/xhtml+xml");
|
|
expect(chapter.data?.toString()).toContain("<p>First</p>");
|
|
const container = manifest.resources.find((resource) => resource.href === manifest.containerPath);
|
|
expect((await source.resource(book, container!.resourceId)).data?.toString()).toContain("container");
|
|
});
|
|
|
|
it("exposes a PDF as a range-capable original without leaking its path in the manifest", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "readabook-reading-"));
|
|
directories.push(directory);
|
|
const filePath = join(directory, "secret-name.pdf");
|
|
writeFileSync(filePath, "%PDF-test");
|
|
const source = new ArchiveReadingSourceAdapter();
|
|
const book = { id: 8, format: "pdf", filePath, fileSize: 9, fileMtime: "2026-01-01T00:00:00.000Z" };
|
|
|
|
const manifest = await source.manifest(book);
|
|
const resource = await source.resource(book, "original");
|
|
|
|
expect(JSON.stringify(manifest)).not.toContain(filePath);
|
|
expect(manifest).toMatchObject({ format: "pdf", spine: [expect.objectContaining({ resourceId: "original" })], capabilities: { byteRanges: true, lightweightPages: false } });
|
|
expect(resource).toMatchObject({ filePath, contentType: "application/pdf", supportsRanges: true });
|
|
});
|
|
});
|