Allow inline video attachment previews

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-06-03 23:17:16 +00:00
parent 5d91c1bb29
commit 5f481d50f1
4 changed files with 22 additions and 11 deletions
@@ -1,4 +1,5 @@
import { Readable } from "node:stream"; import { Readable } from "node:stream";
import type { IncomingMessage } from "node:http";
import express from "express"; import express from "express";
import request from "supertest"; import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -190,6 +191,13 @@ function makeAttachment(contentType: string, originalFilename: string) {
}; };
} }
function parseBinaryResponse(res: IncomingMessage, callback: (error: Error | null, body?: Buffer) => void) {
const chunks: Buffer[] = [];
res.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
res.on("end", () => callback(null, Buffer.concat(chunks)));
res.on("error", callback);
}
describe("normalizeIssueAttachmentMaxBytes", () => { describe("normalizeIssueAttachmentMaxBytes", () => {
it("keeps the process-level attachment cap as the final cap", async () => { it("keeps the process-level attachment cap as the final cap", async () => {
const previous = process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES; const previous = process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES;
@@ -362,7 +370,10 @@ describe("issue attachment routes", () => {
mockIssueService.getAttachmentById.mockResolvedValue(makeAttachment("text/html", "report.html")); mockIssueService.getAttachmentById.mockResolvedValue(makeAttachment("text/html", "report.html"));
const app = await createApp(storage); const app = await createApp(storage);
const res = await request(app).get("/api/attachments/attachment-1/content"); const res = await request(app)
.get("/api/attachments/attachment-1/content")
.buffer(true)
.parse(parseBinaryResponse);
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect([ expect([
+2
View File
@@ -35,6 +35,7 @@ export const DEFAULT_ALLOWED_TYPES: readonly string[] = [
"video/mp4", "video/mp4",
"video/webm", "video/webm",
"video/quicktime", "video/quicktime",
"video/x-m4v",
]; ];
export const DEFAULT_ATTACHMENT_CONTENT_TYPE = "application/octet-stream"; export const DEFAULT_ATTACHMENT_CONTENT_TYPE = "application/octet-stream";
@@ -49,6 +50,7 @@ export const INLINE_ATTACHMENT_TYPES: readonly string[] = [
"video/mp4", "video/mp4",
"video/webm", "video/webm",
"video/quicktime", "video/quicktime",
"video/x-m4v",
]; ];
/** /**
@@ -60,8 +60,6 @@ function makeAttachment(overrides: Partial<IssueAttachment> = {}): IssueAttachme
createdAt: new Date("2026-06-01T00:00:00.000Z"), createdAt: new Date("2026-06-01T00:00:00.000Z"),
updatedAt: new Date("2026-06-01T00:00:00.000Z"), updatedAt: new Date("2026-06-01T00:00:00.000Z"),
contentPath: "/api/attachments/attachment-1/content", contentPath: "/api/attachments/attachment-1/content",
openPath: "/api/attachments/attachment-1/content",
downloadPath: "/api/attachments/attachment-1/content?download=1",
...overrides, ...overrides,
}; };
} }
@@ -271,8 +269,6 @@ describe("IssueAttachmentsSection", () => {
originalFilename: "report.pdf", originalFilename: "report.pdf",
contentType: "application/pdf", contentType: "application/pdf",
contentPath: "/api/attachments/pdf-attachment/content", contentPath: "/api/attachments/pdf-attachment/content",
openPath: "/api/attachments/pdf-attachment/content",
downloadPath: "/api/attachments/pdf-attachment/content?download=1",
}); });
await act(async () => { await act(async () => {
+8 -6
View File
@@ -7,6 +7,12 @@ const GENERIC_ATTACHMENT_CONTENT_TYPES = new Set([
"application/x-binary", "application/x-binary",
]); ]);
type AttachmentPathLike = {
contentPath: string;
openPath?: string;
downloadPath?: string;
};
function normalizedContentType(attachment: Pick<IssueAttachment, "contentType">) { function normalizedContentType(attachment: Pick<IssueAttachment, "contentType">) {
return attachment.contentType.toLowerCase().split(";")[0]?.trim() ?? ""; return attachment.contentType.toLowerCase().split(";")[0]?.trim() ?? "";
} }
@@ -15,15 +21,11 @@ export function attachmentFilename(attachment: Pick<IssueAttachment, "id" | "ori
return attachment.originalFilename ?? attachment.id; return attachment.originalFilename ?? attachment.id;
} }
export function attachmentOpenPath( export function attachmentOpenPath(attachment: AttachmentPathLike) {
attachment: Pick<IssueAttachment, "contentPath" | "openPath">,
) {
return attachment.openPath ?? attachment.contentPath; return attachment.openPath ?? attachment.contentPath;
} }
export function attachmentDownloadPath( export function attachmentDownloadPath(attachment: AttachmentPathLike) {
attachment: Pick<IssueAttachment, "contentPath" | "downloadPath">,
) {
return attachment.downloadPath ?? `${attachment.contentPath}?download=1`; return attachment.downloadPath ?? `${attachment.contentPath}?download=1`;
} }