Cómo cargar archivos en Amazon S3 mediante la API del lector de archivos

Cómo usar la API de FileReader en el navegador para leer un archivo en la memoria como una cadena base64 y cargarlo en Amazon S3 usando el aws-sdk biblioteca de NPM.

Primeros pasos

Para este tutorial, vamos a necesitar un back-end y un front-end. Nuestro back-end se utilizará para comunicarse con Amazon S3, mientras que el front-end nos brindará una interfaz de usuario donde podemos cargar nuestro archivo.

Para acelerarnos, vamos a utilizar Node.js Boilerplate de CheatCode para el back-end y Next.js Boilerplate de CheatCode para el front-end. Para obtener esta configuración, necesitamos clonarlos desde Github.

Comenzaremos con el back-end:

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git server

Una vez clonado, cd en el proyecto e instalar sus dependencias:

Terminal

cd server && npm install

A continuación, necesitamos instalar una dependencia adicional, aws-sdk :

Terminal

npm i aws-sdk

Una vez que todas las dependencias estén instaladas, inicie el servidor con:

Terminal

npm run dev

Con su servidor ejecutándose, en otra ventana o pestaña de terminal, necesitamos clonar el front-end:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate.git client

Una vez clonado, cd en el proyecto e instalar sus dependencias:

Terminal

cd client && npm install

Una vez que todas las dependencias estén instaladas, inicie el front-end con:

Terminal

npm run dev

Con eso, estamos listos para comenzar.

Aumentar el límite del analizador corporal

Mirando nuestro código de servidor, lo primero que debemos hacer es modificar el límite de carga para el body-parser middleware en el repetitivo. Este middleware es responsable, como su nombre lo indica, de analizar los datos del cuerpo sin procesar de una solicitud HTTP enviada al servidor (un servidor Express.js).

/servidor/middleware/bodyParser.js

import bodyParser from "body-parser";

export default (req, res, next) => {
  const contentType = req.headers["content-type"];

  if (contentType && contentType === "application/x-www-form-urlencoded") {
    return bodyParser.urlencoded({ extended: true })(req, res, next);
  }

  return bodyParser.json({ limit: "50mb" })(req, res, next);
};

En Express.js, middleware es el término utilizado para referirse al código que se ejecuta entre una solicitud HTTP que llega inicialmente al servidor y pasa a una ruta/ruta coincidente (si existe).

Arriba, la función que estamos exportando es una función de middleware de Express.js que forma parte de CheatCode Node.js Boilerplate. Esta función acepta una solicitud HTTP de Express.js:podemos identificar que pretendemos que sea una solicitud que Express nos pasa por el req , res y next argumentos que Express pasa a sus devoluciones de llamada de ruta y luego entrega esa solicitud al método apropiado del body-parser dependencia incluida en el repetitivo.

La idea aquí es que queremos usar el "convertidor" apropiado de bodyParser para garantizar que los datos del cuerpo sin procesar que obtenemos de la solicitud HTTP se puedan usar en nuestra aplicación.

Para este tutorial, enviaremos datos con formato JSON desde el navegador. Por lo tanto, podemos esperar que cualquier solicitud que enviemos (carga de archivos) se entregue al bodyParser.json() método. Arriba, podemos ver que estamos pasando un objeto con una propiedad limit establecido en 50mb . Esto evita el limit predeterminado de 100kb en el cuerpo de solicitud HTTP impuesto por la biblioteca.

Debido a que estamos cargando archivos de varios tamaños, debemos aumentar esto para que no recibamos ningún error al cargar. Aquí, estamos usando una "mejor suposición" de 50 megabytes como el tamaño de cuerpo máximo que recibiremos.

Agregar una ruta Express.js

A continuación, debemos agregar una ruta donde enviaremos nuestras cargas. Como insinuamos anteriormente, estamos usando Express.js en el repetitivo. Para mantener nuestro código organizado, hemos dividido diferentes grupos de rutas a las que se accede a través de funciones llamadas desde el index.js principal archivo donde se inicia el servidor Express en /server/index.js .

Allí llamamos a una función api() que carga las rutas relacionadas con la API para el repetitivo.

/servidor/api/index.js

