From 384d5719972fd08f841753e3fb136c76459b7452 Mon Sep 17 00:00:00 2001 From: shb Date: Wed, 18 Jun 2025 12:00:36 +0800 Subject: [PATCH] Merged AWS upload functionality Uploading now works with AWS. Documentation can be found in dms-api.md at the root folder. --- .../dms/dialogs/UploadWithMetadataModal.vue | 42 ++++--- dms-api.md | 43 ++++++- server/api/dms/upload-file.post.js | 112 ++++++++---------- 3 files changed, 114 insertions(+), 83 deletions(-) diff --git a/components/dms/dialogs/UploadWithMetadataModal.vue b/components/dms/dialogs/UploadWithMetadataModal.vue index 892781c..6f8ffe3 100644 --- a/components/dms/dialogs/UploadWithMetadataModal.vue +++ b/components/dms/dialogs/UploadWithMetadataModal.vue @@ -322,29 +322,37 @@ const performUpload = async () => { const file = selectedFiles.value[i]; const metadata = fileMetadata.value[i]; - // Simulate upload progress - for (let progress = 0; progress <= 100; progress += 10) { - uploadProgress.value = Math.round(((i * 100) + progress) / selectedFiles.value.length); - await new Promise(resolve => setTimeout(resolve, 100)); - } - - console.log(file); - - // Here you would implement actual file upload - + // First get the signed URL const formData = new FormData(); formData.append('fileName', metadata.name); - formData.append('file', file); + formData.append('fileType', file.type); - console.log(formData); - - const response = await fetch('/api/dms/upload-file', { + const signedUrlResponse = await fetch('/api/dms/upload-file', { method: "POST", - // Let browser automatically set headers for multipart/form-data body: formData, }); - console.log(response); - // console.log('Uploading:', file.name, 'with metadata:', metadata); + + const signedUrlData = await signedUrlResponse.json(); + + if (!signedUrlResponse.ok || !signedUrlData.signedUrl) { + throw new Error(signedUrlData.message || 'Failed to get signed URL'); + } + + // Upload directly to S3 using the signed URL + const uploadResponse = await fetch(signedUrlData.signedUrl, { + method: 'PUT', + body: file, + headers: { + 'Content-Type': file.type + } + }); + + if (!uploadResponse.ok) { + throw new Error(`Failed to upload file ${metadata.name} to S3`); + } + + // Update progress + uploadProgress.value = Math.round(((i + 1) * 100) / selectedFiles.value.length); } success(`Successfully uploaded ${selectedFiles.value.length} file(s)`); diff --git a/dms-api.md b/dms-api.md index 634f132..d0ad944 100644 --- a/dms-api.md +++ b/dms-api.md @@ -115,4 +115,45 @@ "message": "DMS settings updated successfully", "data": { "settingID": 1 } } - ``` \ No newline at end of file + ``` + +## Files + +### POST /api/dms/upload-file +- **Description**: Generates a signed URL for direct file upload to S3 +- **Method**: POST +- **Content-Type**: multipart/form-data +- **Request Body**: + ```json + { + "fileName": "example.pdf", + "fileType": "application/pdf" + } + ``` +- **Required Fields**: fileName, fileType +- **Response Example**: + ```json + { + "status": 200, + "message": "Signed URL generated for file: example.pdf", + "signedUrl": "https://bucket-name.s3.region.amazonaws.com/..." + } + ``` +- **Error Responses**: + ```json + { + "status": 400, + "message": "Missing required fields { fileName, fileType }" + } + ``` + ```json + { + "status": 500, + "message": "Failed to generate signed URL", + "error": "Error details..." + } + ``` +- **Notes**: + - The signed URL expires after 60 seconds + - Use the returned signedUrl with a PUT request to upload the file directly to S3 + - Include the file's Content-Type header when uploading to S3 \ No newline at end of file diff --git a/server/api/dms/upload-file.post.js b/server/api/dms/upload-file.post.js index 6b16607..6e9879d 100644 --- a/server/api/dms/upload-file.post.js +++ b/server/api/dms/upload-file.post.js @@ -1,21 +1,19 @@ -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; // ES Modules import +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { readMultipartFormData } from 'h3'; export default defineEventHandler(async (event) => { - - const parts = []; - - // Allow headers for specific origins - // setHeader(event, 'Access-Control-Allow-Origin', 'http://localhost:3000'); - // setHeader(event, 'Access-Control-Allow-Methods', 'POST, OPTIONS'); - // setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type'); - - if (event.method === 'OPTIONS') { - return new Response(null, { status: 204 }); - } + // Create S3 client with config + const client = new S3Client({ + region: process.env.NUXT_AWS_REGION, + credentials: { + accessKeyId: process.env.NUXT_AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.NUXT_AWS_SECRET_ACCESS_KEY + } + }); try { - const parts = await readMultipartFormData(event, { maxSize: 20 * 1024 * 1024}); + const parts = await readMultipartFormData(event, { maxSize: 1024 * 1024 }); // 1MB limit since we're only handling metadata if (!parts) { return { @@ -23,61 +21,45 @@ export default defineEventHandler(async (event) => { message: "Bad request. No parts found." } } + + // Extract form data + const fileNamePart = parts.find(p => p.name === "fileName"); + const fileTypePart = parts.find(p => p.name === "fileType"); + + if (!fileNamePart || !fileTypePart) { + return { + status: 400, + message: "Missing required fields { fileName, fileType }" + } + } + + const fileName = fileNamePart.data.toString(); + const fileType = fileTypePart.data.toString(); + + // Generate a unique key using timestamp and filename + const uploadKey = `uploads/${Date.now()}-${fileName}`; + + const command = new PutObjectCommand({ + Bucket: process.env.NUXT_AWS_BUCKET, + Key: uploadKey, + ContentType: fileType, + }); + + // Generate signed URL + const signedUrl = await getSignedUrl(client, command, { expiresIn: 60 }); + + return { + status: 200, + message: `Signed URL generated for file: ${fileName}`, + signedUrl + }; + } catch (error) { - console.error('Failed to read multi-part data:', error); + console.error('Error processing request:', error); return { status: 500, - message: "Failed to read multi-part data" - } - } - - console.log("Hello from the backend!"); - - const S3_Config = { - region: process.env.NUXT_AWS_REGION, - credentials: { - accessKeyId: process.env.NUXT_AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.NUXT_AWS_SECRET_ACCESS_KEY - } - } - - // Create S3 client with config - const client = new S3Client(S3_Config); - - // const { fileName, file } = await readMultipartFormData(event); - - const fileNamePart = parts.find(p => p.name === "fileName"); - const filePart = parts.find(p => p.name === "file"); - - if (!fileNamePart || !filePart) { - return { - status: 400, - message: "Missing required fields { fileName, file }" - } - } - - const fileName = fileNamePart.data.toString(); - const file = filePart.data; - - const input = { - Bucket: process.env.NUXT_AWS_BUCKET, - Key: fileName, - Body: file, - ContentType: filePart.type - } - - const command = new PutObjectCommand(input); - - try { - const response = await client.send(command); - console.log(response); - return response; - } catch (error) { - console.error(error); - return { - status: 500, - message: "Failed to upload file to S3", - error: error + message: "Failed to generate signed URL", + error: error.message } } }) \ No newline at end of file