Cómo cargar archivos en múltiples ubicaciones simultáneamente con Joystick

Cómo cargar archivos a múltiples destinos usando la función de carga de Joystick.

Primeros pasos

Para este tutorial, vamos a utilizar el marco JavaScript de pila completa de CheatCode, Joystick. Joystick reúne un marco de interfaz de usuario de front-end con un back-end de Node.js para crear aplicaciones.

Para comenzar, querremos instalar Joystick a través de NPM. Asegúrese de estar usando Node.js 16+ antes de instalar para garantizar la compatibilidad (lea este tutorial primero si necesita aprender a instalar Node.js o ejecutar varias versiones en su computadora):

Terminal

npm i -g @joystick.js/cli

Esto instalará Joystick globalmente en su computadora. Una vez instalado, vamos a crear un nuevo proyecto:

Terminal

joystick create app

Después de unos segundos, verá un mensaje desconectado de cd en su nuevo proyecto y ejecute joystick start . Antes de hacerlo, necesitamos instalar una dependencia, uuid :

Terminal

cd app && npm i uuid

Usaremos esto para generar un UUID arbitrario que podemos pasar junto con nuestra carga para demostrar la transferencia de datos con su carga. Después de que esté instalado, puede iniciar su servidor:

Terminal

joystick start

Después de esto, su aplicación debería estar ejecutándose y estamos listos para comenzar.

Configuración de un depósito de Amazon S3

Para este tutorial, una de las dos ubicaciones en las que cargamos nuestros archivos será Amazon S3 (la otra estará en una carpeta local dentro de la aplicación). Para S3, debemos asegurarnos de tener algunas cosas:

  1. Una cuenta de Amazon Web Services.
  2. Un usuario de Amazon IAM a las credenciales del proveedor para acceder al depósito.
  3. Un depósito de Amazon S3.

Si ya tiene acceso a estos, puede pasar a la sección "Conexión de un cargador en el servidor" a continuación.

Si no los tiene, primero diríjase a Amazon Web Services y cree una nueva cuenta aquí.

Una vez que se haya registrado, asegúrese de haber completado todos los pasos para agregar su información de facturación y luego diríjase a la página de Credenciales de seguridad de IAM. En el menú de la izquierda, haga clic en la opción "Usuarios" en el subtítulo "Gestión de acceso".

En la esquina superior derecha de esta página, haga clic en el botón azul "Agregar usuarios". En la página siguiente, en el cuadro "Nombre de usuario", escriba un nombre de usuario para su usuario de IAM (Administración de acceso a la identidad) y en "Seleccione el tipo de acceso de AWS", marque la casilla junto a "Clave de acceso:acceso programático". Una vez configurados, haga clic en "Siguiente:Permisos" en la esquina inferior derecha de la página.

En la siguiente pantalla, haga clic en el tercer cuadro llamado "Adjuntar políticas existentes directamente" y luego en el cuadro de búsqueda junto a "Políticas de filtro" en el medio de la página, escriba "s3full" para filtrar la lista al AmazonS3FullAccess opción. Marque la casilla junto a este elemento y luego haga clic en el botón "Siguiente:Etiquetas" en la parte inferior derecha de la página.

La página de "etiquetas" se puede omitir, así como la siguiente (a menos que esté familiarizado con estas y desee completarlas). Después de esto, se revelarán las credenciales de su usuario de IAM.

Nota:las credenciales de IAM son como ORO para los ladrones. Bajo ninguna circunstancia los coloque en un repositorio público de Github ni se los dé a alguien que no conozca o en quien no confíe. Es muy fácil filtrar estas claves y encontrar una factura sorpresa de Amazon a fin de mes con cargos que no acumulaste (hablo por experiencia).

Es mejor almacenar estas credenciales en una ubicación segura como 1Password, LastPass u otra herramienta de administración de contraseñas en la que confíe.

Una vez que haya configurado sus credenciales, regrese a la lista de "Usuarios" que comenzamos desde arriba y haga clic en el usuario que acaba de crear para revelar la página "Resumen". Desde aquí, querrá copiar la cadena larga "ARN de usuario" justo debajo del encabezado de la página. Usaremos esto a continuación para configurar su cubo.

