mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Fix desktop file uploads with extensions (#1741)
* fix(attachments): allow Markdown file uploads on desktop Desktop file upload copied picked files through managed storage with bare picker extensions like md. Normalize those extensions at the command boundary and pass dot-prefixed extensions from the picker path. * fix(attachments): stop enumerating generic file types Generic file uploads should not depend on a hand-maintained non-image MIME table. Keep raster image inference for image handling and use octet-stream for other path-only uploads. * test(attachments): cover picker extension format
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
describe("attachment file types", () => {
|
||||
it("keeps SVG as a file while treating raster image files as images", () => {
|
||||
expect(getMimeTypeFromPath("/tmp/logo.svg")).toBe("image/svg+xml");
|
||||
expect(getMimeTypeFromPath("/tmp/logo.svg")).toBe("application/octet-stream");
|
||||
expect(isRasterImagePath("/tmp/logo.svg")).toBe(false);
|
||||
expect(isRasterImageMimeType("image/svg+xml")).toBe(false);
|
||||
expect(isRasterImageFile(new File(["<svg />"], "logo.svg", { type: "image/svg+xml" }))).toBe(
|
||||
@@ -18,11 +18,20 @@ describe("attachment file types", () => {
|
||||
);
|
||||
|
||||
expect(getRasterImageMimeTypeFromPath("/tmp/screenshot.PNG?cache=1")).toBe("image/png");
|
||||
expect(getMimeTypeFromPath("/tmp/screenshot.PNG?cache=1")).toBe("image/png");
|
||||
expect(isRasterImagePath("/tmp/screenshot.PNG?cache=1")).toBe(true);
|
||||
expect(isRasterImageMimeType("image/png; charset=binary")).toBe(true);
|
||||
expect(isRasterImageFile(new File([new Uint8Array([0])], "screenshot.png"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not require MIME table entries for generic file attachments", () => {
|
||||
expect(getMimeTypeFromPath("/tmp/notes.md")).toBe("application/octet-stream");
|
||||
expect(getMimeTypeFromPath("/tmp/archive.zip")).toBe("application/octet-stream");
|
||||
expect(getMimeTypeFromPath("/tmp/report.docx")).toBe("application/octet-stream");
|
||||
expect(getMimeTypeFromPath("/tmp/runtime.log")).toBe("application/octet-stream");
|
||||
expect(getMimeTypeFromPath("/tmp/export.anything")).toBe("application/octet-stream");
|
||||
});
|
||||
|
||||
it("does not offer SVG in the image picker extension list", () => {
|
||||
expect(new Set(RASTER_IMAGE_FILE_EXTENSIONS)).toEqual(
|
||||
new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "heic", "heif", "avif", "tif", "tiff"]),
|
||||
|
||||
@@ -1,45 +1,3 @@
|
||||
const MIME_TYPE_BY_EXTENSION: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
".svg": "image/svg+xml",
|
||||
".heic": "image/heic",
|
||||
".heif": "image/heif",
|
||||
".avif": "image/avif",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
".pdf": "application/pdf",
|
||||
".txt": "text/plain",
|
||||
".md": "text/markdown",
|
||||
".json": "application/json",
|
||||
".js": "text/javascript",
|
||||
".ts": "text/typescript",
|
||||
".tsx": "text/typescript-jsx",
|
||||
".jsx": "text/javascript",
|
||||
".html": "text/html",
|
||||
".css": "text/css",
|
||||
".xml": "text/xml",
|
||||
".csv": "text/csv",
|
||||
".zip": "application/zip",
|
||||
".gz": "application/gzip",
|
||||
".tar": "application/x-tar",
|
||||
".mp3": "audio/mpeg",
|
||||
".mp4": "video/mp4",
|
||||
".mov": "video/quicktime",
|
||||
".webm": "video/webm",
|
||||
".wav": "audio/wav",
|
||||
".ogg": "audio/ogg",
|
||||
".doc": "application/msword",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".xls": "application/vnd.ms-excel",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".ppt": "application/vnd.ms-powerpoint",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
};
|
||||
|
||||
const RASTER_IMAGE_MIME_TYPE_BY_EXTENSION: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
@@ -55,6 +13,7 @@ const RASTER_IMAGE_MIME_TYPE_BY_EXTENSION: Record<string, string> = {
|
||||
};
|
||||
|
||||
const RASTER_IMAGE_MIME_TYPES = new Set(Object.values(RASTER_IMAGE_MIME_TYPE_BY_EXTENSION));
|
||||
const GENERIC_FILE_MIME_TYPE = "application/octet-stream";
|
||||
|
||||
export const RASTER_IMAGE_FILE_EXTENSIONS = Object.keys(RASTER_IMAGE_MIME_TYPE_BY_EXTENSION).map(
|
||||
(extension) => extension.slice(1),
|
||||
@@ -75,7 +34,7 @@ export function getFileTypeLabel(path: string): string | null {
|
||||
}
|
||||
|
||||
export function getMimeTypeFromPath(path: string): string {
|
||||
return MIME_TYPE_BY_EXTENSION[getFileExtension(path)] ?? "application/octet-stream";
|
||||
return getRasterImageMimeTypeFromPath(path) ?? GENERIC_FILE_MIME_TYPE;
|
||||
}
|
||||
|
||||
export function getRasterImageMimeTypeFromPath(path: string): string | null {
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function readDesktopFileBytes(path: string): Promise<Uint8Array> {
|
||||
const { path: managedPath } = await copyDesktopAttachmentFile({
|
||||
attachmentId: crypto.randomUUID(),
|
||||
sourcePath: path,
|
||||
extension: getFileExtension(path).slice(1) || null,
|
||||
extension: getFileExtension(path) || null,
|
||||
});
|
||||
const base64 = await readDesktopFileBase64(managedPath);
|
||||
return base64ToUint8Array(base64);
|
||||
@@ -63,7 +63,7 @@ async function pickFilesWithDesktopDialog(): Promise<PickedFile[] | null> {
|
||||
const { path: managedPath } = await copyDesktopAttachmentFile({
|
||||
attachmentId: crypto.randomUUID(),
|
||||
sourcePath: filePath,
|
||||
extension: getFileExtension(filePath).slice(1) || null,
|
||||
extension: getFileExtension(filePath) || null,
|
||||
});
|
||||
|
||||
const base64 = await readDesktopFileBase64(managedPath);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { copyAttachmentFileToManagedStorage } from "./attachments";
|
||||
|
||||
const originalPaseoHome = process.env.PASEO_HOME;
|
||||
let testHome: string | null = null;
|
||||
|
||||
async function useTempPaseoHome(): Promise<string> {
|
||||
testHome = await mkdtemp(path.join(os.tmpdir(), "paseo-desktop-attachments-"));
|
||||
process.env.PASEO_HOME = testHome;
|
||||
return testHome;
|
||||
}
|
||||
|
||||
describe("desktop attachment files", () => {
|
||||
afterEach(async () => {
|
||||
if (originalPaseoHome === undefined) {
|
||||
delete process.env.PASEO_HOME;
|
||||
} else {
|
||||
process.env.PASEO_HOME = originalPaseoHome;
|
||||
}
|
||||
|
||||
if (testHome) {
|
||||
await rm(testHome, { recursive: true, force: true });
|
||||
testHome = null;
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts dot-prefixed picker extensions for managed copies", async () => {
|
||||
const paseoHome = await useTempPaseoHome();
|
||||
const sourcePath = path.join(paseoHome, "report.md");
|
||||
await writeFile(sourcePath, "# Report\n");
|
||||
|
||||
const result = await copyAttachmentFileToManagedStorage({
|
||||
attachmentId: "att_markdown",
|
||||
sourcePath,
|
||||
extension: ".md",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
path: path.join(paseoHome, "desktop-attachments", "att_markdown.md"),
|
||||
byteSize: 9,
|
||||
});
|
||||
await expect(readFile(result.path, "utf8")).resolves.toBe("# Report\n");
|
||||
});
|
||||
|
||||
it("normalizes legacy bare extensions for managed copies", async () => {
|
||||
const paseoHome = await useTempPaseoHome();
|
||||
const sourcePath = path.join(paseoHome, "report.md");
|
||||
await writeFile(sourcePath, "# Report\n");
|
||||
|
||||
const result = await copyAttachmentFileToManagedStorage({
|
||||
attachmentId: "att_markdown_legacy",
|
||||
sourcePath,
|
||||
extension: "md",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
path: path.join(paseoHome, "desktop-attachments", "att_markdown_legacy.md"),
|
||||
byteSize: 9,
|
||||
});
|
||||
await expect(readFile(result.path, "utf8")).resolves.toBe("# Report\n");
|
||||
});
|
||||
});
|
||||
@@ -40,10 +40,11 @@ function normalizeExtension(value: unknown): string {
|
||||
throw new Error("Attachment extension must be a string.");
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!EXTENSION_PATTERN.test(normalized)) {
|
||||
const extension = normalized.startsWith(".") ? normalized : `.${normalized}`;
|
||||
if (!EXTENSION_PATTERN.test(extension)) {
|
||||
throw new Error(`Invalid attachment extension: ${value}`);
|
||||
}
|
||||
return normalized;
|
||||
return extension;
|
||||
}
|
||||
|
||||
async function buildManagedAttachmentPath(input: {
|
||||
|
||||
Reference in New Issue
Block a user