import graphql from "./graphql/server";
import s3 from "./s3";

export default (app) => {
  graphql(app);
  s3(app);
};

En ese archivo, debajo de la llamada a graphql() , queremos agregar otra llamada a una función s3() que crearemos a continuación. Aquí, app representa la instancia de la aplicación Express.js a la que agregaremos nuestras rutas. Vamos a crear ese s3() funcionar ahora.

/servidor/api/s3/index.js

import uploadToS3 from "./uploadToS3";

export default (app) => {
  app.use("/uploads/s3", async (req, res) => {
    await uploadToS3({
      bucket: "cheatcode-tutorials",
      acl: "public-read",
      key: req.body?.key,
      data: req.body?.data,
      contentType: req.body?.contentType,
    });

    res.send("Uploaded to S3!");
  });
};

Aquí, tomamos el Express app instancia que pasamos y llamamos al .use() método, pasando la ruta donde nos gustaría que nuestra ruta esté disponible, /uploads/s3 . Dentro de la devolución de llamada de la ruta, llamamos a una función uploadToS3 que definiremos en la siguiente sección.

Es importante tener en cuenta:pretendemos uploadToS3 para devolver una promesa de JavaScript. Por eso tenemos el await palabra clave delante del método. Cuando realizamos la carga, queremos "esperar" a que se resuelva la Promesa antes de responder a la solicitud HTTP original que enviamos desde el cliente. Para asegurarnos de que esto también funcione, hemos añadido la palabra clave async como prefijo. en la función de devolución de llamada de nuestra ruta. Sin esto, JavaScript arrojará un error sobre await siendo una palabra clave reservada cuando se ejecuta este código.

Saltemos a ese uploadToS3 funcione ahora y vea cómo transferir nuestros archivos a AWS.

Conexión de la carga a Amazon S3 en el servidor

Ahora la parte importante. Para realizar nuestra carga en Amazon S3, debemos configurar una conexión a AWS y una instancia de .S3() método en el aws-sdk biblioteca que instalamos anteriormente.

/servidor/api/s3/uploadToS3.js

import AWS from "aws-sdk";
import settings from "../../lib/settings";

AWS.config = new AWS.Config({
  accessKeyId: settings?.aws?.akid,
  secretAccessKey: settings?.aws?.sak,
  region: "us-east-1",
});

const s3 = new AWS.S3();

export default async (options = {}) => { ... };

Antes de pasar al cuerpo de nuestra función, primero debemos conectar una instancia de AWS. Más específicamente, necesitamos pasar una ID de clave de acceso de AWS y una clave de acceso secreta. Este par hace dos cosas:

  1. Autentica nuestra solicitud con AWS.
  2. Valida que este par tenga los permisos correctos para la acción que estamos tratando de realizar (en este caso s3.putObject() ).

La obtención de estas claves está fuera del alcance de este tutorial, pero lea esta documentación de Amazon Web Services para aprender a configurarlas.

Suponiendo que haya obtenido sus claves, o tenga un par existente que pueda usar, a continuación, aprovecharemos la implementación de configuración en CheatCode Node.js Boilerplate para almacenar nuestras claves de forma segura.

/servidor/configuración-desarrollo.json

{
  "authentication": {
    "token": "abcdefghijklmnopqrstuvwxyz1234567890"
  },
  "aws": {
    "akid": "Type your Access Key ID here...",
    "sak":" "Type your Secret Access Key here..."
  },
  [...]
}

Dentro de /server/settings-development.json , arriba, agregamos un nuevo objeto aws , igualándolo a otro objeto con dos propiedades:

  • akid - Esto se establecerá en el ID de la clave de acceso que obtenga de AWS.
  • sak - Esto se establecerá en la clave de acceso secreta que obtiene de AWS.

Dentro de /server/lib/settings.js , este archivo se carga automáticamente en la memoria cuando se inicia el servidor. Notarás que este archivo se llama settings-development.json . El -development parte nos dice que este archivo solo se cargará cuando process.env.NODE_ENV (el entorno actual de Node.js) es igual a development . De manera similar, en producción, crearíamos un archivo separado settings-production.json .

