JavaScript y OPFS: almacenar y leer archivos

En este artículo voy a enseñarte a usar el Origin Private File System con JavaScript para almacenar, leer y eliminar cualquier tipo de archivo en el navegador web.

Sistema de archivos en la web con JavaScript y OPFS
Sistema de archivos en la web con JavaScript y OPFS

Puedes probar la demostración aquí: https://stackblitz.com/edit/vitejs-vite-hl34zf?file=index.html

Como lo dije anteriormente, el Origin Private File System ha llegado para revolucionar las cosas con JavaScript. Gracias al OPFS podemos tener un sistema de archivos completo con JavaScript directamente en el navegador web.

Con esto, podemos escribir cualquier tipo de archivo en el web browser, así como descargarlo más adelante. Todo ello sin depender de localStorage o cosas similares; es una tecnología diferente.

Se pueden guardar documentos de texto, imágenes, vídeos e incluso bases de datos, además de que no hay necesidad de pedir permiso o confirmación al usuario, todo es transparente.

Carpeta raíz

Para acceder al directorio raíz, invocamos a navigator.storage.getDirectory(). Este es el directorio raíz, aquí podemos crear más directorios y archivos. Yo crearé uno así:

const directoryName = "pictures";
const opfsRoot = await navigator.storage.getDirectory();
const picturesFolder = await opfsRoot.getDirectoryHandle(directoryName, {
    create: true,
});

A partir de aquí, voy a almacenar, leer y eliminar desde picturesFolder. Toma en cuenta que aquí un “handle” o manejador no es el archivo o directorio en sí, sino su manejador.

Para obtener el archivo invocamos a getFile y cosas similares, como veremos más adelante.

Almacenar archivo en navegador con JavaScript

Ahora que tenemos un manejador de un directorio podemos invocar a getFileHandle (indicando la opción create en true), luego invocar a createWritable y finalmente invocando a write.

Lo que se recomienda pasar a la función write es un array búfer o blob, no necesitamos hacer conversiones o cosas similares. Y el archivo puede venir de cualquier lugar; por ejemplo, de un servidor web o de un input tipo file.

Para este caso voy a usar un input tipo file y la función queda así:

const storeFile = async (file, folder) => {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = async () => {
            const fileName = file.name;
            const fileHandle = await folder.getFileHandle(fileName, {
                create: true,
            });
            const writable = await fileHandle.createWritable();
            await writable.write(reader.result);
            await writable.close();
            resolve();
        };
        reader.onerror = error => {
            reject(error);
        }
        reader.readAsArrayBuffer(file);
    });
}

Podemos invocar a esa función desde el change del input, un clic de un botón o cualquier otro evento:

$fileSelect.addEventListener("change", async () => {
    const files = $fileSelect.files;
    for (const file of files) {
        await storeFile(file, picturesFolder)
    }
    $fileSelect.value = "";
    renderFiles(picturesFolder);
});

Listar archivos de un directorio

De nuevo, cuando tenemos un manejador o apuntador a un directorio del OPFS podemos recorrerlo con un for await, extrayendo el nombre del archivo y el manejador. La manera más básica es:

for await (let [name, handle] of folder) {
//
}

En este caso estoy usando JavaScript puro para dibujar los elementos, agregando los listeners para descargar el archivo y eliminarlo.

const renderFiles = async (folder) => {
    while ($mainContainer.firstChild) {
        $mainContainer.removeChild($mainContainer.firstChild);
    }
    for await (let [name, handle] of folder) {
        const container = Object.assign(document.createElement("div"));
        const anchorFileName = Object.assign(document.createElement("a"), {
            textContent: name,
            href: "javascript:void(0)",
            classList: ["file"],
        });
        const anchorRemove = Object.assign(document.createElement("a"), {
            textContent: "Eliminar",
            href: "javascript:void(0)",
            classList: ["remove"],
        });
        anchorFileName.addEventListener("click", () => {
            downloadFile(name, folder);
        });
        anchorRemove.addEventListener("click", async () => {
            await removeFile(name, folder);
            await renderFiles(folder);
        })
        container.appendChild(anchorFileName);
        container.appendChild(anchorRemove);
        $mainContainer.append(container);
    }
};

