Namjot Singh
Namjot Singh

Reputation: 11

Upload the images to AWS S3 bucket and Get them in Next.js with node.js

I am working on creating a admin dashboard where you can do CRUD operations on the products. When I am uploading the image, it gets uploaded to the AWS bucket, but it is not showing on the front end, It shows forbidden access, but i have given all permissions to the user. On second upload of image, the first one is visible, and this is my main problem. On saving the product details, all images are adding to the Mongo database.

Here is the code

ProductForm.js

"use client"
// import axios from 'axios';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'

const ProductForm = ({
    _id,
    title: existingTitle,
    description: existingDescription,
    price: existingPrice,
    images: existingImages
}) => {
    const router = useRouter();
    // console.log(existingTitle, existingDescription, existingPrice, _id);

    const [title, setTitle] = useState(existingTitle || "");
    const [description, setDescription] = useState(existingDescription || "");
    const [price, setPrice] = useState(existingPrice || "");
    const [images, setImages] = useState(existingImages || []);

    // To load the existing product details on opening the page / update state for form fields when props change
    useEffect(() => {
        setTitle(existingTitle || "");
        setDescription(existingDescription || "");
        setPrice(existingPrice || "");
        setImages(existingImages || []);
    }, [existingTitle, existingDescription, existingPrice, existingImages])

    // Form submission
    const saveProduct = async (e) => {
        e.preventDefault();

        const productData = { title, description, price, _id, images };
        console.log("productData", productData);

        // Watch the video to send data using AXIOS

        // UPDATE the product if ID exits
        if (_id) {
            await fetch("/api/products", {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(productData),
            })
        }
        // POST (CREATE) the data if ID does not exist
        else {
            await fetch("/api/products", {
                method: 'POST',
                headers: {
                    'Content-Type': 'multipart',
                },
                body: JSON.stringify(productData),
            })
        }
        // setgotoProducts(true); // might not need
        router.push('/products'); // redirect to the products page
    }

    const [start, setstart] = useState(true)
    useEffect(() => {
        console.log("Useffect: ", images);
    }, [start])

    useEffect(() => {
        console.log('Useffect Updated images:', images);
    }, [images]);

    const uploadImages = async (e) => {
        const files = e.target?.files; // e.target?.files
        // console.log(files);

        if (!files || files.length == 0) {
            console.error('No files selected');
            return;
        }
        const data = new FormData();

        // files are stored in a FileList (array like) object, convert it into an Array to use forEach
        for (const file of files) {
            data.append('files', file)
            // console.log(file);
        }
        // or 
        // Array.from(files).forEach(file => data.append('files', file)) // Add each file to FormData

        // Print Formdata object
        for (let pair of data.entries()) { // let [key,value] of data.entries()
            // console.log(pair);
        }

        const res = await fetch('/api/upload', {
            method: 'POST', // headers are automatically added using FormData
            body: data,
        })
            .then(res2 => res2.json())
            .then(linksData => {
                setImages(prevImages => [...prevImages, ...linksData.links]);
                // console.log('Uploaded images:', ...linksData.links);
            })
        console.log('After upload:', images); // not getting updated

        // const res = await fetch('/api/upload', {
        //     method: 'POST',
        //     body: data,
        // });

        // const linksData = await res.json();
        // const updatedImages = [...images, ...linksData.links]; // Create a new array
        // setImages(updatedImages); // Update state
        // console.log('Updated images:', updatedImages); // Use the local variable to debug
    }

    return (
        <form onSubmit={e => { saveProduct(e) }} className='max-w-[600px] ml-10'>
            <div className='flex flex-col gap-1'>
                <label>Name</label>
                <input
                    type="text"
                    value={title}
                    onChange={e => setTitle(e.target.value)} />
            </div>
            <div className='flex flex-col gap-1'>
                <label>Description</label>
                <textarea name="description" id="" value={description} onChange={e => setDescription(e.target.value)} rows={5} />
            </div>
            <div className='flex flex-col gap-1'>
                <label>Price (in CAD)</label>
                <input name='price' type="text" value={price} onChange={e => setPrice(e.target.value)} />
            </div>
            <div className='flex flex-col gap-1'>
                <label>Photos</label>

                <div className="flex flex-wrap gap-3">
                    {!!images.length && images.map((link) => (
                        <div key={link} className="w-28 h-28">
                            <img className="rounded-lg w-full h-full object-fill" src={`${link}?t=${Date.now()}`} alt="productImage" />
                        </div>
                    ))}

                    {/*  */}
                    <label className='cursor-pointer w-28 h-24 flex gap-1 items-center rounded-lg bg-gray-200 text-gray-500'>
                        <img src="/upload.svg" alt="" />Upload
                        <input type="file" onChange={e => uploadImages(e)} className='hidden' name="" id="" multiple />
                    </label>
                </div>
            </div>
            <button type='submit' className='btn-primary mt-5'>Save</button>

        </form>
    )
}

export default ProductForm

api/upload/route.js

import { NextResponse } from 'next/server';
// import { writeFile } from 'fs/promises'; // to save to localStorage
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import fs from 'fs';

const bucketName = "namjot-next-ecommerce";

export async function POST(req) {
    // Get the files from the request
    const data = await req.formData()
    const files = data.getAll('files')
    // console.log('data', data);
    // console.log(files);

    if (!files) return NextResponse.json({ success: false });

    // const bytes = await files.arrayBuffer();
    // const buffer = Buffer.from(bytes)
    // const filepath = `${files.name}` //public/uploads/
    // await writeFile(filepath, buffer)
    // console.log(filepath);

    // AWS S3 Bucket
    const client = new S3Client({
        region: 'us-east-1',
        credentials: {
            accessKeyId: process.env.S3_ACCESS_KEY,
            secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
        },
    });
    const links = [];

    // Upload each file to AWS
    for (const file of files) {
        // Unique file name to avoid duplicate images in Bucket
        const ext = file.name.split('.').pop() // pop() removes the last element from an array and returns that element
        const newFilename = Date.now() + "." + ext;
        // console.log({ ext, file });
        // console.log(newFilename);

        // Convert file to Buffer
        const bytes = await file.arrayBuffer();
        const buffer = Buffer.from(bytes)

        // Upload to S3 bucket
        client.send(new PutObjectCommand({
            Bucket: bucketName,
            Key: newFilename,
            Body: buffer,
            ACL: 'public-read',
            ContentType: file.type,
        }))
        const link = `https://${bucketName}.s3.us-east-1.amazonaws.com/${newFilename}`;
        links.push(link);
    }
    return NextResponse.json({ links });
}

// disable the default bodyparser that parses the FormData
export const config = {
    api: {
        bodyParser: false,
    },
};

Upvotes: 1

Views: 74

Answers (1)

Rizwan Ali
Rizwan Ali

Reputation: 31

I think you need to apply the Read & Write policies on your AWS s3 bucket. https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html

Can you please try with page reload to see if your latest image is still showing or not? Sometime browser need time to load it.

But it's totally related to Read of objects policies. May be you have missed something while applying the policy rules on AWS S3.

Upvotes: 0

Related Questions