El objetivo de esto es la seguridad y evitar el uso de sus claves de producción en un entorno de desarrollo. Los archivos separados evitan la fuga innecesaria y la mezcla de claves.

/servidor/api/s3/uploadToS3.js

import AWS from "aws-sdk";
import settings from "../../lib/settings";

AWS.config = new AWS.Config({
  accessKeyId: settings?.aws?.akid,
  secretAccessKey: settings?.aws?.sak,
  region: "us-east-1",
});

const s3 = new AWS.S3();

export default async (options = {}) => { ... };

De vuelta en nuestro uploadToS3.js archivo, a continuación, importamos el settings archivo que mencionamos anteriormente de /server/lib/settings.js y de eso, tomamos el aws.akid y aws.sak valores que acabamos de establecer.

Finalmente, antes de profundizar en la definición de la función, creamos una nueva instancia de S3 class, almacenándolo en el s3 variable con new AWS.S3() . Con esto, saltemos al núcleo de nuestra función:

/servidor/api/s3/uploadToS3.js

import AWS from "aws-sdk";

[...]

const s3 = new AWS.S3();

export default async (options = {}) => {
  await s3
    .putObject({
      Bucket: options.bucket,
      ACL: options.acl || "public-read",
      Key: options.key,
      Body: Buffer.from(options.data, "base64"),
      ContentType: options.contentType,
    })
    .promise();

  return {
    url: `https://${options.bucket}.s3.amazonaws.com/${options.key}`,
    name: options.key,
    type: options.contentType || "application/",
  };
};

No hay mucho, así que hemos registrado todo aquí. La función central que vamos a llamar en el s3 instancia es .putObject() . Para .putObject() , pasamos un objeto de opciones con algunas configuraciones:

  • Bucket - El depósito de Amazon S3 donde le gustaría almacenar el objeto (un término de S3 para archivo) que carga.
  • ACL - La "Lista de control de acceso" que le gustaría usar para los permisos de archivo. Esto le dice a AWS quién puede acceder al archivo. Puede pasar cualquiera de las ofertas de Amazon de Canned ACL aquí (estamos usando public-read para otorgar acceso abierto).
  • Key - El nombre del archivo tal como existirá en el depósito de Amazon S3.
  • Body - El contenido del archivo que estás subiendo.
  • ContentType - El tipo MIME del archivo que estás subiendo.

Centrándose en Body , podemos ver que sucede algo único. Aquí, estamos llamando al Buffer.from() método integrado en Node.js. Como veremos en un momento, cuando recuperemos nuestro archivo del FileReader en el navegador, se formateará como una cadena base64.

Para garantizar que AWS pueda interpretar los datos que le enviamos, debemos convertir la cadena que hemos pasado del cliente en un búfer. Aquí, pasamos nuestro options.data —la cadena base64—como primer argumento y luego base64 como segundo argumento para dejar Buffer.from() conocer la codificación que necesita para convertir la cadena.

Con esto, tenemos lo que necesitamos cableado para enviar a Amazon. Para que nuestro código sea más legible, aquí encadenamos el .promise() método al final de nuestra llamada a s3.putObject() . Esto le dice al aws-sdk que queremos que devuelva una Promesa de JavaScript.

Tal como vimos en nuestra devolución de llamada de ruta, debemos agregar el async palabra clave a nuestra función para que podamos utilizar el await palabra clave para "esperar" la respuesta de Amazon S3. Técnicamente hablando, no necesitamos esperar a que S3 responda (podríamos omitir el async/await aquí) pero hacerlo en este tutorial nos ayudará a verificar que la carga esté completa (más sobre esto cuando nos dirigimos al cliente).

Una vez que se completa nuestra carga, desde nuestra función, devolvemos un objeto que describe el url , name y type del archivo que acabamos de subir. Aquí, observe que url está formateado para ser la URL del archivo tal como existe en su depósito de Amazon S3.

Con eso, hemos terminado con el servidor. Pasemos al cliente para conectar nuestra interfaz de carga y hacer que esto funcione.

Conexión de la API de FileReader en el cliente

Como estamos usando Next.js en el cliente, vamos a crear un nuevo upload página en nuestro /pages directorio que albergará un componente de ejemplo con nuestro código de carga:

