Cómo transmitir un archivo en respuesta a una solicitud HTTP en Node.js

Cómo enviar un archivo grande en respuesta a una solicitud HTTP usando secuencias sin bloquear el servidor para que no maneje otras solicitudes.

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 hacer eso, necesitamos instalar una dependencia mime :

Terminal

cd app && npm i mime

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.

¿Por qué?

Si está creando una aplicación que maneja solicitudes HTTP para archivos grandes (por ejemplo, imágenes, videos o documentos grandes como archivos PDF), es importante saber cómo usar las secuencias. Al leer un archivo del sistema de archivos en Node.js, por lo general, puede estar acostumbrado a usar algo como fs.readFile() o fs.readFileSync() . El problema con estos métodos es que leen el archivo completo en la memoria . Esto significa que si su servidor usa cualquiera de estos para leer un archivo antes de responder a una solicitud, está consumiendo la memoria de la máquina en la que se ejecuta su aplicación.

Por el contrario, las secuencias no cargan nada en la memoria. En cambio, envían (o "canalizan") los datos directamente a la solicitud, lo que significa que nunca se cargan en la memoria, solo se transfieren directamente. La desventaja de este enfoque es que, según el tamaño del archivo que está transmitiendo a la solicitud, puede haber una demora en el extremo receptor (por ejemplo, cuando ve un "búfer" de video en el navegador, es probable que esté recibiendo datos como un flujo). Si esto es de poca (o ninguna) preocupación para su aplicación, las transmisiones son una excelente manera de maximizar la eficiencia.

Agregar una ruta que devuelve un flujo de archivos

Para mostrar esto, configuraremos una ruta simple dentro de la aplicación que acabamos de crear en /files/:fileName donde :fileName es un parámetro de ruta que se puede reemplazar con el nombre de cualquier archivo (por ejemplo, video.mp4 o potato.png ). Para las pruebas, vamos a utilizar algunas imágenes generadas aleatoriamente de This Person Does Not Exist y una parte editada de un carrete de gráficos VFX. Todos los archivos utilizados para este tutorial se pueden descargar desde el depósito S3 de CheatCode aquí.

/index.servidor.js

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

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/files/:fileName": (req, res) => {   
      // TODO: We'll implement our file stream response here...
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

Para empezar, queremos abrir el /index.server.js archivo en la raíz de la aplicación que acabamos de crear cuando ejecutamos joystick create app arriba. Dentro de este archivo está el código; aquí, el node.app() función:utilizada para iniciar el servidor HTTP (detrás de escena, esto ejecuta un servidor Express.js) para su aplicación y conectar sus rutas, API y otra configuración.

En el routes objeto aquí, hemos definido una propiedad /files/:fileName asignado a la función de controlador de ruta utilizada por Express.js para "manejar" las solicitudes a esa URL. Como sugerimos anteriormente, la idea será que podamos enviar una solicitud HTTP GET a esta ruta, pasando el nombre de algún archivo que esperamos que exista en la posición de :fileName , por ejemplo:http://localhost:2600/files/cat.jpg .

/index.servidor.js

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

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/files/:fileName": (req, res) => {   
      const filePath = `public/files/${req?.params?.fileName}`;
       
      if (fs.existsSync(filePath)) {
        // TODO: If the file exists, we'll stream it to the response here...
      }

      return res.status(404).send(`404 – File ${filePath} not found.`);
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

A continuación, dentro de esa función de controlador de ruta, creamos una variable const filePath que se asigna a una cadena interpolada (lo que significa que toma texto sin formato e inyecta o incrusta un valor dinámico en él) que combina la ruta public/files/ con el nombre de archivo pasado como :fileName en nuestra ruta (accesible en nuestro código aquí como req.params.fileName ).

La idea aquí es que en el public carpeta en la raíz de nuestra aplicación, queremos crear otra carpeta files donde almacenaremos los archivos para probar nuestra transmisión. Esto es arbitrario y puramente por ejemplo . La razón por la que elegimos esta ubicación es que el /public la carpeta contiene datos que pretendemos estar disponible públicamente y el /files anidado La carpeta es solo una forma de separar visualmente nuestros datos de prueba de otros archivos públicos. Técnicamente, el archivo que transmite puede provenir de cualquier parte de su servidor. Solo tenga cuidado de no exponer archivos que no tiene la intención de exponer.