Una vez que haya copiado esto, en el cuadro de búsqueda en la parte superior de la página (a la derecha del logotipo "AWS") escriba s3 y seleccione la primera opción que aparece debajo de "Servicios" en los resultados de búsqueda.

En la página siguiente, haga clic en el botón naranja "Crear depósito" en la esquina superior derecha de la página. Desde esta página, debemos completar los siguientes campos:

  1. Para "Nombre del depósito", ingrese un nombre único (los nombres de los depósitos deben ser exclusivos de la región que seleccione para la segunda opción) que describa lo que contendrá su depósito.
  2. Para "Región de AWS", seleccione la región más cercana a la mayoría de sus usuarios o la más cercana a usted.
  3. En "Propiedad del objeto", seleccione la casilla "ACL habilitadas". Aunque esto no se recomienda, lo necesitaremos para personalizar los permisos por usuario en su aplicación.
  4. Para "Bloquear el acceso público...", esta opción depende de usted. Si su depósito NO almacenará archivos confidenciales o archivos que le gustaría mantener en privado, puede desmarcar esta casilla (y marcar la advertencia "Reconozco" que aparece cuando lo hace). Para el cubo en uso durante el resto del tutorial, hemos desmarcado esta casilla para permitir objetos públicos.

Una vez configurados, puede omitir las otras configuraciones y hacer clic en "Crear depósito" en la parte inferior de la página. Una vez que haya creado su cubo, ubíquelo en la lista de cubos y haga clic en él para revelarlo en el tablero. Desde aquí, ubique la pestaña "Permisos" en la parte superior de la página y en esta pestaña, ubique y haga clic en el botón "Editar" en el bloque "Política de depósito".

En el cuadro que aparece, querrá pegar la siguiente declaración, reemplazando el <bucket-name> marcador de posición con el nombre del depósito que acaba de crear y <user arn you copied> con el "ARN de usuario" que copiamos arriba.

Ejemplo de política de depósitos de Amazon S3

