29 de octubre, 2021 - 7 min
En uno de mis últimos proyectos me ví con la necesidad de crear un script que te permita subir archivos estáticos a AWS S3. Por ciertas razones, el ambiente donde el script se ejecuta no permite instalar herramientas como aws-cli por lo que tuve que usar la librerìa @aws-sdk/client-s3 para lograr lo que necesitaba.
En este post veremos como usar @aws-sdk/client-s3
para subir carpetas y archivos a S3 así como también algunos puntos que debemos tomar en cuenta al usar esta librería.
Además del aws-sdk, también necesitaremos las librerías mime-types
y slash
. Más adelante explicaré para que sirve cada uno de ellos.
yarn add @aws-sdk/client-s3 mime-types slash
La forma en como funciona el sdk es bastante simple:
commando
PUT con el nombre del bucket, el archivo y su contenido.send
(ejecutar) el comandoconst 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 un `commando` upload específicando del bucket, el archivo y su contenido. const pathFile = './my-file.txt' const params = { Bucket: S3_BUCKET, // The name of the bucket. For example, 'sample_bucket_101'. Key: 'my-file.txt', // The name of the object. For example, 'my-file.txt'. Body: fs.readFileSync(pathFile), // The content of the object. For example, 'Hello world!". ContentType: 'application/text', // The content type of the file } const uploadCommand = new PutObjectCommand(params); // 3. Usas el cliente para `send` (o ejecutar) el comando client.send(uploadCommand) .then(() => { console.log('Success to upload the file to S3.') }) .catch(err => { console.log('Error to upload file', err); });
A pesar de que la librería es bastante robusta hay ciertos puntos que o no están documentados, o simplemente no son tan intuitivos. Esta es una pequeña lista de puntos a tener en cuenta al usar @aws-sdk/client-s3, quizás alguno de ellos te ayude a no tener que gastar horas y horas de research en internet:
A diferencia del aws cli, el @aws-sdk/client-s3 solo te permite subir un archivo a la vez. Para subir un conjunto de archivos o carpetas tendremos que escribir lógica extra con javascript (veremos un ejemplo en la siguiente sección).
Debes especificar el ContentType para cada archivo o de lo contrario @aws-sdk/client-s3
usará por defecto "application/octet-stream"
. Para esto puedes la librería mime-types
que cuenta con utilitarios para trabajar con content-types.
Puedes revisar el issue relacionado a esto aquí.
const mime = require('mime-types'); const uploadCommand = new PutObjectCommand({ ..., ContentType: mime.lookup(filePath), }); client.send(uploadCommand)
Si el Key
del archivo tiene la forma de una ruta, @aws-sdk/client-s3
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 de carpetas que se crearían en el bucket de S3:
my-bucket-s3/ └── static/ └── js/ └── app.js
Relacionado a lo anterior, si el Key
del archivo tiene un '/' al inicio @aws-sdk/client-s3
subirá el archivo a una carpeta con el nombre ´/´.
Tomando como ejemplo /app.js
const uploadCommand = new PutObjectCommand({ ..., Key: '/app.js', }); client.send(uploadCommand)
Esta sería la estructura se crearía en el bucket de S3:
my-bucket-s3/ └── / └── app.js
Si deseas usar una ruta como Key
del archivo recuerda convertir los Windows-backslash a slash. Para esto podemos usar la librería slash
.
const slash = require('slash'); const filePath = 'folder\\dist\\app.js' const uploadCommand = new PutObjectCommand({ ..., Key: slash(filePath), // from 'folder\\dist\\app.js' to 'folder/dist/app.js' }); client.send(uploadCommand)
A diferencia del aws-cli, el @aws-sdk/client-s3
solo te permite subir un único archivo. Debido a esto, necesitaremos crear algunos utilitarios para poder subir cada uno de los archivos a S3.
Específicamente, los utilitarios que necesitaremos serán:
walk
, para obtener cada uno de los archivos de una carpeta de forma recursiva.uploadFile
, para enviar cada uno de los archivos obtenidos por walk
a S3.Tengamos en cuenta que la carpeta de archivos que tenemos que subir a S3 es my-app/dist. Esta contiene todos los estáticos que nuestra app necesita para funcionar correctamente.
La carpeta dentro del bucket donde queremos que estén todos estos estáticos es static.
Al final lo que se quiere lograr es poder acceder a los archivos por la url url-publica-de-s3.com/static/
my-app/ └── dist/ <== upload this to S3 └── app.js └── manifest.json ├── chunks/ └── app.js └── chunk-1.js └── chunk-2.js └── chunk-3.js ├── images/ └── background.png └── src/
Walk
utilEsta función nos ayudará a recorrer de forma recursiva toda una carpeta. Con "recursiva"
me refiero a que el utilitario también recorrerá los archivos de las sub carpetas que puedan existir.
Como segundo parámetro este acepta un callback para ser ejecutado en 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); // if the current item is a ´directory´ run the walk function over this. if (fs.statSync(filePath).isDirectory()) { walk(filePath, callback); } else { // run the callback in each file found callback(filePath); } }); }
uploadFile
utilEste util nos permitira subir un archivo específico a S3.
Aquí usaremos las librerías 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 = '' }) { /** * remove folder path to get only the relative url based on the folderPath * from `/project/my-app/dist/chunks/app.js` to => `chunks/app.js` */ const filename = filePath.replace(folderPath, ''); /** * append the folder to upload * from 'chunks/chunk-1.js' to => 'static/js/chunks/chunk-1.js' */ const s3File = slash(path.join(bucketBasePath, filename)); /** * remove the '/' at the start of the string if it exists * some files are in the format '/image.png', this is a problem because it'll upload the file to a folder '/' */ const s3KeyFile = s3File[0] === '/' ? s3File.slice(1, s3File.length) : s3File; const params = { Bucket: s3Bucket, // The name of the bucket. For example, 'sample_bucket_101'. Key: s3KeyFile, // The name of the object. For example, 'sample_upload.txt'. Body: fs.readFileSync(filePath), // The content of the object. For example, 'Hello world!". ContentType: mime.lookup(filePath), }; console.log('Uploading file: ', s3KeyFile); const uploadCommand = new PutObjectCommand(params); return client.send(uploadCommand).catch(err => { console.log('Error to upload file', err); process.exit(1); }); }
Finalmente, utilizamos estos dos utils para crear la función que nos permitirá subir la carpeta de archivos a S3:
async function uploadFolderToS3(folderPath, bucketBasePath) { const filesArr = []; // get all files walk(folderPath, filePath => { filesArr.push(filePath); }); // upload each file to S3 filesArr.reduce((p, filePath) => { return p.then(_ => uploadFile({ filePath, folderPath, bucketBasePath })); }, Promise.resolve()); } const distFolderPath = path.join(process.cwd(), './dist'); // `static` is the folder in the bucket where we want to upload the files uploadFolderToS3(distFolderPath, 'static')
Como vimos, usar @aws-sdk/client-s3 no es tan complicado. Lo difícil es escribir toda la lógica necesaria para cumplir con el caso de uso que se nos presente, que en este caso era subida de una carpeta de archivos a S3.
¡Gracias por leer!