mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
media: added more mime types, added mime type check on dropzone
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import * as large from "../../src/media/storage/mime-types";
|
import * as large from "../../src/media/storage/mime-types";
|
||||||
import * as tiny from "../../src/media/storage/mime-types-tiny";
|
import * as tiny from "../../src/media/storage/mime-types-tiny";
|
||||||
|
import { getRandomizedFilename } from "../../src/media/utils";
|
||||||
|
|
||||||
describe("media/mime-types", () => {
|
describe("media/mime-types", () => {
|
||||||
test("tiny resolves", () => {
|
test("tiny resolves", () => {
|
||||||
@@ -36,19 +37,62 @@ describe("media/mime-types", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("isMimeType", () => {
|
test("isMimeType", () => {
|
||||||
expect(tiny.isMimeType("image/jpeg")).toBe(true);
|
const tests = [
|
||||||
expect(tiny.isMimeType("image/jpeg", ["image/png"])).toBe(true);
|
["image/avif", true],
|
||||||
expect(tiny.isMimeType("image/png", ["image/png"])).toBe(false);
|
["image/AVIF", true],
|
||||||
expect(tiny.isMimeType("image/png")).toBe(true);
|
["image/jpeg", true],
|
||||||
expect(tiny.isMimeType("whatever")).toBe(false);
|
["image/jpeg", true, ["image/png"]],
|
||||||
expect(tiny.isMimeType("text/tab-separated-values")).toBe(true);
|
["image/png", false, ["image/png"]],
|
||||||
|
["image/png", true],
|
||||||
|
["image/heif", true],
|
||||||
|
["image/heic", true],
|
||||||
|
["image/gif", true],
|
||||||
|
["whatever", false],
|
||||||
|
["text/tab-separated-values", true],
|
||||||
|
["application/zip", true]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [mime, expected, exclude] of tests) {
|
||||||
|
expect(
|
||||||
|
tiny.isMimeType(mime, exclude as any),
|
||||||
|
`isMimeType(): ${mime} should be ${expected}`
|
||||||
|
).toBe(expected as any);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("extension", () => {
|
test("extension", () => {
|
||||||
expect(tiny.extension("image/png")).toBe("png");
|
const tests = [
|
||||||
expect(tiny.extension("image/jpeg")).toBe("jpeg");
|
["image/avif", "avif"],
|
||||||
expect(tiny.extension("application/zip")).toBe("zip");
|
["image/png", "png"],
|
||||||
expect(tiny.extension("text/tab-separated-values")).toBe("tsv");
|
["image/PNG", "png"],
|
||||||
expect(tiny.extension("application/zip")).toBe("zip");
|
["image/jpeg", "jpeg"],
|
||||||
|
["application/zip", "zip"],
|
||||||
|
["text/tab-separated-values", "tsv"],
|
||||||
|
["application/zip", "zip"]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [mime, ext] of tests) {
|
||||||
|
expect(tiny.extension(mime), `extension(): ${mime} should be ${ext}`).toBe(ext);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getRandomizedFilename", () => {
|
||||||
|
const tests = [
|
||||||
|
["file.txt", "txt"],
|
||||||
|
["file.TXT", "txt"],
|
||||||
|
["image.jpg", "jpg"],
|
||||||
|
["image.avif", "avif"],
|
||||||
|
["image.heic", "heic"],
|
||||||
|
["image.jpeg", "jpeg"],
|
||||||
|
["-473Wx593H-466453554-black-MODEL.jpg", "jpg"],
|
||||||
|
["-473Wx593H-466453554-black-MODEL.avif", "avif"]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [filename, ext] of tests) {
|
||||||
|
expect(
|
||||||
|
getRandomizedFilename(filename).split(".").pop(),
|
||||||
|
`getRandomizedFilename(): ${filename} should end with ${ext}`
|
||||||
|
).toBe(ext);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const Q = {
|
export const Q = {
|
||||||
video: ["mp4", "webm"],
|
video: ["mp4", "webm"],
|
||||||
audio: ["ogg"],
|
audio: ["ogg"],
|
||||||
image: ["jpeg", "png", "gif", "webp", "bmp", "tiff"],
|
image: ["jpeg", "png", "gif", "webp", "bmp", "tiff", "avif", "heic", "heif"],
|
||||||
text: ["html", "css", "mdx", "yaml", "vcard", "csv", "vtt"],
|
text: ["html", "css", "mdx", "yaml", "vcard", "csv", "vtt"],
|
||||||
application: ["zip", "xml", "toml", "json", "json5"],
|
application: ["zip", "xml", "toml", "json", "json5"],
|
||||||
font: ["woff", "woff2", "ttf", "otf"]
|
font: ["woff", "woff2", "ttf", "otf"]
|
||||||
@@ -76,7 +76,8 @@ export function guess(f: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMimeType(mime: any, exclude: string[] = []) {
|
export function isMimeType(_mime: any, exclude: string[] = []) {
|
||||||
|
const mime = _mime.toLowerCase();
|
||||||
if (exclude.includes(mime)) return false;
|
if (exclude.includes(mime)) return false;
|
||||||
|
|
||||||
// try quick first
|
// try quick first
|
||||||
@@ -96,7 +97,9 @@ export function isMimeType(mime: any, exclude: string[] = []) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extension(mime: string) {
|
export function extension(_mime: string) {
|
||||||
|
const mime = _mime.toLowerCase();
|
||||||
|
|
||||||
for (const [t, e] of Object.entries(Q)) {
|
for (const [t, e] of Object.entries(Q)) {
|
||||||
for (const _e of e) {
|
for (const _e of e) {
|
||||||
if (mime === `${t}/${_e}`) {
|
if (mime === `${t}/${_e}`) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export function getExtensionFromName(filename: string): string | undefined {
|
|||||||
if (!filename.includes(".")) return;
|
if (!filename.includes(".")) return;
|
||||||
|
|
||||||
const parts = filename.split(".");
|
const parts = filename.split(".");
|
||||||
return parts[parts.length - 1];
|
return parts[parts.length - 1]?.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRandomizedFilename(file: File, length?: number): string;
|
export function getRandomizedFilename(file: File, length?: number): string;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type DropzoneProps = {
|
|||||||
initialItems?: FileState[];
|
initialItems?: FileState[];
|
||||||
flow?: "start" | "end";
|
flow?: "start" | "end";
|
||||||
maxItems?: number;
|
maxItems?: number;
|
||||||
|
allowedMimeTypes?: string[];
|
||||||
overwrite?: boolean;
|
overwrite?: boolean;
|
||||||
autoUpload?: boolean;
|
autoUpload?: boolean;
|
||||||
onRejected?: (files: FileWithPath[]) => void;
|
onRejected?: (files: FileWithPath[]) => void;
|
||||||
@@ -75,6 +76,7 @@ export function Dropzone({
|
|||||||
handleDelete,
|
handleDelete,
|
||||||
initialItems = [],
|
initialItems = [],
|
||||||
flow = "start",
|
flow = "start",
|
||||||
|
allowedMimeTypes,
|
||||||
maxItems,
|
maxItems,
|
||||||
overwrite,
|
overwrite,
|
||||||
autoUpload,
|
autoUpload,
|
||||||
@@ -109,8 +111,26 @@ export function Dropzone({
|
|||||||
return added > remaining;
|
return added > remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAllowed(i: DataTransferItem | DataTransferItem[] | File | File[]): boolean {
|
||||||
|
const items = Array.isArray(i) ? i : [i];
|
||||||
|
const specs = items.map((item) => ({
|
||||||
|
kind: "kind" in item ? item.kind : "file",
|
||||||
|
type: item.type,
|
||||||
|
size: "size" in item ? item.size : 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
return specs.every((spec) => {
|
||||||
|
if (spec.kind !== "file") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !(allowedMimeTypes && !allowedMimeTypes.includes(spec.type));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { isOver, handleFileInputChange, ref } = useDropzone({
|
const { isOver, handleFileInputChange, ref } = useDropzone({
|
||||||
onDropped: (newFiles: FileWithPath[]) => {
|
onDropped: (newFiles: FileWithPath[]) => {
|
||||||
|
if (!isAllowed(newFiles)) return;
|
||||||
|
|
||||||
let to_drop = 0;
|
let to_drop = 0;
|
||||||
const added = newFiles.length;
|
const added = newFiles.length;
|
||||||
|
|
||||||
@@ -155,6 +175,11 @@ export function Dropzone({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOver: (items) => {
|
onOver: (items) => {
|
||||||
|
if (!isAllowed(items)) {
|
||||||
|
setIsOverAccepted(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const max_reached = isMaxReached(items.length);
|
const max_reached = isMaxReached(items.length);
|
||||||
setIsOverAccepted(!max_reached);
|
setIsOverAccepted(!max_reached);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ export function DropzoneContainer({
|
|||||||
return api.media.deleteFile(file.path);
|
return api.media.deleteFile(file.path);
|
||||||
});
|
});
|
||||||
|
|
||||||
const actualItems = (initialItems || $q.data || []) as MediaFieldSchema[];
|
const actualItems = (initialItems ??
|
||||||
|
(Array.isArray($q.data) ? $q.data : []) ??
|
||||||
|
[]) as MediaFieldSchema[];
|
||||||
const _initialItems = mediaItemsToFileStates(actualItems, { baseUrl });
|
const _initialItems = mediaItemsToFileStates(actualItems, { baseUrl });
|
||||||
|
|
||||||
const key = id + JSON.stringify(_initialItems);
|
const key = id + JSON.stringify(_initialItems);
|
||||||
|
|||||||
@@ -80,10 +80,8 @@ export interface FileWithPath extends File {
|
|||||||
export async function fromEvent(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]> {
|
export async function fromEvent(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]> {
|
||||||
if (isObject<DragEvent>(evt) && isDataTransfer(evt.dataTransfer)) {
|
if (isObject<DragEvent>(evt) && isDataTransfer(evt.dataTransfer)) {
|
||||||
return getDataTransferFiles(evt.dataTransfer, evt.type);
|
return getDataTransferFiles(evt.dataTransfer, evt.type);
|
||||||
// biome-ignore lint/style/noUselessElse: not useless
|
|
||||||
} else if (isChangeEvt(evt)) {
|
} else if (isChangeEvt(evt)) {
|
||||||
return getInputFiles(evt);
|
return getInputFiles(evt);
|
||||||
// biome-ignore lint/style/noUselessElse: not useless
|
|
||||||
} else if (
|
} else if (
|
||||||
Array.isArray(evt) &&
|
Array.isArray(evt) &&
|
||||||
evt.every((item) => "getFile" in item && typeof item.getFile === "function")
|
evt.every((item) => "getFile" in item && typeof item.getFile === "function")
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { type FileWithPath, fromEvent } from "./file-selector";
|
|||||||
|
|
||||||
type DropzoneProps = {
|
type DropzoneProps = {
|
||||||
onDropped: (files: FileWithPath[]) => void;
|
onDropped: (files: FileWithPath[]) => void;
|
||||||
onOver?: (items: DataTransferItem[]) => void;
|
onOver?: (items: DataTransferItem[], event: DragEvent) => void;
|
||||||
onLeave?: () => void;
|
onLeave?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const events = {
|
const events = {
|
||||||
enter: ["dragenter", "dragover", "dragstart"],
|
enter: ["dragenter", "dragover", "dragstart"] as const,
|
||||||
leave: ["dragleave", "drop"]
|
leave: ["dragleave", "drop"] as const
|
||||||
};
|
};
|
||||||
const allEvents = [...events.enter, ...events.leave];
|
const allEvents = [...events.enter, ...events.leave];
|
||||||
|
|
||||||
@@ -24,10 +24,10 @@ export function useDropzone({ onDropped, onOver, onLeave }: DropzoneProps) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleHighlight = useCallback(async (e: Event) => {
|
const toggleHighlight = useCallback(async (e: DragEvent) => {
|
||||||
const _isOver = events.enter.includes(e.type);
|
const _isOver = events.enter.includes(e.type as any);
|
||||||
if (onOver && _isOver !== isOver && !onOverCalled.current) {
|
if (onOver && _isOver !== isOver && !onOverCalled.current) {
|
||||||
onOver((await fromEvent(e)) as DataTransferItem[]);
|
onOver((await fromEvent(e)) as DataTransferItem[], e);
|
||||||
onOverCalled.current = true;
|
onOverCalled.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ export default function DropzoneElementTest() {
|
|||||||
<Scrollable>
|
<Scrollable>
|
||||||
<div className="flex flex-col w-full h-full p-4 gap-4">
|
<div className="flex flex-col w-full h-full p-4 gap-4">
|
||||||
<div>
|
<div>
|
||||||
|
<b>Dropzone no auto avif only</b>
|
||||||
|
<Media.Dropzone autoUpload={false} allowedMimeTypes={["image/avif"]} />
|
||||||
|
|
||||||
<b>Dropzone User Avatar 1 (fully customized)</b>
|
<b>Dropzone User Avatar 1 (fully customized)</b>
|
||||||
<Media.Dropzone
|
<Media.Dropzone
|
||||||
entity={{ name: "users", id: 1, field: "avatar" }}
|
entity={{ name: "users", id: 1, field: "avatar" }}
|
||||||
|
|||||||
Reference in New Issue
Block a user