TL;DR: Using Axios with my NextJS 15 application to upload a file and have it display the upload progress. Issue is has a possible solution.
I was looking into using Axios with my NextJS 15 application to upload a file and have it display the upload progress. This would also include multi-file uploading and I would also love to figure out uploading large file. What my issue is now, figuring out if I could call a service action function within axios and call the await uploadFileToS3(file, S3Payload)
function within my upload page 🤔?!
Such as:
// app/actions/s3File.ts
"use server"
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
import { revalidatePath } from "next/cache"
type S3Payload = {
uploadDestination: string
bucket: string
hostName: string
uploadEndpoint: string
}
export async function uploadFileToS3( formData: FormData, payload: S3Payload ): Promise<any[]> {
const s3 = new S3Client({
...
})
const { uploadDestination, bucket, hostName, uploadEndpoint } = payload
try {
const files = formData.getAll("file") as File[]
const response = await Promise.all(
files.map(async (file) => {
const {name: fileName, type: fileType, lastModified: date, size: fileSize} = file
const fileExtension = fileType.split("/")\[1\]
const fileID = `${uploadDestination}\_CUSTOMRANDOMID` as string
const filePath = `${uploadDestination}/${fileID}.${fileExtension}`
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const uploadFileObject = {
Bucket: bucket,
Key: filePath,
Body: buffer,
ContentType: fileType,
ContentLength: fileSize,
}
const uploadFile = new PutObjectCommand(uploadFileObject)
const upload = await s3.send(uploadFile)
})
)
revalidatePath("/")
return response
} catch (error) {
...
}
}
and my upload file is currently looking like this:
// uploadPage/component.tsx
...
import { Dropzone, DropzoneProps, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { uploadFileToS3 } from "@/app/actions/s3File";
...
async function handleOnSubmit(e: any) {
setUploading(true)
const files = e
const uploadDestination = mediaType
try {
const formData = new FormData()
files.forEach((file) => formData.append("file", file))
// This is where I think the Axios would come in effect for upload progress?!
await uploadFileToS3(formData, {
uploadDestination, // Picture | Video | Audio | ECT
bucket: process.env.NEXT_PUBLIC_S3_BUCKET_NAME!,
hostName: process.env.NEXT_PUBLIC_S3_HOST_NAME!,
uploadEndpoint: `https://${process.env.NEXT\_PUBLIC\_S3\_BUCKET\_NAME!}.${process.env.NEXT\_PUBLIC\_S3\_HOST\_NAME!}`
})
} catch (error) {
console.error("File(s) couldn't be uploaded to S3", error)
}
}
return <>
...
<Dropzone
onDrop={(files) => handleOnSubmit(files)}
onReject={(files) => console.log('rejected files', files)}
loading={isUploading}
bg="none"
radius="md"
c="white"
{...props}
>
<Group justify="center" gap="2rem" style={{ pointerEvents: 'none' }} py="4rem">
<Dropzone.Accept>
<FileUploadIcon variant="twotone" />
</Dropzone.Accept>
<Dropzone.Reject>
<Cancel01Icon variant="twotone" />
</Dropzone.Reject>
<Dropzone.Idle>
<CloudUploadIcon variant="twotone" size="5rem" />
</Dropzone.Idle>
<Stack gap="0" m="0" p="0">
<Title c="white" lh="1" fw="900" ff="text" ta={{base: "center", lg: "left"}}>
{uploadTitle ? uploadTitle : "Upload Media"}
</Title>
<Text size="sm" c="grey" lh="1" ta={{base: "center", lg: "left"}}>
{helperText ? helperText : "Drag and Drop or Click to Upload File(s)."}
</Text>
</Stack>
</Group>
</Dropzone>
<Progress radius="0 0 0 1rem" size="xl" value={uploadProgress} color="primary" mt="0.5rem" animated />
...
</>
I have also tried to add this to the async function handleOnSubmit(e: any)
function
files.forEach((file: string | Blob) => {
formData.append("file", file)
axios
.post(
uploadFileToS3(
formData,
s3Payload
),
{
headers: {
'x-ms-blob-type': 'BlockBlob',
},
maxContentLength: 2e10,
maxBodyLength: 2e10,
onUploadProgress: (event: any) => {
console.log("Hello from event", event)
setUploadProgress(Math.round((event.loaded / event.total) * 100))
}
}
).then((response) => {
console.log(response)
})
.catch((error) => {
if (error.response) {
console.log(error.response)
console.log("server responded")
} else if (error.request) {
console.log("network error")
} else {
console.log(error)
}
})
})
This will upload the content through the Server Action but won't run the rest of the operation such as onUploadProgress
or the completed .then
. It returns the response error
of POST https://DEVSERVER/PAGE/[object%20Promise] 404 (Not Found)
.
I have figured a possible solution? However, I'm unsure if it makes sense to do so or just seems redundant?
But I have created an API route to effectively collect the data; then send it to a controller to separate the formData
and create different variables for the files and the payload; then I pass that controller data down to the server action.
This does seem to be working.
The API route looks something like this:
// api/upload/route.ts
import { handleFileUpload } from "@/app/controllers/upload.controller";
export async function POST(req: any) {
return handleFileUpload(req)
}
The controller looks something like this:
// controllers/upload.controller.ts
import { NextResponse } from "next/server"
import { uploadFileToS3 } from "@/app/actions/s3File.ts";
export const handleFileUpload = async (req: any) => {
try {
const form = await req.formData()
const files = form.getAll("files") as any
const payloadRAW = form.get("payload")
const {
uploadDestination,
mediaID,
bucket,
uploadEndpoint,
pathname,
redirectPath
} = JSON.parse(payloadRAW)
const payload = {
uploadDestination,
mediaID,
bucket,
uploadEndpoint,
pathname,
redirectPath
} as any
const uploadURL = await uploadFileToS3(files, payload);
return NextResponse.json({ message: "success", uploadURL });
} catch (error) {
console.error("Error uploading file:", error);
return NextResponse.json({ message: "failure", reason: error.message });
}
}
I then reconfigured my server action to take in the Files: File[]
instead of the formData: FormData
, along with some other general changes.