/cliente/páginas/subir/index.js

import React, { useState } from "react";
import pong from "../../lib/pong";

const Upload = () => {
  const [uploading, setUploading] = useState(false);

  const handleUpload = (uploadEvent) => { ... };

  return (
    <div>
      <header className="page-header">
        <h4>Upload a File</h4>
      </header>
      <form className="mb-3">
        <label className="form-label">File to Upload</label>
        <input
          disabled={uploading}
          type="file"
          className="form-control"
          onChange={handleUpload}
        />
      </form>
      {uploading && <p>Uploading your file to S3...</p>}
    </div>
  );
};

Upload.propTypes = {};

export default Upload;

Primero, configuramos un componente React con el marcado suficiente para obtener una interfaz de usuario básica. Para el diseño, confiamos en Bootstrap, que se configura automáticamente para nosotros en el modelo estándar.

La parte importante aquí es el <input type="file" /> cuál es la entrada del archivo adjuntaremos un FileReader instancia a. Cuando seleccionamos un archivo usando esto, el onChange Se llamará a la función, pasando el evento DOM que contiene nuestros archivos seleccionados. Aquí, estamos definiendo una nueva función handleUpload que usaremos para este evento.

/cliente/páginas/subir/index.js

import React, { useState } from "react";
import pong from "../../lib/pong";

const Upload = () => {
  const [uploading, setUploading] = useState(false);

  const handleUpload = (uploadEvent) => {
    uploadEvent.persist();
    setUploading(true);

    const [file] = uploadEvent.target.files;
    const reader = new FileReader();

    reader.onloadend = (onLoadEndEvent) => {
      fetch("http://localhost:5001/uploads/s3", {
        method: "POST",
        mode: "cors",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          key: file.name,
          data: onLoadEndEvent.target.result.split(",")[1],
          contentType: file.type,
        }),
      })
        .then(() => {
          setUploading(false);
          pong.success("File uploaded!");
          uploadEvent.target.value = "";
        })
        .catch((error) => {
          setUploading(false);
          pong.danger(error.message || error.reason || error);
          uploadEvent.target.value = "";
        });
    };

    reader.readAsDataURL(file);
  };

  return (
    <div>
      <header className="page-header">
        <h4>Upload a File</h4>
      </header>
      <form className="mb-3">
        <label className="form-label">File to Upload</label>
        <input
          disabled={uploading}
          type="file"
          className="form-control"
          onChange={handleUpload}
        />
      </form>
      {uploading && <p>Uploading your file to S3...</p>}
    </div>
  );
};

Upload.propTypes = {};

export default Upload;

Rellenando el handleUpload función, tenemos algunas cosas que hacer. Primero, justo dentro del cuerpo de la función, agregamos una llamada al .persist() de React método en el uploadEvent (este es el evento DOM pasado a través del onChange método en nuestro <input /> ). Necesitamos hacer esto porque React crea algo conocido como un evento sintético que no disponible dentro de las funciones fuera del hilo de ejecución principal (más sobre esto en un momento).

A continuación, usamos el useState() gancho de React para crear una variable de estado uploading y cambiarlo a true . Si mira hacia abajo en nuestro marcado, puede ver que usamos esto para deshabilitar la entrada del archivo mientras estamos en la mitad de la carga y mostramos un mensaje de comentarios para confirmar que el proceso está en marcha.

Después de esto, profundizamos en la funcionalidad principal. Primero, necesitamos obtener el archivo que elegimos del navegador. Para hacerlo llamamos al uploadEvent.target.files y use JavaScript Array Destructuring para "arrancar" el primer archivo en la matriz de archivos y asignarlo a la variable file .

A continuación, creamos nuestra instancia del FileReader() en el navegador. Esto está integrado en los navegadores modernos, por lo que no hay nada que importar.

En respuesta, recibimos un reader instancia. Saltando más allá de reader.onloadend por un segundo, en la parte inferior de nuestro handleUpload función, tenemos una llamada a reader.readAsDataURL() , pasando el file acabamos de desestructurar desde el uploadEvent.target.files formación. Esta línea es responsable de decirle al lector de archivos en qué formato queremos que se lea nuestro archivo en la memoria. Aquí, una URL de datos nos devuelve algo como esto:

