From b621a73ca9a0548043082f989f4486b0f84d20f7 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Mon, 18 May 2026 07:02:14 +0700 Subject: [PATCH] feat: add file validation utilities Co-Authored-By: Claude Opus 4.7 --- src/utils/file.js | 43 +++++++++++++++++++++++ test/file.test.js | 88 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/utils/file.js create mode 100644 test/file.test.js diff --git a/src/utils/file.js b/src/utils/file.js new file mode 100644 index 0000000..2ef5f52 --- /dev/null +++ b/src/utils/file.js @@ -0,0 +1,43 @@ +const FILE_TYPES = { + document: 2 * 1024 * 1024 * 1024, // 2GB + photo: 10 * 1024 * 1024, // 10MB + video: 2 * 1024 * 1024 * 1024, // 2GB + audio: 200 * 1024 * 1024, // 200MB + voice: 200 * 1024 * 1024, // 200MB + animation: 2 * 1024 * 1024 * 1024 // 2GB +}; + +export const getFileType = (mime, caption) => { + const mimeUpper = mime?.split('/')[0]?.toLowerCase(); + const captionLower = caption?.toLowerCase(); + + if (mimeUpper === 'video') return 'video'; + if (mimeUpper === 'audio') return 'audio'; + if (mimeUpper === 'document') return 'document'; + if (mimeUpper === 'image') return captionLower?.includes('gif') ? 'animation' : 'photo'; + if (captionLower?.includes('voice')) return 'voice'; + if (captionLower?.includes('animation')) return 'animation'; + + return mimeUpper || 'document'; +}; + +export const checkFileSize = (sizeBytes, fileType) => { + const limit = FILE_TYPES[fileType] || FILE_TYPES.document; + return sizeBytes <= limit; +}; + +export const extractFileName = (msg, request) => { + if (request?.headers?.['x-file-name']) { + return request.headers['x-file-name']; + } + return msg.document?.fileName || msg.photo?.slice(-1)[0]?.fileName || msg.audio?.fileName || + msg.voice?.fileName || msg.animation?.fileName || 'file'; +}; + +export const extractMimeType = (msg, request) => { + if (request?.headers?.['x-mime-type']) { + return request.headers['x-mime-type']; + } + return msg.document?.mimeType || msg.photo?.slice(-1)[0]?.mimeType || msg.audio?.mimeType || + msg.voice?.mimeType || msg.animation?.mimeType || 'application/octet-stream'; +}; diff --git a/test/file.test.js b/test/file.test.js new file mode 100644 index 0000000..fa64349 --- /dev/null +++ b/test/file.test.js @@ -0,0 +1,88 @@ +import { describe, it, expect } from "bun:test"; +import { getFileType, checkFileSize, extractFileName, extractMimeType } from "../src/utils/file.js"; + +describe("File Utilities", () => { + describe("getFileType", () => { + it("should classify video mime types as video", () => { + expect(getFileType("video/mp4", "")).toBe("video"); + expect(getFileType("video/quicktime", "")).toBe("video"); + }); + + it("should classify audio mime types as audio", () => { + expect(getFileType("audio/mpeg", "")).toBe("audio"); + expect(getFileType("audio/ogg", "")).toBe("audio"); + }); + + it("should classify image mime types based on caption", () => { + expect(getFileType("image/jpeg", "my photo")).toBe("photo"); + expect(getFileType("image/png", "cool image.png")).toBe("photo"); + expect(getFileType("image/gif", "funny.gif")).toBe("animation"); + expect(getFileType("image/png", "funny gif")).toBe("animation"); + }); + + it("should classify voice and animation based on caption", () => { + expect(getFileType("application/octet-stream", "this is a voice note")).toBe("voice"); + expect(getFileType("application/octet-stream", "cool animation")).toBe("animation"); + }); + + it("should default to mime first segment or document", () => { + expect(getFileType("application/pdf", "")).toBe("application"); + expect(getFileType(null, "")).toBe("document"); + }); + }); + + describe("checkFileSize", () => { + it("should allow files under the size limit", () => { + expect(checkFileSize(5 * 1024 * 1024, "photo")).toBe(true); // Photo limit is 10MB + expect(checkFileSize(1 * 1024 * 1024 * 1024, "video")).toBe(true); // Video limit is 2GB + }); + + it("should block files exceeding the size limit", () => { + expect(checkFileSize(15 * 1024 * 1024, "photo")).toBe(false); // Photo limit is 10MB + expect(checkFileSize(3 * 1024 * 1024 * 1024, "video")).toBe(false); // Video limit is 2GB + }); + + it("should fall back to document limit if fileType is unknown", () => { + expect(checkFileSize(1 * 1024 * 1024 * 1024, "unknown")).toBe(true); // Document limit is 2GB + expect(checkFileSize(3 * 1024 * 1024 * 1024, "unknown")).toBe(false); + }); + }); + + describe("extractFileName", () => { + it("should extract file name from headers if present", () => { + const req = { headers: { "x-file-name": "custom.txt" } }; + expect(extractFileName({}, req)).toBe("custom.txt"); + }); + + it("should extract file name from various message attachment types", () => { + expect(extractFileName({ document: { fileName: "doc.pdf" } }, null)).toBe("doc.pdf"); + expect(extractFileName({ photo: [{ fileName: "low.jpg" }, { fileName: "high.jpg" }] }, null)).toBe("high.jpg"); + expect(extractFileName({ audio: { fileName: "song.mp3" } }, null)).toBe("song.mp3"); + expect(extractFileName({ voice: { fileName: "voice.ogg" } }, null)).toBe("voice.ogg"); + expect(extractFileName({ animation: { fileName: "anim.gif" } }, null)).toBe("anim.gif"); + }); + + it("should return default filename if not found", () => { + expect(extractFileName({}, null)).toBe("file"); + }); + }); + + describe("extractMimeType", () => { + it("should extract mime type from headers if present", () => { + const req = { headers: { "x-mime-type": "text/plain" } }; + expect(extractMimeType({}, req)).toBe("text/plain"); + }); + + it("should extract mime type from various message attachment types", () => { + expect(extractMimeType({ document: { mimeType: "application/pdf" } }, null)).toBe("application/pdf"); + expect(extractMimeType({ photo: [{ mimeType: "image/jpeg" }, { mimeType: "image/png" }] }, null)).toBe("image/png"); + expect(extractMimeType({ audio: { mimeType: "audio/mpeg" } }, null)).toBe("audio/mpeg"); + expect(extractMimeType({ voice: { mimeType: "audio/ogg" } }, null)).toBe("audio/ogg"); + expect(extractMimeType({ animation: { mimeType: "video/mp4" } }, null)).toBe("video/mp4"); + }); + + it("should return default mime type if not found", () => { + expect(extractMimeType({}, null)).toBe("application/octet-stream"); + }); + }); +});