From 5570f8781c4db10019b648bbc90d868c44021a1b Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sat, 30 Nov 2024 10:00:02 +0100 Subject: [PATCH] Add media type sniffing by extension --- packages/common/src/MediaType.ts | 69 ++++++++++++++++++++++++++ packages/frontend/src/FileReducer.ts | 7 +-- packages/frontend/src/routes/Piece.tsx | 2 + 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 packages/common/src/MediaType.ts diff --git a/packages/common/src/MediaType.ts b/packages/common/src/MediaType.ts new file mode 100644 index 0000000..cebd9f3 --- /dev/null +++ b/packages/common/src/MediaType.ts @@ -0,0 +1,69 @@ +const mediaTypeExtension = new Map(); +const extensionMediaType = new Map(); + +register("application/x-musescore", "mscz"); +register("application/vnd.recordare.musicxml+xml", "musicxml"); +register("application/vnd.recordare.musicxml", "mxl"); +register("application/pdf", "pdf"); + +register("audio/aac", "aac"); +register("audio/flac", "flac"); +register("audio/midi", "mid", "midi"); +register("audio/mpeg", "mp3"); +register("audio/wav", "wav"); + +export const ACCEPTED_EXTENSIONS = (() => { + const ret = []; + for (const extension of extensionMediaType.keys()) { + ret.push("." + extension); + } + + ret.sort((a, b) => a.localeCompare(b)); + return ret.join(","); +})(); + +function register(mediaType: string, ...extensions: [string, ...string[]]) { + const [primaryExtension, ...secondaryExtensions] = extensions; + + mediaTypeExtension.set(mediaType, primaryExtension); + + extensionMediaType.set(primaryExtension, mediaType); + for (const extension of secondaryExtensions) { + extensionMediaType.set(extension, mediaType); + } +} + +export function getMediaTypeForExtension(extension: string): string | undefined { + return extensionMediaType.get(extension); +} + +export function getPrimaryExtension(extension: string): string { + const mediaType = extensionMediaType.get(extension); + if (mediaType === undefined) { + return extension; + } + + const primaryExtension = mediaTypeExtension.get(mediaType)!; + return primaryExtension; +} + +export function getExtensionFromMediaType(mediaType: string): string | undefined { + return mediaTypeExtension.get(mediaType); +} + +export function getExtensionFromFilename(filename: string): string | undefined { + return filename.match(/(?<=.)[^.]+$/)?.[0]; +} + +export function getMediaTypeForFilename(filename: string): string | undefined { + const extension = getExtensionFromFilename(filename); + if (extension === undefined) { + return undefined; + } + + return getMediaTypeForExtension(extension); +} + +export function getMediaTypeForFile(file: File): string { + return getMediaTypeForFilename(file.name) ?? file.type; +} diff --git a/packages/frontend/src/FileReducer.ts b/packages/frontend/src/FileReducer.ts index a068bef..ed3ad13 100644 --- a/packages/frontend/src/FileReducer.ts +++ b/packages/frontend/src/FileReducer.ts @@ -1,3 +1,4 @@ +import { getMediaTypeForFile } from "common/MediaType"; import { mapProp, Update } from "./store"; export function FileReducer(prev: FileReducer.State, action: FileReducer.Action): FileReducer.State { @@ -7,10 +8,10 @@ export function FileReducer(prev: FileReducer.State, action: FileReducer.Action) case "file": if (prev.file !== null) { if (action.file !== null) { - if (prev.file.name === prev.filename && prev.file.type === prev.mediaType) { + if (prev.file.name === prev.filename && getMediaTypeForFile(prev.file) === prev.mediaType) { return Object.freeze({ filename: action.file.name, - mediaType: action.file.type, + mediaType: getMediaTypeForFile(action.file), file: action.file, }); } else { @@ -28,7 +29,7 @@ export function FileReducer(prev: FileReducer.State, action: FileReducer.Action) if (prev.filename === "" && prev.mediaType === "") { return Object.freeze({ filename: action.file.name, - mediaType: action.file.type, + mediaType: getMediaTypeForFile(action.file), file: action.file, }); } else { diff --git a/packages/frontend/src/routes/Piece.tsx b/packages/frontend/src/routes/Piece.tsx index d33ad84..29d8c12 100644 --- a/packages/frontend/src/routes/Piece.tsx +++ b/packages/frontend/src/routes/Piece.tsx @@ -1,5 +1,6 @@ import type { Attachment, Piece } from "backend/database"; import { AttachmentId, PieceId } from "common"; +import { ACCEPTED_EXTENSIONS } from "common/MediaType"; import { ELYSIA_FORM_DATA } from "elysia"; import { FormEventHandler, useId, useReducer, useRef, useState } from "react"; import { Link, useParams } from "react-router-dom"; @@ -254,6 +255,7 @@ function AttachmentForm(props: AttachmentForm.Props) { const file = e.target.files?.item(0) ?? null; reduce(FileReducer.setFile(file)); }} + accept={ACCEPTED_EXTENSIONS} />