{
  "Id": "Policy1654277614273",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1654277612532",
      "Action": "s3:*",
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::<bucket-name>/*",
      "Principal": {
        "AWS": [
          "<user arn you copied>"
        ]
      }
    }
  ]
}

Después de personalizarlo para su depósito y usuario, desplácese hacia abajo y haga clic en el botón naranja "Guardar cambios". Una vez que esto está configurado, lo que acabamos de lograr fue permitir que las credenciales de usuario de IAM que acabamos de crear tengan acceso completo al depósito que acabamos de crear. Esto entrará en juego cuando configuremos nuestro cargador a continuación y establezcamos la "ACL" ("lista de control de acceso" en lenguaje AWS) que mencionamos anteriormente.

Cableando un cargador en el servidor

Para admitir la carga de archivos en una aplicación Joystick, debemos definir un cargador en el servidor en nuestro /index.server.js expediente. Echemos un vistazo a la configuración básica y analicemos:

/index.servidor.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  uploaders: {
    photos: {
      providers: ['local', 's3'],
      local: {
        path: 'uploads',
      },
      s3: {
        region: 'us-east-1',
        accessKeyId: joystick?.settings?.private?.aws?.accessKeyId,
        secretAccessKey: joystick?.settings?.private?.aws?.secretAccessKey,
        bucket: 'cheatcode-tutorials',
        acl: 'public-read',
      },
      mimeTypes: ['image/jpeg', 'image/png', 'image/svg+xml', 'image/webp'],
      maxSizeInMegabytes: 5,
      fileName: ({ input, fileName, mimeType }) => {
        // NOTE: Return the full path and file name that you want the file to be stored in
        // relative to the provider.
        return `photos/${input?.photoId}_${fileName}`;
      },
    },
  },
  routes: { ... },
});

Esto es todo lo que necesitamos para soportar cargas de múltiples ubicaciones. Primero, arriba, estamos llamando al node.app() función importada del @joystick.js/node paquete que inicia nuestro servidor por nosotros (usando Express.js detrás de escena). A esa función, podemos pasar opciones en un objeto para personalizar el comportamiento de nuestra aplicación.

Aquí, el uploaders La opción toma un objeto donde cada propiedad define uno de los cargadores que queremos admitir en nuestra aplicación (aquí, estamos definiendo un cargador llamado photos ). A esa propiedad, le pasamos el objeto o "definición" para nuestro cargador.

En la parte superior, pasamos un providers matriz de cadenas para especificar dónde queremos que vaya nuestra carga (Joystick enruta automáticamente la carga de un archivo a estos proveedores). Aquí, podemos especificar uno o más proveedores que recibirán una carga. En este caso, queremos cargar en dos ubicaciones:nuestra máquina local y Amazon S3.

Basado en el providers que pasamos, a continuación, debemos definir la configuración para esos proveedores específicos.

Para local , pasamos un objeto con un solo objeto path que especifica la ruta local (relativa a la raíz de nuestra aplicación) donde se almacenarán nuestros archivos.

Para s3 , las cosas son un poco más complicadas. Aquí, necesitamos especificar algunas propiedades diferentes:

  • region que es el código abreviado de la región de AWS para la región donde se encuentra nuestro depósito.
  • accessKeyId que es el "ID de clave de acceso" que generó junto con su usuario de IAM anteriormente.
  • secretAccessKey que es la "Clave de acceso secreta" que generó junto con su usuario de IAM anteriormente.
  • bucket que es el nombre del depósito donde desea que se almacenen sus archivos.
  • acl que es la "lista de control de acceso" o el permiso general que desea aplicar a todos los archivos cargados a través de este cargador. Para nuestro ejemplo, estamos usando public-read lo que significa que los archivos son de solo lectura para usuarios públicos.

Nota :para el accessKeyId y secretAccessKey valores aquí, observe que estamos extrayendo estos valores de joystick?.settings?.private?.aws . En una aplicación Joystick, puede especificar configuraciones para cada entorno en su aplicación en el settings.<env>.json archivo en la raíz de su aplicación (donde <env> hay algún entorno compatible con su aplicación).

Aquí, porque estamos en el development entorno, esperamos que estos valores se definan en nuestro settings.development.json expediente. Aquí hay una versión actualizada de este archivo (deberá completar su accessKeyId y secretAccessKey que obtuvo de AWS anteriormente):

/configuraciones.desarrollo.json

{
  "config": {
    "databases": [
      {
        "provider": "mongodb",
        "users": true,
        "options": {}
      }
    ],
    "i18n": {
      "defaultLanguage": "en-US"
    },
    "middleware": {},
    "email": {
      "from": "",
      "smtp": {
        "host": "",
        "port": 587,
        "username": "",
        "password": ""
      }
    }
  },
  "global": {},
  "public": {},
  "private": {
    "aws": {
      "accessKeyId": "",
      "secretAccessKey": ""
    }
  }
}

Un archivo de configuración en Joystick admite cuatro propiedades raíz:config , global , public y private . Aquí, utilizamos el private objeto al que solo se puede acceder en el servidor para almacenar nuestras credenciales de AWS (NO queremos ponerlas en global o public ya que estarán expuestos al navegador si lo hacemos).

De vuelta en nuestra definición de cargador, después de s3 , tenemos algunas configuraciones genéricas específicas para el cargador. Estos incluyen:

  • mimeTypes que es una serie de cadenas que especifican los tipos MIME admitidos por este cargador (por ejemplo, aquí solo pasamos tipos MIME de imagen para evitar que se carguen cosas como videos, documentos o archivos de audio).
  • maxSizeInMegabytes el tamaño máximo de archivo (en megabytes) permitido para este cargador. El cargador rechazará los archivos que excedan este límite.
  • fileName una función que nos da la oportunidad de personalizar la ruta/nombre de archivo para el archivo que estamos subiendo. Esta función recibe un objeto que contiene el fileName , fileSize , fileExtension y mimeType para el archivo subido, así como el input pasamos del cliente (más sobre esto más adelante). Aquí, devolvemos una ruta que anida las cargas en una carpeta photos y antepone el fileName del archivo subido con el photoId pasado a través del input objeto.

¡Eso es todo! Con esto, ahora tenemos un cargador listo para usar en el servidor. Pasemos al cliente y veamos cómo subimos los archivos.

Llamar a un cargador en el cliente

Afortunadamente, llamar a un cargador desde el cliente es bastante simple:solo necesitamos llamar a una sola función upload del @joystick.js/ui paquete (el mismo que usamos para definir nuestros componentes). Para hacer nuestro trabajo un poco más fácil aquí, vamos a reutilizar el /ui/pages/index/index.js existente archivo que ya se creó para nosotros cuando ejecutamos joystick create app antes.

Reemplacemos el contenido existente de eso con lo que está debajo y repasemos:

/ui/pages/index/index.js

import ui, { upload } from "@joystick.js/ui";
import { v4 as uuid } from "uuid";

const Index = ui.component({
  state: {
    uploads: [],
    progress: 0,
  },
  events: {
    'change input[type="file"]': (event, component) => {
      component.setState({ urls: [], }, () => {
        upload('photos', {
          files: event.target.files,
          input: {
            // NOTE: Arbitrary, just to demonstrate passing data alongside your upload.
            // This is accessible within the `fileName` function on your uploader definition.
            photoId: uuid(),
          },
          onProgress: (progress = 0, provider = '') => {
            component.setState({ progress, provider });
          },
        }).then((uploads) => {
          component.setState({ progress: 0, uploads });
        }).catch((errors) => {
          console.warn(errors);
        });
      });
    },
  },
  css: `
    .progress-bar {
      width: 100%;
      height: 10px;
      border-radius: 30px;
      background: #eee;
      margin-top: 30px;
    }

    .progress-bar .progress {
      height: 10px;
      background: #ffcc00;
      border-radius: 30px;
    }
  `,
  render: ({ when, state, each }) => {
    return `
      <div>
        <input type="file" />
        ${when(state.progress > 0, `
          <div class="progress-bar">
            <div class="progress" style="width:${state.progress}%;"></div>
          </div>
        `)}
        ${when(state.uploads?.length > 0, `
          <ul>
            ${each(state.uploads, (upload) => {
              return `<li>${upload.provider}: ${upload.url ? `<a href="${upload.url}">${upload.url}</a>` : upload.error}</li>`;
            })}
          </ul>
        `)}
      </div>
    `;
  },
});

export default Index;

Empezando por el render función, aquí, especificamos algo de HTML que queremos representar para nuestro componente. La parte importante aquí es el <input type="file" /> etiqueta que es cómo seleccionaremos los archivos para cargar desde nuestra computadora.

Debajo de esto, usando el when función de representación (este es el nombre utilizado para las funciones "contextuales" especiales que se pasan al render de un componente función en Joystick) para decir "cuando el valor de state.progress es mayor que 0 , renderice este HTML". "Este HTML", aquí, es el marcado para una barra de progreso que se llenará a medida que se complete nuestra carga.

Para simular el relleno, hemos agregado un style en línea atributo que establece el CSS width propiedad dinámicamente en el interior <div class="progress"></div> elemento al valor de state.progress concatenado con un % símbolo de porcentaje (Joystick nos proporciona automáticamente el porcentaje de finalización de carga como un valor flotante/decimal).

Debajo de esto, nuevamente usando el when() función, si vemos que state.uploads tiene una longitud superior a 0 (lo que significa que subimos un archivo y recibimos una respuesta de todos nuestros proveedores), queremos generar un <ul></ul> etiqueta que enumera los proveedores y las URL devueltas por esos proveedores para nuestros archivos.

Aquí, utilizamos el each() función de representación, que, como su nombre lo indica, nos ayuda a representar algo de HTML para cada uno elemento en una matriz. Aquí, para cada objeto esperado dentro de state.uploads , devolvemos un <li></li> etiqueta que nos dice el provider para cargas específicas (por ejemplo, local o s3 ) junto con la URL devuelta por el proveedor.

Justo encima de esto, utilizando el css opción en nuestros componentes, pasamos algunos estilos simples para nuestra barra de progreso (siéntase libre de copiar esto y modificarlo para su propia aplicación).

La parte importante aquí es el events bloque justo encima de css . Aquí, definimos los detectores de eventos DOM de JavaScript que queremos escuchar dentro nuestro componente (es decir, Joystick analiza automáticamente los detectores de eventos definidos aquí para este componente). A events , pasamos un objeto con propiedades definidas como una cadena que combina dos valores con un espacio en el medio:el tipo de DOM event queremos escuchar y el element queremos escuchar el evento on (<event> <element> ).

En este caso, queremos escuchar un change evento en nuestro <input type="file" /> elemento. Cuando esto ocurre, significa que nuestro usuario ha seleccionado un archivo que desea cargar; un momento perfecto para activar la carga de ese archivo A esta propiedad, le pasamos la función que Joystick llamará cuando se detecte este evento en nuestra entrada de archivo.

Dentro, primero llamamos al component.setState() para vaciar nuestro state.urls valor, asumiendo que estamos ejecutando nuestro cargador varias veces y no queremos mezclar las URL de respuesta.

A continuación, dentro, llamamos al upload() función que hemos importado de @joystick.js/ui por encima de. Esta función es casi idéntica a la get() y set() funciones en Joystick que se utilizan para llamar a puntos finales de API definidos como captadores y definidores en su aplicación Joystick.

Se necesitan dos argumentos:

  1. El nombre del cargador que definimos en el servidor que manejará esta carga (por ejemplo, aquí, pasamos 'photos' ya que ese es el nombre que usamos para nuestro cargador en el servidor).
  2. Un objeto de opciones que proporciona el files queremos subir, cualquier miscelánea input datos que queremos transmitir y un onProgress función que se llama cada vez que cambia el progreso de nuestra carga.

Para files aquí, solo estamos pasando event.target.files que contiene la matriz de archivos del navegador proporcionada en el change evento para una entrada de archivo (esto es obligatorio ya que le dice a Joystick cuál archivos que estamos tratando de cargar). Para input , solo por el bien de la demostración, pasamos un objeto con una sola propiedad photoId configurado para una llamada a uuid() . Esta es una función del uuid paquete que instalamos anteriormente (ver la importación en la parte superior de este archivo) que genera un valor UUID aleatorio. Si bien esto no es necesario , demuestra cómo pasar datos adicionales junto con nuestro cargador para usar con el fileName() función en nuestra definición de cargador.

Para onProgress , cada vez que Joystick recibe un evento de progreso del servidor, llama a la función que pasamos a onProgress aquí con dos argumentos:primero, el progress de la subida como porcentaje y provider que es el nombre del proveedor al que pertenece el progreso. Por ejemplo, aquí, porque estamos subiendo a local y s3 , esperaríamos que esto se llamara con algún progress porcentaje y local o s3 para el provider valor. Esto nos permite realizar un seguimiento del progreso por proveedor si así lo deseamos.

Finalmente, porque esperamos upload() para devolver una promesa de JavaScript, hemos agregado un .then() devolución de llamada y .catch() devolución de llamada al final. Si nuestra carga se completa sin ningún problema, el .then() se disparará la devolución de llamada, recibiendo una serie de objetos que describen el resultado de carga para cada proveedor (es decir, un objeto para local , un objeto para s3 , etc.).

Porque estamos representando nuestra lista de subidas en nuestro render() función, aquí, simplemente tomamos la matriz sin procesar y la configuramos en state.uploads (recuerde, esto es a lo que nos referimos en nuestro render() función).

Entonces está claro, en la parte superior de nuestro objeto de opciones pasado a ui.component() hemos proporcionado un state objeto que establece algunos valores predeterminados para nuestros dos valores de estado:uploads como una matriz vacía [] y progress como 0 .

¡Deberias hacer eso! Ahora, si seleccionamos un archivo de imagen de nuestra computadora y lo cargamos, deberíamos ver que nuestra barra de progreso se llena y una lista de URL se muestra en la pantalla después de que se complete.

Terminando

En este tutorial, aprendimos cómo agregar cargas a una aplicación Joystick. Aprendimos cómo definir un cargador en el servidor, especificar múltiples proveedores/destinos, pasar la configuración para cada proveedor y cómo personalizar el mimeTypes permitido. , fileSize y fileName para el archivo que estamos cargando. En el cliente, aprendimos cómo llamar a nuestro cargador, manejando tanto el progreso de la carga como las URL resultantes una vez que se completa nuestra carga.