mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: block PDF uploads, fix PBM/PGM/PPM batch failures, add tests
- Added isImageFile() filter to all drop handlers (dropzone, collage,
file-upload-area) so PDFs and non-image files are rejected on drop.
Previously only the file picker's accept attribute filtered; drag-and-
drop accepted anything.
- Added ppm, pgm, pbm to CLI_DECODED_FORMATS in file-validation.ts.
These formats were missing, causing Sharp metadata checks to fail for
some files during batch validation, which silently dropped them from
results ("File not found in batch results").
- Added integration tests for PBM, PGM, PPM, TIFF, QOI, JP2, SVGZ
single-file processing, plus a test confirming PDF is rejected.
This commit is contained in:
@@ -172,6 +172,9 @@ const CLI_DECODED_FORMATS = new Set([
|
|||||||
"cur",
|
"cur",
|
||||||
"dpx",
|
"dpx",
|
||||||
"fits",
|
"fits",
|
||||||
|
"ppm",
|
||||||
|
"pgm",
|
||||||
|
"pbm",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,81 @@ import { FileImage, Upload } from "lucide-react";
|
|||||||
import { type DragEvent, useCallback, useState } from "react";
|
import { type DragEvent, useCallback, useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const IMAGE_EXTENSIONS = new Set([
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"gif",
|
||||||
|
"webp",
|
||||||
|
"svg",
|
||||||
|
"bmp",
|
||||||
|
"avif",
|
||||||
|
"tiff",
|
||||||
|
"tif",
|
||||||
|
"ico",
|
||||||
|
"heic",
|
||||||
|
"heif",
|
||||||
|
"hif",
|
||||||
|
"jxl",
|
||||||
|
"apng",
|
||||||
|
"dng",
|
||||||
|
"cr2",
|
||||||
|
"cr3",
|
||||||
|
"nef",
|
||||||
|
"nrw",
|
||||||
|
"arw",
|
||||||
|
"orf",
|
||||||
|
"rw2",
|
||||||
|
"raf",
|
||||||
|
"pef",
|
||||||
|
"3fr",
|
||||||
|
"iiq",
|
||||||
|
"srw",
|
||||||
|
"x3f",
|
||||||
|
"rwl",
|
||||||
|
"gpr",
|
||||||
|
"fff",
|
||||||
|
"mrw",
|
||||||
|
"mef",
|
||||||
|
"kdc",
|
||||||
|
"dcr",
|
||||||
|
"erf",
|
||||||
|
"ptx",
|
||||||
|
"tga",
|
||||||
|
"psd",
|
||||||
|
"exr",
|
||||||
|
"hdr",
|
||||||
|
"svgz",
|
||||||
|
"jp2",
|
||||||
|
"j2k",
|
||||||
|
"j2c",
|
||||||
|
"jpc",
|
||||||
|
"jpf",
|
||||||
|
"jpx",
|
||||||
|
"qoi",
|
||||||
|
"eps",
|
||||||
|
"epsf",
|
||||||
|
"dds",
|
||||||
|
"cur",
|
||||||
|
"dpx",
|
||||||
|
"cin",
|
||||||
|
"fits",
|
||||||
|
"fit",
|
||||||
|
"fts",
|
||||||
|
"pbm",
|
||||||
|
"pgm",
|
||||||
|
"ppm",
|
||||||
|
"pnm",
|
||||||
|
"pam",
|
||||||
|
"pfm",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isImageFile(file: File): boolean {
|
||||||
|
if (file.type.startsWith("image/")) return true;
|
||||||
|
const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
|
||||||
|
return IMAGE_EXTENSIONS.has(ext);
|
||||||
|
}
|
||||||
|
|
||||||
interface DropzoneProps {
|
interface DropzoneProps {
|
||||||
onFiles?: (files: File[]) => void;
|
onFiles?: (files: File[]) => void;
|
||||||
accept?: string;
|
accept?: string;
|
||||||
@@ -41,7 +116,7 @@ export function Dropzone({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
const files = Array.from(e.dataTransfer.files);
|
const files = Array.from(e.dataTransfer.files).filter(isImageFile);
|
||||||
if (files.length > 0) onFiles?.(files);
|
if (files.length > 0) onFiles?.(files);
|
||||||
},
|
},
|
||||||
[onFiles],
|
[onFiles],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Upload } from "lucide-react";
|
import { Upload } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { isImageFile } from "@/components/common/dropzone";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useFilesPageStore } from "@/stores/files-page-store";
|
import { useFilesPageStore } from "@/stores/files-page-store";
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ export function FileUploadArea() {
|
|||||||
function handleDrop(e: React.DragEvent) {
|
function handleDrop(e: React.DragEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith("image/"));
|
const files = Array.from(e.dataTransfer.files).filter(isImageFile);
|
||||||
if (files.length > 0) uploadFiles(files);
|
if (files.length > 0) uploadFiles(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { type DragEvent, useCallback, useEffect, useRef, useState } from "react";
|
import { type DragEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { isImageFile } from "@/components/common/dropzone";
|
||||||
import { type CollageTemplate, getTemplateById } from "@/lib/collage-templates";
|
import { type CollageTemplate, getTemplateById } from "@/lib/collage-templates";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { CellTransform, CollageImage } from "@/stores/collage-store";
|
import type { CellTransform, CollageImage } from "@/stores/collage-store";
|
||||||
@@ -95,7 +96,7 @@ function UploadArea() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
const files = Array.from(e.dataTransfer.files);
|
const files = Array.from(e.dataTransfer.files).filter(isImageFile);
|
||||||
if (files.length > 0) addImages(files);
|
if (files.length > 0) addImages(files);
|
||||||
},
|
},
|
||||||
[addImages],
|
[addImages],
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|||||||
import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp } from "./test-server.js";
|
import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp } from "./test-server.js";
|
||||||
|
|
||||||
const FIXTURES = join(__dirname, "..", "fixtures");
|
const FIXTURES = join(__dirname, "..", "fixtures");
|
||||||
|
const FORMATS = join(FIXTURES, "formats");
|
||||||
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
|
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
|
||||||
const _JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
|
const _JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
|
||||||
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
|
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
|
||||||
@@ -207,6 +208,41 @@ describe("Multiple input formats", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Exotic input formats ─────────────────────────────────────────
|
||||||
|
describe("Exotic format processing", () => {
|
||||||
|
const exoticFormats = [
|
||||||
|
{ ext: "pbm", mime: "image/x-portable-bitmap" },
|
||||||
|
{ ext: "pgm", mime: "image/x-portable-graymap" },
|
||||||
|
{ ext: "ppm", mime: "image/x-portable-pixmap" },
|
||||||
|
{ ext: "tiff", mime: "image/tiff" },
|
||||||
|
{ ext: "qoi", mime: "image/x-qoi" },
|
||||||
|
{ ext: "jp2", mime: "image/jp2" },
|
||||||
|
{ ext: "svgz", mime: "image/svg+xml" },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { ext, mime } of exoticFormats) {
|
||||||
|
it(`processes ${ext.toUpperCase()} input without error`, async () => {
|
||||||
|
const buf = readFileSync(join(FORMATS, `sample.${ext}`));
|
||||||
|
const res = await postTool({ mode: "quality", quality: 50 }, buf, `sample.${ext}`, mime);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const result = JSON.parse(res.body);
|
||||||
|
expect(result.downloadUrl).toBeDefined();
|
||||||
|
expect(result.processedSize).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("rejects PDF files", async () => {
|
||||||
|
const pdfHeader = Buffer.from("%PDF-1.4 fake pdf content for test");
|
||||||
|
const res = await postTool(
|
||||||
|
{ mode: "quality", quality: 50 },
|
||||||
|
pdfHeader,
|
||||||
|
"document.pdf",
|
||||||
|
"application/pdf",
|
||||||
|
);
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Error handling ────────────────────────────────────────────────
|
// ── Error handling ────────────────────────────────────────────────
|
||||||
describe("Error handling", () => {
|
describe("Error handling", () => {
|
||||||
it("returns 400 when no file is provided", async () => {
|
it("returns 400 when no file is provided", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user