Lo que más nos importa aquí es el if declaración y el fs.existsSync() pasó a ella. Esta función (del fs importado dependencia que hemos agregado en la parte superior:una biblioteca Node.js incorporada) devuelve un valor booleano true o false diciéndonos si la ruta dada realmente existe o no. En nuestro código aquí, solo queremos transmitir el archivo si realmente existe. Si no es así, en la parte inferior de nuestra función queremos enviar un código de estado HTTP 404 y un mensaje que le informe al solicitante que el archivo no existe.

Terminal

import node from "@joystick.js/node";
import fs from 'fs';
import mime from 'mime';
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/files/:fileName": (req, res) => {   
      const filePath = `public/files/${req?.params?.fileName}`;
       
      if (fs.existsSync(filePath)) {
        res.setHeader('Content-Type', mime.getType(filePath));
        res.setHeader('Content-Disposition', `attachment; filename="${req?.params?.fileName}"`);
        const stream = fs.createReadStream(filePath);
        return stream.pipe(res);
      }

      return res.status(404).send(`404 – File ${filePath} not found.`);
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

Ahora para las cosas importantes. Primero, arriba, hemos agregado una importación para el mime paquete que nos ayudará a detectar dinámicamente el tipo MIME ("Extensiones de correo de Internet multipropósito", un formato estándar bien soportado para describir archivos multimedia) para el archivo. Esto es importante ya que necesitamos comunicarle al solicitante qué el flujo contiene para que sepan cómo manejarlo correctamente.

Para ello, si nuestro archivo existe, comenzamos llamando al res.setHeader() función proporcionada por Express.js, pasando el nombre del encabezado que queremos establecer, seguido del valor de ese encabezado. Aquí, Content-Type (el encabezado HTTP estándar para un formato de tipo de respuesta en la web) se establece en el valor de what mime.getType() devoluciones para nuestro filePath .

A continuación, configuramos Content-Disposition que es otro encabezado HTTP estándar que contiene instrucciones sobre cómo el solicitante debe manejar el archivo. Hay dos valores posibles para esto:'inline' lo que sugiere que el navegador/solicitante debería simplemente cargar el archivo directamente, o attachment; filename="<name>" lo que sugiere que el archivo debe descargarse (más información aquí). Técnicamente, este comportamiento depende del navegador o del solicitante que recibe el archivo, por lo que no vale la pena preocuparse por ello.

A continuación, la parte importante de este tutorial:para crear nuestra transmisión, llamamos a fs.createReadStream() pasando el filePath y almacenar el resultado (un objeto de flujo) en una variable const stream . Ahora para la parte "mágica". Lo bueno de una transmisión es que se puede "canalizar" en otro lugar. Este término "tubería" se toma de la misma convención en los sistemas Linux/Unix donde puedes hacer cosas como cat settings.development.json | grep mongodb (aquí el | El carácter de canalización le dice al sistema operativo que "entregue" o "canalice" el resultado de cat settings.development.json a grep mongodb ).

En nuestro código aquí, queremos canalizar nuestra transmisión a Express.js res ponse objeto para nuestra ruta con stream.pipe(res) (se lee mejor como "canalizar el stream a res "). En otras palabras, queremos responder a una solicitud de esta ruta con el flujo de nuestro archivo.

¡Eso es todo! Ahora, si abrimos un navegador y presionamos una URL como http://localhost:2600/files/art.mp4 (suponiendo que esté utilizando los archivos de ejemplo vinculados desde el depósito S3 anterior), debería ver que el video comienza a cargarse en el navegador. Preste atención a cómo la cantidad "cargada" del video continúa almacenándose/creciendo con el tiempo. Estos son los datos de transmisión que llegan al navegador (nuestro solicitante).

Terminando

En este tutorial, aprendimos a usar flujos para responder a solicitudes HTTP. Aprendimos cómo configurar una ruta simple, primero verificando si existe un archivo (devolviendo un 404 si no existe) y luego, cómo recuperar dinámicamente el tipo MIME para un archivo y luego crear y canalizar una secuencia de el contenido de ese archivo a la respuesta de nuestra solicitud HTTP.