From 40e66def1e7d05bcb74efeb0155f507a58c639e8 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 1 Jun 2025 16:06:56 +1000 Subject: [PATCH] Multi-upload to image library #56 (#60) * feat: support for file upload handler to track multiple files * feat: update image upload endpoint to allow multiple files * fix: lint --- components/UploadFileDialog.vue | 32 +++++++++++++++----- pages/admin/metadata/games/[id]/index.vue | 1 + server/api/v1/admin/game/image/index.post.ts | 6 ++-- server/api/v1/admin/game/metadata.post.ts | 6 ++-- server/api/v1/admin/news/index.post.ts | 5 +-- server/internal/utils/handlefileupload.ts | 14 ++++----- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/components/UploadFileDialog.vue b/components/UploadFileDialog.vue index b2b8b91..101f40c 100644 --- a/components/UploadFileDialog.vue +++ b/components/UploadFileDialog.vue @@ -51,15 +51,22 @@ class="transition mt-2 block text-sm font-semibold text-zinc-400 group-hover:text-zinc-500" >Upload file -

- {{ currentFile.name }} -

+
+

+ {{ currentFile }} +

+
@@ -67,7 +74,7 @@
({ }); const file = ref(); -const currentFile = computed(() => file.value?.item(0)); +const currentFiles = computed(() => file.value); +const currentFileList = computed(() => { + if (!currentFiles.value) return undefined; + const list = []; + for (const file of currentFiles.value) { + list.push(file.name); + } + return list; +}); const props = defineProps<{ endpoint: string; accept: string; + multiple?: boolean; options?: { [key: string]: string }; }>(); const emit = defineEmits(["upload"]); @@ -134,10 +150,12 @@ const emit = defineEmits(["upload"]); const uploadLoading = ref(false); const uploadError = ref(); async function uploadFile() { - if (!currentFile.value) return; + if (!currentFiles.value) return; const form = new FormData(); - form.append("file", currentFile.value); + for (const file of currentFiles.value) { + form.append(file.name, file); + } if (props.options) { for (const [key, value] of Object.entries(props.options)) { diff --git a/pages/admin/metadata/games/[id]/index.vue b/pages/admin/metadata/games/[id]/index.vue index f773a3c..415fe7e 100644 --- a/pages/admin/metadata/games/[id]/index.vue +++ b/pages/admin/metadata/games/[id]/index.vue @@ -293,6 +293,7 @@ :options="{ id: game.id }" accept="image/*" endpoint="/api/v1/admin/game/image" + :multiple="true" @upload="(result: Game) => uploadAfterImageUpload(result)" /> diff --git a/server/api/v1/admin/game/image/index.post.ts b/server/api/v1/admin/game/image/index.post.ts index 7ff453d..e230e9f 100644 --- a/server/api/v1/admin/game/image/index.post.ts +++ b/server/api/v1/admin/game/image/index.post.ts @@ -20,8 +20,8 @@ export default defineEventHandler(async (h3) => { statusMessage: "Failed to upload file", }); - const [id, options, pull, dump] = uploadResult; - if (!id) { + const [ids, options, pull, dump] = uploadResult; + if (ids.length == 0) { dump(); throw createError({ statusCode: 400, @@ -48,7 +48,7 @@ export default defineEventHandler(async (h3) => { }, data: { mImageLibraryObjectIds: { - push: id, + push: ids, }, }, }); diff --git a/server/api/v1/admin/game/metadata.post.ts b/server/api/v1/admin/game/metadata.post.ts index df64c04..c64ef97 100644 --- a/server/api/v1/admin/game/metadata.post.ts +++ b/server/api/v1/admin/game/metadata.post.ts @@ -14,14 +14,16 @@ export default defineEventHandler(async (h3) => { statusMessage: "This endpoint requires multipart form data.", }); - const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]); + const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1); if (!uploadResult) throw createError({ statusCode: 400, statusMessage: "Failed to upload file", }); - const [id, options, pull, dump] = uploadResult; + const [ids, options, pull, dump] = uploadResult; + + const id = ids.at(0); // handleFileUpload reads the rest of the options for us. const name = options.name; diff --git a/server/api/v1/admin/news/index.post.ts b/server/api/v1/admin/news/index.post.ts index 5e3286e..abda24d 100644 --- a/server/api/v1/admin/news/index.post.ts +++ b/server/api/v1/admin/news/index.post.ts @@ -14,19 +14,20 @@ export default defineEventHandler(async (h3) => { statusMessage: "This endpoint requires multipart form data.", }); - const uploadResult = await handleFileUpload(h3, {}, ["internal:read"]); + const uploadResult = await handleFileUpload(h3, {}, ["internal:read"], 1); if (!uploadResult) throw createError({ statusCode: 400, statusMessage: "Failed to upload file", }); - const [imageId, options, pull, _dump] = uploadResult; + const [imageIds, options, pull, _dump] = uploadResult; const title = options.title; const description = options.description; const content = options.content; const tags = options.tags ? (JSON.parse(options.tags) as string[]) : []; + const imageId = imageIds.at(0); if (!title || !description || !content) throw createError({ diff --git a/server/internal/utils/handlefileupload.ts b/server/internal/utils/handlefileupload.ts index e5fe80c..4e36c44 100644 --- a/server/internal/utils/handlefileupload.ts +++ b/server/internal/utils/handlefileupload.ts @@ -6,23 +6,21 @@ export async function handleFileUpload( h3: H3Event, metadata: { [key: string]: string }, permissions: Array, -): Promise< - [string | undefined, { [key: string]: string }, Pull, Dump] | undefined -> { + max = -1, +): Promise<[string[], { [key: string]: string }, Pull, Dump] | undefined> { const formData = await readMultipartFormData(h3); if (!formData) return undefined; const transactionalHandler = new ObjectTransactionalHandler(); const [add, pull, dump] = transactionalHandler.new(metadata, permissions); const options: { [key: string]: string } = {}; - let id; + const ids = []; for (const entry of formData) { if (entry.filename) { - // Only pick one file - if (id) continue; + if (max > 0 && ids.length >= max) continue; // Add file to transaction handler so we can void it later if we error out - id = add(entry.data); + ids.push(add(entry.data)); continue; } if (!entry.name) continue; @@ -30,5 +28,5 @@ export async function handleFileUpload( options[entry.name] = entry.data.toString("utf-8"); } - return [id, options, pull, dump]; + return [ids, options, pull, dump]; }