¿Cómo subir archivos a S3 usando @aws-sdk/client-s3?
Cómo usar @aws-sdk/client-s3 para subir archivos y carpetas enteras a AWS S3 desde Node.js, con los gotchas más comunes (content type, claves con barras, paths de Windows).
Introducción
En uno de mis últimos proyectos me vi con la necesidad de crear un script que subiera archivos estáticos a AWS S3. Por motivos del entorno donde el script corría, no podía instalar herramientas como aws-cli, así que terminé usando la librería @aws-sdk/client-s3 para resolverlo.
En este post voy a mostrar cómo usar @aws-sdk/client-s3 para subir archivos y carpetas a S3, junto con algunos puntos a tener en cuenta cuando se trabaja con esta librería.
Instalación
Además del SDK, vamos a necesitar mime-types y slash. Más adelante explico para qué sirve cada uno:
yarn add @aws-sdk/client-s3 mime-types slash¿Cómo se usa @aws-sdk/client-s3?
El flujo del SDK es bastante directo:
- Inicializas el cliente con tus credenciales de AWS.
- Creas un comando
PutObjectcon el bucket, el archivo y su contenido. - Le pides al cliente que ejecute (
send) el comando.
const fs = require('fs');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
// 1. Inicializas el cliente con tus credenciales de AWS.
const { AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET } =
process.env;
const client = new S3Client({
region: AWS_REGION,
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
});
// 2. Creas el comando con el bucket, el archivo y su contenido.
const pathFile = './my-file.txt';
const params = {
Bucket: S3_BUCKET, // nombre del bucket. Por ejemplo, 'sample_bucket_101'.
Key: 'my-file.txt', // nombre del objeto. Por ejemplo, 'my-file.txt'.
Body: fs.readFileSync(pathFile), // contenido del objeto.
ContentType: 'application/text', // content type del archivo.
};
const uploadCommand = new PutObjectCommand(params);
// 3. Le pides al cliente que ejecute el comando.
client
.send(uploadCommand)
.then(() => {
console.log('Success uploading the file to S3.');
})
.catch(err => {
console.log('Error uploading file', err);
});Cosas a tener en cuenta
Aunque la librería es bastante robusta, hay detalles que o no están documentados, o no son tan intuitivos. Esta es una lista corta de cosas que vale la pena saber al usar @aws-sdk/client-s3; con suerte, te ahorra horas de búsqueda en internet:
-
A diferencia del CLI de AWS,
@aws-sdk/client-s3solo deja subir un archivo a la vez. Para subir un conjunto de archivos o una carpeta entera hay que escribir lógica extra en JavaScript (lo veremos en la siguiente sección). -
Hay que especificar
ContentTypepor archivo o@aws-sdk/client-s3usará por defecto"application/octet-stream". Para esto se puede usarmime-types, que tiene utilitarios para resolver content-types desde la extensión del archivo. Puedes leer el issue relacionado acá.const mime = require('mime-types'); const uploadCommand = new PutObjectCommand({ // ... ContentType: mime.lookup(filePath), }); client.send(uploadCommand); -
Si el
Keydel archivo tiene la forma de una ruta,@aws-sdk/client-s3va a crear esas mismas carpetas en el bucket de S3.Tomando como ejemplo
static/js/app.js:const uploadCommand = new PutObjectCommand({ // ... Key: 'static/js/app.js', }); client.send(uploadCommand);Esta sería la estructura resultante en el bucket:
my-bucket-s3/ └── static/ └── js/ └── app.js -
Relacionado al punto anterior: si el
Keyempieza con/,@aws-sdk/client-s3va a crear una carpeta llamada/. Tomando/app.jscomo ejemplo:const uploadCommand = new PutObjectCommand({ // ... Key: '/app.js', }); client.send(uploadCommand);Resultado en el bucket:
my-bucket-s3/ └── / └── app.js -
Si vas a usar una ruta como
Key, recuerda convertir las barras invertidas de Windows a forward-slashes. Para esto podemos usarslash:const slash = require('slash'); const filePath = 'folder\\dist\\app.js'; const uploadCommand = new PutObjectCommand({ // ... Key: slash(filePath), // 'folder\\dist\\app.js' → 'folder/dist/app.js' }); client.send(uploadCommand);
Subir una carpeta de archivos a S3
A diferencia de aws-cli, @aws-sdk/client-s3 solo permite subir un archivo por vez. Por eso necesitamos algunos utilitarios:
walk, para recorrer una carpeta de forma recursiva.uploadFile, para subir cada archivo encontrado a S3.
Asumamos que la carpeta a subir es my-app/dist, que contiene los estáticos que la app necesita para funcionar. Y que dentro del bucket queremos que esos estáticos vivan bajo la carpeta static.
Al final, lo que queremos lograr es poder acceder a los archivos vía la URL url-publica-de-s3.com/static/:
my-app/
└── dist/ <== subir esto a S3
├── app.js
├── manifest.json
├── chunks/
│ ├── app.js
│ ├── chunk-1.js
│ ├── chunk-2.js
│ └── chunk-3.js
└── images/
└── background.png
└── src/Util: walk
Esta función recorre toda una carpeta de forma recursiva — me refiero a que también entra en las subcarpetas. Como segundo parámetro recibe un callback que se ejecuta para cada archivo encontrado:
const path = require('path');
const fs = require('fs');
function walk(dir, callback) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
// si es un directorio, recursamos
if (fs.statSync(filePath).isDirectory()) {
walk(filePath, callback);
} else {
// ejecutamos el callback en cada archivo
callback(filePath);
}
});
}Util: uploadFile
Sube un archivo específico a S3. Acá usamos mime-types y slash:
const fs = require('fs');
const path = require('path');
const mime = require('mime-types');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const slash = require('slash');
const { AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET } =
process.env;
const client = new S3Client({
region: AWS_REGION,
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
});
function uploadFile({ filePath, folderPath, bucketBasePath = '' }) {
/**
* Quitamos el folderPath para quedarnos con la ruta relativa
* de '/project/my-app/dist/chunks/app.js' a 'chunks/app.js'
*/
const filename = filePath.replace(folderPath, '');
/**
* Anteponemos la carpeta destino
* de 'chunks/chunk-1.js' a 'static/js/chunks/chunk-1.js'
*/
const s3File = slash(path.join(bucketBasePath, filename));
/**
* Removemos el '/' inicial si existe
* algunos archivos vienen como '/image.png' y eso crearía una carpeta llamada '/'
*/
const s3KeyFile = s3File[0] === '/' ? s3File.slice(1) : s3File;
const params = {
Bucket: S3_BUCKET,
Key: s3KeyFile,
Body: fs.readFileSync(filePath),
ContentType: mime.lookup(filePath),
};
console.log('Uploading file: ', s3KeyFile);
const uploadCommand = new PutObjectCommand(params);
return client.send(uploadCommand).catch(err => {
console.log('Error uploading file', err);
process.exit(1);
});
}Finalmente, combinamos los dos utils para subir la carpeta entera:
async function uploadFolderToS3(folderPath, bucketBasePath) {
const filesArr = [];
// recolectamos todos los archivos
walk(folderPath, filePath => {
filesArr.push(filePath);
});
// los subimos uno por uno, en serie
await filesArr.reduce((p, filePath) => {
return p.then(() => uploadFile({ filePath, folderPath, bucketBasePath }));
}, Promise.resolve());
}
const distFolderPath = path.join(process.cwd(), './dist');
// 'static' es la carpeta destino dentro del bucket
uploadFolderToS3(distFolderPath, 'static');Conclusión
Como vimos, usar @aws-sdk/client-s3 no es tan complicado. Lo difícil es escribir la lógica alrededor para cubrir el caso de uso real — en este caso, subir una carpeta entera de estáticos a S3.
¡Gracias por leer!