feat: enhance mime type handling and improve file metadata management

Updated mime types to include additional formats for text and application categories. Improved file metadata handling in the Storage class to accurately reflect file properties during uploads. Enhanced the DropzoneInner component with new icons for various file types and refined the MediaInfoModal to support a broader range of text formats for previews.
This commit is contained in:
dswbx
2025-09-29 15:55:25 +02:00
parent 55082e9d0e
commit d31416f85d
4 changed files with 90 additions and 23 deletions

View File

@@ -71,22 +71,29 @@ export class Storage implements EmitsEvents {
let info: FileUploadPayload = {
name,
meta: {
meta: isFile(file)
? {
size: file.size,
type: file.type,
}
: {
size: 0,
type: "application/octet-stream",
},
etag: typeof result === "string" ? result : "",
};
// normally only etag is returned
if (typeof result === "object") {
info = result;
} else if (isFile(file)) {
info.meta.size = file.size;
info.meta.type = file.type;
}
// try to get better meta info
if (!isMimeType(info.meta.type, ["application/octet-stream", "application/json"])) {
if (
!info.meta.type ||
["application/octet-stream", "application/json"].includes(info.meta.type) ||
!info.meta.size
) {
const meta = await this.#adapter.getObjectMeta(name);
if (!meta) {
throw new Error("Failed to get object meta");

View File

@@ -2,8 +2,8 @@ export const Q = {
video: ["mp4", "webm"],
audio: ["ogg"],
image: ["jpeg", "png", "gif", "webp", "bmp", "tiff", "avif", "heic", "heif"],
text: ["html", "css", "mdx", "yaml", "vcard", "csv", "vtt"],
application: ["zip", "xml", "toml", "json", "json5", "pdf"],
text: ["html", "css", "mdx", "yaml", "vcard", "csv", "vtt", "xml"],
application: ["zip", "toml", "json", "json5", "pdf", "sql"],
font: ["woff", "woff2", "ttf", "otf"],
} as const;

View File

@@ -1,7 +1,22 @@
import { type ComponentPropsWithoutRef, memo, type ReactNode, useCallback, useMemo } from "react";
import { twMerge } from "tailwind-merge";
import { useRenderCount } from "ui/hooks/use-render-count";
import { TbDots, TbExternalLink, TbTrash, TbUpload } from "react-icons/tb";
import {
TbDots,
TbExternalLink,
TbFileTypeCsv,
TbFileText,
TbJson,
TbFileTypePdf,
TbMarkdown,
TbMusic,
TbTrash,
TbUpload,
TbFileTypeTxt,
TbFileTypeXml,
TbZip,
TbFileTypeSql,
} from "react-icons/tb";
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown";
import { IconButton } from "ui/components/buttons/IconButton";
import { formatNumber } from "core/utils";
@@ -85,7 +100,7 @@ const UploadPlaceholder = ({ onClick, text = "Upload files" }) => {
);
};
type ReducedFile = Pick<FileState, "body" | "type" | "path" | "name" | "size">;
type ReducedFile = Omit<FileState, "state" | "progress">;
export type PreviewComponentProps = {
file: ReducedFile;
fallback?: (props: { file: ReducedFile }) => ReactNode;
@@ -271,8 +286,59 @@ const VideoPreview = ({
return <video {...props} src={objectUrl} />;
};
const Previews = [
{
mime: "text/plain",
Icon: TbFileTypeTxt,
},
{
mime: "text/csv",
Icon: TbFileTypeCsv,
},
{
mime: "text/xml",
Icon: TbFileTypeXml,
},
{
mime: "text/markdown",
Icon: TbMarkdown,
},
{
mime: /^text\/.*$/,
Icon: TbFileText,
},
{
mime: "application/json",
Icon: TbJson,
},
{
mime: "application/pdf",
Icon: TbFileTypePdf,
},
{
mime: /^audio\/.*$/,
Icon: TbMusic,
},
{
mime: "application/zip",
Icon: TbZip,
},
{
mime: "application/sql",
Icon: TbFileTypeSql,
},
];
const FallbackPreview = ({ file }: { file: ReducedFile }) => {
const previewIcon = Previews.find((p) =>
p.mime instanceof RegExp ? p.mime.test(file.type) : p.mime === file.type,
);
if (previewIcon) {
return <previewIcon.Icon className="size-10 text-gray-400" />;
}
return (
<div className="text-xs text-primary/50 text-center font-mono leading-none">{file.type}</div>
<div className="text-xs text-primary/50 text-center font-mono leading-none max-w-[90%] truncate">
{file.type}
</div>
);
};

View File

@@ -156,6 +156,8 @@ const Item = ({
);
};
const textFormats = [/^text\/.*$/, /application\/(json|ld\+json|javascript|xml|rtf|sql)/];
const FilePreview = ({ file }: { file: FileState }) => {
const objectUrl = typeof file.body === "string" ? file.body : URL.createObjectURL(file.body);
@@ -174,15 +176,7 @@ const FilePreview = ({ file }: { file: FileState }) => {
/>
);
}
if (
[
"text/plain",
"text/markdown",
"text/csv",
"text/tab-separated-values",
"application/json",
].includes(file.type)
) {
if (textFormats.some((f) => f.test(file.type))) {
return <TextPreview file={file} />;
}
@@ -227,7 +221,7 @@ const TextPreview = ({ file }: { file: FileState }) => {
}, [file, useRange]);
return (
<pre className="text-sm font-mono whitespace-pre-wrap break-all overflow-y-scroll w-250 md:max-w-[80dvw] h-[60dvh] md:h-[80dvh] py-4 px-6 debug">
<pre className="text-sm font-mono whitespace-pre-wrap break-all overflow-y-scroll w-250 md:max-w-[80dvw] h-[60dvh] md:h-[80dvh] py-4 px-6">
{text}
{useRange && (