Ejemplo de cadena Base64

data:text/plain;base64,4oCcVGhlcmXigJlzIG5vIHJvb20gZm9yIHN1YnRsZXR5IG9uIHRoZSBpbnRlcm5ldC7igJ0g4oCUIEdlb3JnZSBIb3R6

Aunque no lo parezca, esta cadena es capaz de representar todo el contenido de un archivo. Cuando nuestro reader ha cargado completamente nuestro archivo en la memoria, el reader.onloadend se llama a la función event, pasando el objeto onloadevent como argumento. Desde este objeto de evento, podemos obtener acceso a la URL de datos que representa el contenido de nuestro archivo.

Antes de hacerlo, configuramos una llamada a fetch() , pasando la supuesta URL de nuestra ruta de carga en el servidor (cuando ejecuta npm run dev en el repetitivo, ejecuta el servidor en el puerto 5001 ). En el objeto de opciones para fetch() nos aseguramos de establecer el HTTP method a POST para que podamos enviar un cuerpo junto con nuestra solicitud.

También nos aseguramos de configurar el modo cors a verdadero para que nuestra solicitud pase el middleware CORS en el servidor (esto limita las URL que pueden acceder a un servidor; esto está preconfigurado para funcionar entre el modelo estándar de Next.js y el modelo estándar de Node.js para usted). Después de esto, también configuramos el Content-Type encabezado que es un encabezado HTTP estándar que le dice a nuestro servidor en qué formato nuestro POST el cuerpo está adentro. Tenga en cuenta que esto no el mismo que nuestro tipo de archivo.

En el body campo, llamamos a JSON.stringify()fetch() requiere que pasemos el cuerpo como una cadena, no como un objeto, y para eso, pasemos un objeto con los datos que necesitaremos en el servidor para cargar nuestro archivo en S3.

Aquí, key está establecido en file.name para asegurarnos de que el archivo que colocamos en el depósito S3 sea idéntico al nombre del archivo seleccionado de nuestra computadora. contentType se establece en el tipo MIME que se nos proporciona automáticamente en el objeto de archivo del navegador (por ejemplo, si abrimos un .png archivo, esto se establecería en image/png ).

La parte importante aquí es data . Tenga en cuenta que estamos haciendo uso del onLoadEndEvent como insinuamos arriba. Esto contiene el contenido de nuestro archivo como una cadena base64 en su target.result campo. Aquí, la llamada a .split(',') al final está diciendo "divida esto en dos partes, la primera son los metadatos sobre la cadena base64 y la segunda es la cadena base64 real".

Necesitamos hacer esto porque solo la parte después de la coma en nuestra URL de datos (vea el ejemplo anterior) es una cadena base64 real. Si no quita esto, Amazon S3 almacenará nuestro archivo pero cuando lo abramos, será ilegible. Para terminar esta línea, usamos la notación de corchete de matriz para decir "danos el segundo elemento de la matriz (posición 1 en una matriz de JavaScript de base cero)."

Con esto, nuestra solicitud se envía al servidor. Para terminar, agregamos un .then() devolución de llamada—fetch nos devuelve una promesa de JavaScript, que confirma el éxito de las cargas y "restablece" nuestra interfaz de usuario. Nosotros setUploading() a false , borre el <input /> y luego use el pong biblioteca de alertas integrada en el modelo de Next.js para mostrar un mensaje en la pantalla.

En el caso de que haya una falla, hacemos lo mismo, sin embargo, brindamos un mensaje de error (si está disponible) en lugar de un mensaje de éxito.

Si todo funciona según lo planeado, deberíamos ver algo como esto:

Terminando

En este tutorial, aprendimos cómo cargar archivos en Amazon S3 usando la API de FileReader en el navegador. Aprendimos a configurar una conexión a Amazon S3 a través del aws-sdk , así como también cómo crear una ruta HTTP a la que podamos llamar desde el cliente.

En el navegador, aprendimos a usar el FileReader API para convertir nuestro archivo en una cadena Base64 y luego usar fetch() para pasar nuestro archivo a la ruta HTTP que creamos.