diff --git a/scripts/__tests__/import-documents.test.ts b/scripts/__tests__/import-documents.test.ts index 994ded97e..754a41e38 100644 --- a/scripts/__tests__/import-documents.test.ts +++ b/scripts/__tests__/import-documents.test.ts @@ -343,7 +343,7 @@ async function papra() { return { status: 200, json: { - documentsCount: 3, + documentsCount: 4, documents: [ { id: "doc_a", @@ -365,6 +365,15 @@ async function papra() { { id: "tag_l", name: "Lab" }, ], }, + { + id: "doc_huge", + name: "Scan.pdf", + originalName: "scan.pdf", + originalSize: 40_000_000, + createdAt: "2025-03-10T12:00:00.000Z", + documentDate: "2020-03-10", + tags: [{ id: "tag_h", name: "Health" }], + }, { id: "doc_old", name: "Old.pdf", @@ -677,7 +686,31 @@ describe("import-documents.mjs — Papra", () => { ); expect(pp.seen[0].auth).toBe("Bearer pp_test"); expect(res.stdout).toMatch(/Imported: 1/); - expect(res.stdout).toMatch(/Skipped: 1/); + // doc_b answered 413 with the cap; doc_huge is known to be larger than + // that and is skipped without being downloaded or sent. + expect(res.stdout).toMatch(/Skipped: 2/); expect(res.stdout).toMatch(/larger than HealthLog accepts/); + expect(pp.seen.some((s) => s.path.includes("doc_huge"))).toBe(false); + }); + + it("shows small sizes in KB on a dry run", async () => { + const pp = await papra(); + const res = await runScript( + [ + "papra", + "--papra-url", + pp.url, + "--papra-org", + "org_1", + "--tag", + "Health", + "--since", + "2016-01-01", + "--dry-run", + ], + { PAPRA_TOKEN: "pp_test" }, + ); + expect(res.code).toBe(0); + expect(res.stdout).toMatch(/2\.0 KB\s+Papra doc_a/); }); }); diff --git a/scripts/import-documents.mjs b/scripts/import-documents.mjs index c909a35f2..194af9d24 100644 --- a/scripts/import-documents.mjs +++ b/scripts/import-documents.mjs @@ -647,6 +647,8 @@ async function upload(options, source, doc, file) { return { outcome: "skipped", reason: `larger than HealthLog accepts${meta.maxFileBytes ? ` (${mib(meta.maxFileBytes)})` : ""}`, + maxFileBytes: + typeof meta.maxFileBytes === "number" ? meta.maxFileBytes : undefined, }; case 415: return { @@ -668,9 +670,10 @@ async function upload(options, source, doc, file) { function mib(bytes) { if (typeof bytes !== "number") return "?"; - return bytes >= 1024 * 1024 * 1024 - ? `${(bytes / 1024 ** 3).toFixed(1)} GB` - : `${(bytes / 1024 ** 2).toFixed(1)} MB`; + if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`; + // Under 0.1 MB a size in MB reads as 0.0; say it in KB. + if (bytes < 0.1 * 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 ** 2).toFixed(1)} MB`; } // ── Run ──────────────────────────────────────────────────────────────────── @@ -705,6 +708,8 @@ async function run(argv, env = process.env) { }; let listed = 0; let totalBytes = 0; + /** HealthLog's per-file limit, once a 413 has said it. */ + let maxFileBytes = null; let unknownSizes = 0; out( @@ -743,6 +748,20 @@ async function run(argv, env = process.env) { continue; } + // HealthLog says its per-file limit only when it refuses a file. Once + // it has, a document the source already says is larger is skipped + // without downloading it. + if ( + maxFileBytes !== null && + typeof doc.size === "number" && + doc.size > maxFileBytes + ) { + const reason = `larger than HealthLog accepts (${mib(maxFileBytes)})`; + tally.skipped.push({ where, reason }); + out(` skipped ${where}: ${reason}`); + continue; + } + const file = await source.download(doc); if (file.skip) { tally.skipped.push({ where, reason: file.skip }); @@ -750,6 +769,9 @@ async function run(argv, env = process.env) { continue; } const result = await upload(options, source, doc, file); + if (typeof result.maxFileBytes === "number") { + maxFileBytes = result.maxFileBytes; + } if (result.outcome === "imported") { tally.imported++; out(` imported ${where}`);