Nota: aunque aquí uso la palabra “descargar”, no hay necesidad de servidores o cosas similares. Todo es fuera de línea, lo que hacemos al descargar el archivo es leerlo del navegador y preguntar al usuario la ubicación donde quiere guardarlo.

Descargar archivo del Origin Private File System

Ya vimos cómo guardar un archivo en el navegador con JavaScript y también cómo obtener la lista de archivos que existen dentro de un directorio.

Veamos cómo recuperar un archivo. Aquí vamos a descargarlo, pero podemos enviarlo a un servidor o hacer cualquier otra cosa.

const downloadFile = async (fileName, folder) => {
    const fileHandle = await folder.getFileHandle(fileName);
    const file = await fileHandle.getFile();
    const a = document.createElement("a");
    const objectUrl = URL.createObjectURL(file);
    a.href = objectUrl;
    a.download = fileName;
    a.click();
    URL.revokeObjectURL(objectUrl);
}

El verdadero archivo está en la constante file, y luego simplemente creamos un enlace para descargarlo con URL.createObjectURL. Aquí podríamos hacer cualquier otra cosa con el archivo.

Eliminar archivo

Finalmente veamos cómo eliminar un archivo almacenado previamente en el navegador web con JavaScript. Una vez que tenemos su manejador solo invocamos a remove (toma en cuenta que, al momento de escribir este post, remove solo funciona en Chrome y Edge):

const removeFile = async (fileName, folder) => {
    if (!('remove' in FileSystemFileHandle.prototype)) {
        return alert("Tu navegador no soporta la eliminación de archivos");
    }
    if (!confirm('¿De verdad quieres eliminarlo?')) {
        return;
    }
    const fileHandle = await folder.getFileHandle(fileName);
    await fileHandle.remove();
}

¿Por qué he dicho que es una revolución?

El OPFS ha traído consigo dos cosas: el sistema de archivos en sí, y la posibilidad de que varias librerías existentes la usen.

Lo que más me ha gustado es que SQLite3 ahora funciona perfectamente en el navegador web, así que estamos más cerca de crear aplicaciones web sin servidores (para cuando no se necesite ningún servidor) ya que podemos tener una base de datos y también almacenar archivos.

Vídeo explicativo

Complementa la información del OPFS con JavaScript mirando el siguiente vídeo: https://www.youtube.com/watch?v=GZ3rvxRdaB4

Conclusión y ejemplo

Te he mostrado las funciones más importantes para usar el Origin Private File System, es un ejemplo muy simple pero que trata de enseñarte lo más importante. Si quieres ver el código completo ve a mi GitHub: https://github.com/parzibyte/hello-opfs-js

Puedes probar el ejemplo en el servidor que tú prefieras, solo asegúrate de enviar los encabezados COOP y COEP. Cuando ejecutes el ejemplo aparecerá la lista de archivos y un campo para que puedas almacenar más ficheros (como en la imagen del inicio del post)

A partir de este ejemplo podemos hacer cosas más complejas, pues ya tenemos todo un sistema de archivos en el navegador web al que podemos acceder con JavaScript.

Incluso podemos encriptar los archivos o mostrarlos como imagen o vídeo; las posibilidades son infinitas.

Espero traer una aplicación más completa en el futuro, mientras tanto te dejo con más posts de JavaScript.

Encantado de ayudarte


Estoy disponible para trabajar en tu proyecto, modificar el programa del post o realizar tu tarea pendiente, no dudes en ponerte en contacto conmigo.

No te pierdas ninguno de mis posts

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

Dejar un comentario