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 type { IncomingMessage } from "node:http";
import express from "express";
import request from "supertest";
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", () => {
it("keeps the process-level attachment cap as the final cap", async () => {
const previous = process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES;
@@ -362,7 +370,10 @@ describe("issue attachment routes", () => {
mockIssueService.getAttachmentById.mockResolvedValue(makeAttachment("text/html", "report.html"));
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([
+2
View File
@@ -35,6 +35,7 @@ export const DEFAULT_ALLOWED_TYPES: readonly string[] = [
"video/mp4",
"video/webm",
"video/quicktime",
"video/x-m4v",
];
export const DEFAULT_ATTACHMENT_CONTENT_TYPE = "application/octet-stream";
@@ -49,6 +50,7 @@ export const INLINE_ATTACHMENT_TYPES: readonly string[] = [
"video/mp4",
"video/webm",
"video/quicktime",
"video/x-m4v",
];
/**
@@ -60,8 +60,6 @@ function makeAttachment(overrides: Partial<IssueAttachment> = {}): IssueAttachme
createdAt: new Date("2026-06-01T00:00:00.000Z"),
updatedAt: new Date("2026-06-01T00:00:00.000Z"),
contentPath: "/api/attachments/attachment-1/content",
openPath: "/api/attachments/attachment-1/content",
downloadPath: "/api/attachments/attachment-1/content?download=1",
...overrides,
};
}
@@ -271,8 +269,6 @@ describe("IssueAttachmentsSection", () => {
originalFilename: "report.pdf",
contentType: "application/pdf",
contentPath: "/api/attachments/pdf-attachment/content",
openPath: "/api/attachments/pdf-attachment/content",
downloadPath: "/api/attachments/pdf-attachment/content?download=1",
});
await act(async () => {
+8 -6
View File
@@ -7,6 +7,12 @@ const GENERIC_ATTACHMENT_CONTENT_TYPES = new Set([
"application/x-binary",
]);
type AttachmentPathLike = {
contentPath: string;
openPath?: string;
downloadPath?: string;
};
function normalizedContentType(attachment: Pick<IssueAttachment, "contentType">) {
return attachment.contentType.toLowerCase().split(";")[0]?.trim() ?? "";
}
@@ -15,15 +21,11 @@ export function attachmentFilename(attachment: Pick<IssueAttachment, "id" | "ori
return attachment.originalFilename ?? attachment.id;
}
export function attachmentOpenPath(
attachment: Pick<IssueAttachment, "contentPath" | "openPath">,
) {
export function attachmentOpenPath(attachment: AttachmentPathLike) {
return attachment.openPath ?? attachment.contentPath;
}
export function attachmentDownloadPath(
attachment: Pick<IssueAttachment, "contentPath" | "downloadPath">,
) {
export function attachmentDownloadPath(attachment: AttachmentPathLike) {
return attachment.downloadPath ?? `${attachment.contentPath}?download=1`;
}