feat: enhance file handling by adding MIME type resolution and updating file creation permissions

This commit is contained in:
Nawaz Dhandala
2025-11-18 18:05:46 +00:00
parent 654f64aaf7
commit 62d74c1d84
3 changed files with 134 additions and 81 deletions

View File

@@ -5,6 +5,7 @@ import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
import TableMetadata from "../../Types/Database/TableMetadata";
import IconProp from "../../Types/Icon/IconProp";
import Permission from "../../Types/Permission";
import { Entity } from "typeorm";
@EnableDocumentation()
@@ -20,7 +21,7 @@ import { Entity } from "typeorm";
})
@CrudApiEndpoint(new Route("/file"))
@TableAccessControl({
create: [],
create: [Permission.CurrentUser, Permission.AuthenticatedRequest],
read: [],
delete: [],
update: [],

View File

@@ -6,6 +6,24 @@ enum MimeType {
jpg = "image/jpeg",
jpeg = "image/jpeg",
svg = "image/svg+xml",
gif = "image/gif",
webp = "image/webp",
pdf = "application/pdf",
doc = "application/msword",
docx = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
txt = "text/plain",
md = "text/markdown",
csv = "text/csv",
rtf = "application/rtf",
odt = "application/vnd.oasis.opendocument.text",
json = "application/json",
zip = "application/zip",
xls = "application/vnd.ms-excel",
xlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ods = "application/vnd.oasis.opendocument.spreadsheet",
ppt = "application/vnd.ms-powerpoint",
pptx = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
odp = "application/vnd.oasis.opendocument.presentation",
// TODO add more mime types.
}

View File

@@ -90,16 +90,57 @@ const FilePicker: FunctionComponent<ComponentProps> = (
// Upload these files.
const filesResult: Array<FileModel> = [];
const resolveMimeType = (file: File): MimeType | undefined => {
const direct: string | undefined = file.type || undefined;
if (direct && Object.values(MimeType).includes(direct as MimeType)) {
return direct as MimeType;
}
// fallback based on extension
const ext: string | undefined = file.name
.split(".")
.pop()
?.toLowerCase();
if (!ext) {
return undefined;
}
const map: { [key: string]: MimeType } = {
png: MimeType.png,
jpg: MimeType.jpg,
jpeg: MimeType.jpeg,
svg: MimeType.svg,
gif: MimeType.gif,
webp: MimeType.webp,
pdf: MimeType.pdf,
doc: MimeType.doc,
docx: MimeType.docx,
txt: MimeType.txt,
log: MimeType.txt,
md: MimeType.md,
markdown: MimeType.md,
csv: MimeType.csv,
json: MimeType.json,
zip: MimeType.zip,
rtf: MimeType.rtf,
odt: MimeType.odt,
xls: MimeType.xls,
xlsx: MimeType.xlsx,
ods: MimeType.ods,
ppt: MimeType.ppt,
pptx: MimeType.pptx,
odp: MimeType.odp,
};
return map[ext];
};
for (const acceptedFile of acceptedFiles) {
const fileModel: FileModel = new FileModel();
fileModel.name = acceptedFile.name;
const arrayBuffer: ArrayBuffer = await acceptedFile.arrayBuffer();
const fileBuffer: Uint8Array = new Uint8Array(arrayBuffer);
fileModel.file = Buffer.from(fileBuffer);
fileModel.isPublic = false;
fileModel.fileType = acceptedFile.type as MimeType;
fileModel.fileType = resolveMimeType(acceptedFile) || MimeType.txt; // default to text/plain to satisfy required field
const result: HTTPResponse<FileModel> =
(await ModelAPI.create<FileModel>({
@@ -183,84 +224,77 @@ const FilePicker: FunctionComponent<ComponentProps> = (
data-testid={props.dataTestId}
className="flex max-w-lg justify-center rounded-md border-2 border-dashed border-gray-300 px-6 pt-5 pb-6"
>
{props.isMultiFilePicker ||
(filesModel.length === 0 && (
<div
{...getRootProps({
className: "space-y-1 text-center",
})}
>
<svg
className="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
<div className="flex text-sm text-gray-600">
<label className="relative cursor-pointer rounded-md bg-white font-medium text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:text-indigo-500">
{!props.placeholder && !error && (
<span>{"Upload a file"}</span>
)}
{error && (
<span>
<span>{error}</span>
</span>
)}
{props.placeholder && !error && (
<span>{props.placeholder}</span>
)}
<input
tabIndex={props.tabIndex}
{...(getInputProps() as any)}
id="file-upload"
name="file-upload"
type="file"
className="sr-only"
/>
</label>
<p className="pl-1">or drag and drop</p>
<div
{...getRootProps({
className: "w-full space-y-3 text-center",
})}
>
{(filesModel.length === 0 || props.isMultiFilePicker) && (
<>
<div className="flex flex-col items-center space-y-2">
<svg
className="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
<div className="flex flex-col items-center text-sm text-gray-600 space-y-1">
<label className="relative cursor-pointer rounded-md bg-white font-medium text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:text-indigo-500">
{!props.placeholder && !error && (
<span>{filesModel.length > 0 ? "Add more files" : "Upload files"}</span>
)}
{error && (
<span>
<span>{error}</span>
</span>
)}
{props.placeholder && !error && (
<span>{props.placeholder}</span>
)}
<input
tabIndex={props.tabIndex}
{...(getInputProps() as any)}
id="file-upload"
name="file-upload"
type="file"
className="sr-only"
/>
</label>
<p className="text-gray-500">
{filesModel.length === 0 ? "Click to choose files" : "Click to add more"} or drag & drop.
</p>
<p className="text-xs text-gray-500">
{props.mimeTypes && props.mimeTypes?.length > 0 && (
<span>Types: </span>
)}
{props.mimeTypes &&
props.mimeTypes
.map((type: MimeType) => {
const enumKey: string | undefined =
Object.keys(MimeType)[
Object.values(MimeType).indexOf(type)
];
return enumKey?.toUpperCase() || "";
})
.filter((item: string | undefined, pos: number, array: Array<string | undefined>) => {
return array.indexOf(item) === pos;
})
.join(", ")}
{props.mimeTypes && props.mimeTypes?.length > 0 && <span>.</span>} Max 10MB each.
</p>
</div>
</div>
<p className="text-xs text-gray-500">
{props.mimeTypes && props.mimeTypes?.length > 0 && (
<span>File types: </span>
)}
{props.mimeTypes &&
props.mimeTypes
.map((type: MimeType) => {
const enumKey: string | undefined =
Object.keys(MimeType)[
Object.values(MimeType).indexOf(type)
];
return enumKey?.toUpperCase() || "";
})
.filter(
(
item: string | undefined,
pos: number,
array: Array<string | undefined>,
) => {
return array.indexOf(item) === pos;
},
)
.join(", ")}
{props.mimeTypes && props.mimeTypes?.length > 0 && (
<span>.</span>
)}
&nbsp;10 MB or less.
</p>
</div>
))}
</>
)}
</div>
<aside>{getThumbs()}</aside>
</div>
{props.error && (