feat(media): validate upload file size limits by media type
This commit is contained in:
parent
227be9ac41
commit
3a63240f46
|
|
@ -1,2 +1,5 @@
|
|||
# Public
|
||||
NEXT_PUBLIC_API_URL=https://localhost:3000/api
|
||||
NEXT_PUBLIC_MAX_IMAGE_UPLOAD_MB=50
|
||||
NEXT_PUBLIC_MAX_VIDEO_UPLOAD_MB=500
|
||||
NEXT_PUBLIC_MAX_AUDIO_UPLOAD_MB=100
|
||||
|
|
|
|||
|
|
@ -2,10 +2,22 @@ import { z } from "zod";
|
|||
|
||||
const webEnvSchema = z.object({
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||
NEXT_PUBLIC_MAX_IMAGE_UPLOAD_MB: z.coerce.number().positive().max(200).default(50),
|
||||
NEXT_PUBLIC_MAX_VIDEO_UPLOAD_MB: z.coerce
|
||||
.number()
|
||||
.positive()
|
||||
.max(2048)
|
||||
.default(500),
|
||||
NEXT_PUBLIC_MAX_AUDIO_UPLOAD_MB: z.coerce.number().positive().max(500).default(100),
|
||||
});
|
||||
|
||||
export const webEnv = webEnvSchema.parse({
|
||||
...process.env,
|
||||
NODE_ENV: process.env.NODE_ENV ?? "development",
|
||||
NEXT_PUBLIC_MAX_IMAGE_UPLOAD_MB:
|
||||
process.env.NEXT_PUBLIC_MAX_IMAGE_UPLOAD_MB ?? "50",
|
||||
NEXT_PUBLIC_MAX_VIDEO_UPLOAD_MB:
|
||||
process.env.NEXT_PUBLIC_MAX_VIDEO_UPLOAD_MB ?? "500",
|
||||
NEXT_PUBLIC_MAX_AUDIO_UPLOAD_MB:
|
||||
process.env.NEXT_PUBLIC_MAX_AUDIO_UPLOAD_MB ?? "100",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
formatFileSize,
|
||||
getFileSizeLimitBytes,
|
||||
getFileSizeLimitError,
|
||||
} from "@/lib/media/file-size-validation";
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
describe("media file size validation", () => {
|
||||
test("uses the configured default upload limits", () => {
|
||||
expect(getFileSizeLimitBytes({ mediaType: "image" })).toBe(50 * MB);
|
||||
expect(getFileSizeLimitBytes({ mediaType: "video" })).toBe(500 * MB);
|
||||
expect(getFileSizeLimitBytes({ mediaType: "audio" })).toBe(100 * MB);
|
||||
});
|
||||
|
||||
test("returns no error when file size is at the limit", () => {
|
||||
expect(
|
||||
getFileSizeLimitError({
|
||||
fileName: "clip.mp4",
|
||||
fileSize: 500 * MB,
|
||||
mediaType: "video",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("returns a clear message when file exceeds limit", () => {
|
||||
expect(
|
||||
getFileSizeLimitError({
|
||||
fileName: "huge.wav",
|
||||
fileSize: 101 * MB,
|
||||
mediaType: "audio",
|
||||
}),
|
||||
).toBe("huge.wav is 101 MB. audio files must be 100 MB or smaller.");
|
||||
});
|
||||
|
||||
test("formats small values using KB", () => {
|
||||
expect(formatFileSize({ bytes: 1500 })).toBe("1.5 KB");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { webEnv } from "@/env/web";
|
||||
import type { MediaType } from "@/types/assets";
|
||||
|
||||
const MB_IN_BYTES = 1024 * 1024;
|
||||
|
||||
const LIMIT_CAP_MB: Record<MediaType, number> = {
|
||||
image: 200,
|
||||
video: 2048,
|
||||
audio: 500,
|
||||
};
|
||||
|
||||
const LIMIT_DEFAULT_MB: Record<MediaType, number> = {
|
||||
image: 50,
|
||||
video: 500,
|
||||
audio: 100,
|
||||
};
|
||||
|
||||
const LIMITS_MB: Record<MediaType, number> = {
|
||||
image: Math.min(webEnv.NEXT_PUBLIC_MAX_IMAGE_UPLOAD_MB, LIMIT_CAP_MB.image),
|
||||
video: Math.min(webEnv.NEXT_PUBLIC_MAX_VIDEO_UPLOAD_MB, LIMIT_CAP_MB.video),
|
||||
audio: Math.min(webEnv.NEXT_PUBLIC_MAX_AUDIO_UPLOAD_MB, LIMIT_CAP_MB.audio),
|
||||
};
|
||||
|
||||
export function getFileSizeLimitBytes({
|
||||
mediaType,
|
||||
}: {
|
||||
mediaType: MediaType;
|
||||
}): number {
|
||||
return LIMITS_MB[mediaType] * MB_IN_BYTES;
|
||||
}
|
||||
|
||||
export function formatFileSize({
|
||||
bytes,
|
||||
}: {
|
||||
bytes: number;
|
||||
}): string {
|
||||
if (bytes < MB_IN_BYTES) {
|
||||
const kb = bytes / 1024;
|
||||
return `${kb.toFixed(kb >= 100 ? 0 : 1)} KB`;
|
||||
}
|
||||
|
||||
const mb = bytes / MB_IN_BYTES;
|
||||
if (mb < 1024) {
|
||||
return `${mb.toFixed(mb >= 100 ? 0 : 1)} MB`;
|
||||
}
|
||||
|
||||
const gb = mb / 1024;
|
||||
return `${gb.toFixed(gb >= 100 ? 0 : 1)} GB`;
|
||||
}
|
||||
|
||||
export function getFileSizeLimitError({
|
||||
fileName,
|
||||
fileSize,
|
||||
mediaType,
|
||||
}: {
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
mediaType: MediaType;
|
||||
}): string | null {
|
||||
const limitBytes = getFileSizeLimitBytes({ mediaType });
|
||||
if (fileSize <= limitBytes) return null;
|
||||
|
||||
return `${fileName} is ${formatFileSize({ bytes: fileSize })}. ${mediaType} files must be ${formatFileSize({ bytes: limitBytes })} or smaller.`;
|
||||
}
|
||||
|
||||
export const mediaUploadLimitDefaultsMb = LIMIT_DEFAULT_MB;
|
||||
export const mediaUploadLimitCapsMb = LIMIT_CAP_MB;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { toast } from "sonner";
|
||||
import type { MediaAsset } from "@/types/assets";
|
||||
import { getMediaTypeFromFile } from "@/lib/media/media-utils";
|
||||
import { getFileSizeLimitError } from "@/lib/media/file-size-validation";
|
||||
import { getVideoInfo } from "./mediabunny";
|
||||
import { Input, ALL_FORMATS, BlobSource, VideoSampleSink } from "mediabunny";
|
||||
|
||||
|
|
@ -166,6 +167,16 @@ export async function processMediaAssets({
|
|||
continue;
|
||||
}
|
||||
|
||||
const fileSizeLimitError = getFileSizeLimitError({
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
mediaType: fileType,
|
||||
});
|
||||
if (fileSizeLimitError) {
|
||||
toast.error(fileSizeLimitError);
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
let thumbnailUrl: string | undefined;
|
||||
let duration: number | undefined;
|
||||
|
|
|
|||
Loading…
Reference in New Issue