¿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).
- javascript
- node.js
- aws
- aws-s3
- tooling
Por qué el SDK en vez del CLI
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 tiene tres pasos:
- 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);
});Detalles que la documentación no menciona
Aunque la librería es 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');Dónde está el trabajo real
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!