IAM
- Crear un grupo de usuarios en IAM
- Adjuntar la politica AmazonS3FullAccess
- Crear un usuario en IAM
- Agregar el usuario al grupo creado anteriormente
- Una vez creado el usuario seleccionarlo y ver su perfil. Seleccionar Credenciales de seguridad
- Seleccionar la opcion de Crear llaves de acceso
- En caso de uso seleccionar CLI
- Una vez generadas copiar ambas llaves de acceso o descargar archivo csv
Crear S3 Bucket
- Crear un bucket con un nombre unico, copiar el nombre del bucket y la region donde se creo
- Asegurarse que la opciones de ACLs desabilitado y Bloquear acceso publico esten seleccionadas
- Una vez creado seleccionar el bucket y dirigirse a la pestaña de permisos
- En la seccion de CORS pegar lo siguiente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"POST",
"GET"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"ETag"
]
}
]
- Crear una politica para el bucket. Puede utilizarse https://awspolicygen.s3.amazonaws.com/policygen.html para generarla. Se debera de colocar en Principal el ARN del Usuario, en Acciones seleccionar GetObject y PutObject y en el ARN el arn del bucket añadiendo /* al final para conder acceso completo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"Id": "Policy1716700011680",
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stm699991779",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": "arn:aws:s3:::nombre-bucket/*",
"Principal": {
"AWS": [
"arn:aws:iam::103650:user/nombre-de-usuario"
]
}
}
]
}
NEXT
- Crear proyecto de Next con
npx create-next-app@latest s3-app-dev
- Instalar las siguientes paquetes:
npm i @aws-sdk/client-s3
npm i @aws-sdk/s3-request-presigner
npm i axios
npx shadcn-ui@latest init
npx shadcn-ui@latest add input
npx shadcn-ui@latest add button
- ocumentacion del sdk para javascript https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/
- Crear archivo .env con las siguientes variables (colocar las varianles apropiadas)
1
2
3
4
AWS_ACCESS_KEY =
AWS_SECRET_KEY =
AWS_BUCKET_NAME =
AWS_BUCKET_REGION =
- Crear
app/api/s3/route.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const { S3Client } = require("@aws-sdk/client-s3");
import { NextRequest, NextResponse } from "next/server";
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// crea un cliente de S3 con las credenciales especificadas en las variables de entorno de AWS y el nombre de la region del bucket de S3
const client = new S3Client({
region: process.env.AWS_BUCKET_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY,
},
});
// nombre del bucket de S3
const bucketName = process.env.AWS_BUCKET_NAME;
// POST /api/s3
// API para subir archivos a S3
// Se espera que el archivo sea enviado como un form-data con el nombre "image". Retorna un JSON con la url del archivo subido o un mensaje de error
export async function POST(req: NextRequest) {
const formData = await req.formData();
const image = formData.get("image");
const random = new Date().getTime();
// verifica que el archivo sea valido y que sea un objeto con el nombre del archivo y que sea un archivo valido
if (image && typeof image === "object" && image.name) {
const Body = (await image.arrayBuffer()) as Buffer;
const params = {
Bucket: bucketName,
//Key: image.name,
Key: `${random}-${image.name}`,
Body,
ContentType: image.type,
};
// sube el archivo a S3 con los parametros especificados en la variable "params" y espera a que se complete la subida del archivo
const command = new PutObjectCommand(params);
await client.send(command);
const getObjectParams = {
Bucket: bucketName,
Key: `${random}-${image.name}`,
ACL: "private"
};
// obtiene la url firmada del archivo subido para poder ser accedido publicamente por un tiempo limitado
const getCommand = new GetObjectCommand(getObjectParams);
const url = await getSignedUrl(client, getCommand, { expiresIn: 3600 });
// retorna un mensaje de exito en formato JSON con la url del archivo subido y la url firmada del archivo subido para ser accedido publicamente
return NextResponse.json({
success: true,
message: "La imagen se subio correctamente!",
data: {
url
},
});
// si no se pudo subir la imagen retorna un mensaje de error en formato JSON
return NextResponse.json({
success: false,
message: "No se pudo subir la imagen",
data: null,
});
}
}
- Modificar
app/page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
"use client"
import { ChangeEvent, FormEvent, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import axios from "axios";
import Image from "next/image";
// Home page
// Esta pagina permite subir una imagen a un servidor de S3 y muestra la imagen subida
export default function Home() {
// estado para guardar la url de la imagen subida
const [image, setImage] = useState<string>();
// estado para guardar la imagen seleccionada
const [selectedImage, setSelectedImage] = useState<File>();
// funcion para subir una imagen a un servidor de S3
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
// previene el comportamiento por defecto del formulario
event.preventDefault();
// verifica que la imagen seleccionada exista y que sea un objeto valido de tipo File
try {
if (selectedImage) {
const formData = new FormData();
formData.append("image", selectedImage)
const headers = {
"Content-Type": "multipart/form-data"
}
// envia la imagen seleccionada al servidor de S3 y espera a que se suba la imagen
const { data } = await axios.post("/api/s3", formData, { headers })
if (data.success)
setImage(data.data.url)
}
} catch (error) {
console.error(error)
}
}
// funcion para manejar el cambio de la imagen seleccionada
const handleFileChange = (ev: ChangeEvent<HTMLInputElement>) => {
const file = ev.target.files && ev.target.files[0];
if (file)
setSelectedImage(file)
}
// retorna el componente principal de la pagina Home con un formulario para subir una imagen y mostrar la imagen subida
return (
<main className="flex w-screen h-screen flex-col gap-3 items-center justify-center">
<h1 className="text-3xl font-bold">Subir imagen a S3</h1>
{
image && <Image alt={image} src={image} height={400} width={400}
className="rounded-md" />
}
<form onSubmit={onSubmit} className="flex flex-col gap-3 w-50">
<label htmlFor="image" className="font-medium">Selecciona tu imagen</label >
<Input id="image" type="file" onChange={handleFileChange} />
<Button type="submit" className="w-100">Submit</Button>
</form>
</main>
);
}
- Modificar
app/layout.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark">
<body className={inter.className}>{children}</body>
</html>
);
}
- Modificar
next.config.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** @type {import('next').NextConfig} */
const nextConfig = {
// permite que o Next.js optimize as imagens automaticamente
images: {
// habilita o uso de formatos de imagem modernos (AVIF e WebP)
formats: ["image/avif", "image/webp"],
// habilita o uso de imagens responsivas
remotePatterns: [
{
// Permite a NextJS optimizar imagens de un bucket S3
protocol: "https",
hostname: "nombre-bucket.s3.us-west-1.amazonaws.com",
port: "",
pathname: "/**",
},
],
},
};
export default nextConfig;