Examples of how to upload, download and delete files from S3 bucket using multer and the AWS SDK for JavaScript v3 Node.js.
Note: I used Prisma with MySQL for the database and tailwind for the styling. These are irrelevant details for this example. Any database or styling would work. The important part is how the Node.js backend communicates with S3.
This repo contains 3 different examples:
Check out those directories for specific examples. Here's the gist of what's going on though:
- Images get POSTed to the server where they can be modified or validated or whatever, then uploaded from the server to an S3 bucket.
- To get the image from S3, the server generates a signed url so the client can GET the image from S3 directly and securely.
- To delete the image from S3, the server sends a delete request to S3.
To post an image to the server, the client sends a multipart/form-data
request to the server with the image data and any other data that the client wants to send.
HTML:
<form action="/posts" method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*"/>
<input type="text" name="caption" placeholder="Caption"/>
<button type="submit">Submit</button>
</form>
React/Next:
export default function NewPost() {
const [file, setFile] = useState()
const [caption, setCaption] = useState("")
const submit = async event => {
event.preventDefault()
const formData = new FormData();
formData.append("image", file)
formData.append("caption", caption)
await axios.post("/api/posts", formData, { headers: {'Content-Type': 'multipart/form-data'}})
}
return (
<form onSubmit={submit}>
<input onChange={e => setFile(e.target.files[0])} type="file" accept="image/*"></input>
<input value={caption} onChange={e => setCaption(e.target.value)} type="text" placeholder='Caption'></input>
<button type="submit">Submit</button>
</form>
)
}
The server then accepts the image data using multer and keeps the image in memory so it can be easily modified and sent to S3. The app's in this example modify the image by resizing it using sharp. At all times the image is stored in memory as a buffer.
import multer from 'multer'
import sharp from 'sharp'
const storage = multer.memoryStorage()
const upload = multer({ storage: storage })
app.post('/posts', upload.single('image'), async (req, res) => {
const file = req.file
const caption = req.body.caption
const fileBuffer = await sharp(file.buffer)
.resize({ height: 1920, width: 1080, fit: "contain" })
.toBuffer()
// ...
})
Next we need to send the image to S3. First S3 needs to be configured with the correct credentials.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"
import dotenv from 'dotenv'
dotenv.config()
const bucketName = process.env.AWS_BUCKET_NAME
const region = process.env.AWS_BUCKET_REGION
const accessKeyId = process.env.AWS_ACCESS_KEY
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY
const s3Client = new S3Client({
region,
credentials: {
accessKeyId,
secretAccessKey
}
})
Then the image buffer data can be sent to S3 using the PutObjectCommand
. Since the images in S3 all need to have unique names, we can use the crypto library to generate a unique unguessable name.
import crypto from 'crypto'
const generateFileName = (bytes = 32) => crypto.randomBytes(bytes).toString('hex')
app.post('/posts', upload.single('image'), async (req, res) => {
const file = req.file
const caption = req.body.caption
const fileBuffer = await sharp(file.buffer)
.resize({ height: 1920, width: 1080, fit: "contain" })
.toBuffer()
// Configure the upload details to send to S3
const fileName = generateFileName()
const uploadParams = {
Bucket: bucketName,
Body: fileBuffer,
Key: fileName,
ContentType: file.mimetype
}
// Send the upload to S3
await s3Client.send(new PutObjectCommand(uploadParams));
// Save the image name to the database. Any other req.body data can be saved here too but we don't need any other image data.
const post = await prisma.posts.create({
data: {
imageName,
caption,
}
})
res.send(post)
})
Once the images are being successfully uploaded to S3, we need to generate a signed URL so the client can GET the image from S3. The database only stores the image name, so we generate a signed URL using the image name.
import { GetObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
app.get("/", async (req, res) => {
const posts = await prisma.posts.findMany({ orderBy: [{ created: 'desc' }] }) // Get all posts from the database
for (let post of posts) { // For each post, generate a signed URL and save it to the post object
post.imageUrl = await getSignedUrl(
s3Client,
GetObjectCommand({
Bucket: bucketName,
Key: imageName
}),
{ expiresIn: 60 }// 60 seconds
)
}
res.send(posts)
})
https://aws.amazon.com/blogs/developer/generate-presigned-url-modular-aws-sdk-javascript/
Then any client can GET the image from S3 using the signed URL in the src
of an img
tag.
If you want to delete the image from S3, you can use the DeleteObjectCommand
passing in the image name, then delete the corresponding post from the database.
app.delete("/api/posts/:id", async (req, res) => {
const id = +req.params.id
const post = await prisma.posts.findUnique({where: {id}})
const deleteParams = {
Bucket: bucketName,
Key: post.imageName,
}
return s3Client.send(new DeleteObjectCommand(deleteParams))
await prisma.posts.delete({where: {id}})
res.send(post)
})