Allow inline video attachment previews
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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([
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user