El sistema de archivos privados de origen

File System Standard presenta un sistema de archivos privados de origen (OPFS) como un extremo de almacenamiento privado para el origen de la página y no visible para el usuario que proporciona acceso opcional a un tipo especial de archivo altamente optimizado para su rendimiento.

Navegadores compatibles

El sistema de archivos privados de origen es compatible con los navegadores modernos y está estandarizado por el Grupo de trabajo de tecnología de aplicaciones de hipertexto web (WhatWG) en el File System Living Standard.

Navegadores compatibles

  • Chrome: 86.
  • Borde: 86.
  • Firefox: 111.
  • Safari: 15.2.

Origen

Motivación

Cuando piensas en los archivos en tu computadora, probablemente piensas en una jerarquía de archivos: archivos organizados en carpetas que puedes explorar con el explorador de archivos de tu sistema operativo. Por ejemplo, en Windows, para un usuario llamado Tom, su lista de tareas pendientes puede encontrarse en C:\Users\Tom\Documents\ToDo.txt. En este ejemplo, ToDo.txt es el nombre del archivo, y Users, Tom y Documents son nombres de las carpetas. “C:” en Windows representa el directorio raíz de la unidad.

Es la forma tradicional de trabajar con archivos en la Web.

Para editar la lista de tareas pendientes en una aplicación web, sigue el siguiente flujo:

  1. El usuario sube el archivo a un servidor o lo abre en el cliente con <input type="file">.
  2. El usuario realiza los cambios y, luego, descarga el archivo resultante con un <a download="ToDo.txt> insertado que tú click() de manera programática a través de JavaScript.
  3. Para abrir carpetas, debes usar un atributo especial en <input type="file" webkitdirectory>, que, a pesar de su nombre de propiedad, es prácticamente compatible con todos los navegadores.

Una forma moderna de trabajar con archivos en la Web

Este flujo no representa la opinión que tienen los usuarios sobre la edición de archivos y significa que descargan copias de sus archivos de entrada. Por lo tanto, la API de File System Access introdujo tres métodos de selección (showOpenFilePicker(), showSaveFilePicker() y showDirectoryPicker()) que hacen exactamente lo que su nombre sugiere. Habilitan un flujo de la siguiente manera:

  1. Abre ToDo.txt con showOpenFilePicker() y obtén un objeto FileSystemFileHandle.
  2. Desde el objeto FileSystemFileHandle, obtén un File llamando al método getFile() del controlador del archivo.
  3. Modifica el archivo y, luego, llama a requestPermission({mode: 'readwrite'}) en el controlador.
  4. Si el usuario acepta la solicitud de permiso, guarda los cambios en el archivo original.
  5. Como alternativa, llama a showSaveFilePicker() y permite que el usuario elija un archivo nuevo. (Si el usuario elige un archivo abierto previamente, se reemplazará su contenido). Para guardar de manera repetida, puedes mantener el controlador de archivos, por lo que no tienes que volver a mostrar el diálogo para guardar el archivo.

Restricciones para trabajar con archivos en la Web

Los archivos y las carpetas a los que se puede acceder a través de estos métodos se alojan en lo que se conoce como el sistema de archivos visible para el usuario. Los archivos guardados de la Web, y específicamente los archivos ejecutables, están marcados con la marca de la web, por lo que existe una advertencia adicional que el sistema operativo puede mostrar antes de que se ejecute un archivo potencialmente peligroso. Como función de seguridad adicional, los archivos obtenidos de la Web también están protegidos por la Navegación segura, que, por razones de simplicidad y en el contexto de este artículo, se considera un análisis de virus basado en la nube. Cuando escribes datos en un archivo con la API de File System Access, las operaciones de escritura no se encuentran en su lugar, sino que usan un archivo temporal. El archivo en sí no se modifica, a menos que pase todas estas verificaciones de seguridad. Como puedes imaginar, este trabajo hace que las operaciones con archivos sean relativamente lentas, a pesar de las mejoras que se apliquen siempre que sea posible, por ejemplo, en macOS. Sin embargo, cada llamada a write() es independiente, por lo que, de forma interna, abre el archivo, busca un desplazamiento determinado y, por último, escribe datos.

Los archivos como la base del procesamiento

Al mismo tiempo, los archivos son una excelente manera de registrar datos. Por ejemplo, SQLite almacena bases de datos completas en un solo archivo. Otro ejemplo son los mipmaps, que se usan en el procesamiento de imágenes. Los mipmaps son secuencias de imágenes optimizadas y calculadas previamente. Cada una de ellas es una representación de menor resolución de la versión anterior, lo que permite realizar muchas operaciones, como hacer zoom más rápido. Entonces, ¿cómo pueden las aplicaciones web obtener los beneficios de los archivos, pero sin los costos de rendimiento del procesamiento de archivos basado en la Web? La respuesta es el sistema de archivos privados de origen.

El sistema de archivos privados visible para el usuario en comparación con el sistema de archivos privados de origen

A diferencia del sistema de archivos visible para el usuario que explora con el explorador de archivos del sistema operativo, con archivos y carpetas que puedes leer, escribir, mover y cambiar el nombre, el sistema de archivos privados de origen no está pensado para que los usuarios lo vean. Como su nombre lo indica, los archivos y las carpetas del sistema de archivos privados de origen son privados y, de manera más concreta, privados para el origen de un sitio. Escribe location.origin en la consola de Herramientas para desarrolladores para descubrir el origen de una página. Por ejemplo, el origen de la página https://developer.chrome.com/articles/ es https://developer.chrome.com (es decir, la parte /articles no es parte del origen). Para obtener más información sobre la teoría de orígenes, consulta Información sobre el concepto de "mismo sitio" y "same-origin". Todas las páginas que comparten el mismo origen pueden ver los mismos datos del sistema de archivos privados de origen, por lo que https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ puede ver los mismos detalles que el ejemplo anterior. Cada origen tiene su propio sistema de archivos privados de origen independiente, lo que significa que el sistema de archivos privados de origen de https://developer.chrome.com es completamente distinto del de, por ejemplo, https://web.dev. En Windows, el directorio raíz del sistema de archivos visible para el usuario es C:\\. El equivalente para el sistema de archivos privados de origen es un directorio raíz inicialmente vacío por origen al que se accede llamando al método asíncrono. navigator.storage.getDirectory() Para obtener una comparación del sistema de archivos visible para el usuario y el sistema de archivos privados de origen, consulta el siguiente diagrama. El diagrama muestra que, aparte del directorio raíz, todo lo demás es conceptualmente igual, con una jerarquía de archivos y carpetas para organizar y organizar según sea necesario para tus necesidades de datos y almacenamiento.

Diagrama del sistema de archivos visible para el usuario y del sistema de archivos privados de origen con dos jerarquías de archivos ejemplares. El punto de entrada del sistema de archivos visible para el usuario es un disco duro simbólico. El punto de entrada para el sistema de archivos privado de origen llama al método &quot;navigator.storage.getDirectory&quot;.

Especificaciones del sistema de archivos privados de origen

Al igual que otros mecanismos de almacenamiento en el navegador (por ejemplo, localStorage o IndexedDB), el sistema de archivos privados de origen está sujeto a las restricciones de cuota del navegador. Cuando un usuario borra todos los datos de navegación o todos los datos de sitios, también se borra el sistema de archivos privados de origen. Llama a navigator.storage.estimate() y, en el objeto de respuesta resultante, consulta la entrada usage para ver cuánto almacenamiento ya consume tu app, que se desglosa por mecanismo de almacenamiento en el objeto usageDetails, en el que quieres ver la entrada fileSystem específicamente. Como el usuario no puede ver el sistema de archivos privados de origen, no se muestran mensajes de permisos ni se verifican las verificaciones de la Navegación segura.

Cómo obtener acceso al directorio raíz

Para obtener acceso al directorio raíz, ejecuta el siguiente comando. El resultado será un controlador de directorio vacío, más específicamente, un FileSystemDirectoryHandle.

const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);

Subproceso principal o trabajador web

Existen dos maneras de usar el sistema de archivos privados de origen: en el subproceso principal o en un Web Worker. Los trabajadores web no pueden bloquear el subproceso principal, lo que significa que, en este contexto, las APIs pueden ser síncronas, un patrón que generalmente no está permitido en el subproceso principal. Las APIs síncronas pueden ser más rápidas, ya que evitan tener que lidiar con promesas, y las operaciones de archivos suelen ser síncronas en lenguajes como C que se pueden compilar en WebAssembly.

// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);

Si necesitas las operaciones de archivo más rápidas posibles o trabajas con WebAssembly, ve directamente a Usa el sistema de archivos privados de origen en un Web Worker. De lo contrario, puedes seguir leyendo.

Usa el sistema de archivos privados de origen en el subproceso principal

Crea nuevos archivos y carpetas

Una vez que tengas una carpeta raíz, crea archivos y carpetas con los métodos getFileHandle() y getDirectoryHandle(), respectivamente. Si pasas {create: true}, se creará el archivo o la carpeta si no existen. Construye una jerarquía de archivos llamando a estas funciones con un directorio recién creado como punto de partida.

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

La jerarquía de archivos resultante de la muestra de código anterior.

Cómo acceder a archivos y carpetas existentes

Si conoces su nombre, accede a archivos y carpetas creados con anterioridad llamando a los métodos getFileHandle() o getDirectoryHandle(), y pasando el nombre del archivo o la carpeta.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

Obtén el archivo asociado con un controlador de archivos para lectura

Un FileSystemFileHandle representa un archivo en el sistema de archivos. Para obtener el File asociado, usa el método getFile(). Un objeto File es un tipo específico de Blob y se puede usar en cualquier contexto que pueda usar un Blob. En particular, FileReader, URL.createObjectURL(), createImageBitmap() y XMLHttpRequest.send() aceptan Blobs y Files. Si puedes, obtener un File de FileSystemFileHandle "gratis" a los datos para que puedas acceder a ellos y ponerlos a disposición del sistema de archivos visible para el usuario.

const file = await fileHandle.getFile();
console.log(await file.text());

Escribir en un archivo mediante transmisión

Transmite datos a un archivo llamando a createWritable(), que crea un FileSystemWritableFileStream hacia ese archivo y, luego, write() el contenido. Al final, deberás close() la transmisión.

const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();

Borra archivos y carpetas

Borra archivos y carpetas llamando al método remove() particular del controlador de archivos o directorio. Para borrar una carpeta, incluidas todas las subcarpetas, pasa la opción {recursive: true}.

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

Como alternativa, si conoces el nombre del archivo o la carpeta que se borrará de un directorio, usa el método removeEntry().

directoryHandle.removeEntry('my first nested file');

Mover archivos y carpetas, y cambiarles el nombre

Cambia el nombre de archivos y carpetas y muévelos con el método move(). El movimiento y el cambio de nombre pueden ocurrir juntos o de forma aislada.

// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
    .move(nestedDirectoryHandle, 'my first renamed and now nested file');

Cómo resolver la ruta de un archivo o una carpeta

Para saber dónde se encuentra una carpeta o un archivo determinado en relación con un directorio de referencia, usa el método resolve() y pásale un FileSystemHandle como argumento. Para obtener la ruta completa de un archivo o una carpeta en el sistema de archivos privados de origen, usa el directorio raíz como directorio de referencia obtenido a través de navigator.storage.getDirectory().

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

Verifica si dos identificadores de archivos o carpetas apuntan al mismo archivo o carpeta.

A veces, tienes dos identificadores y no sabes si apuntan al mismo archivo o carpeta. Para verificar si este es el caso, usa el método isSameEntry().

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

Muestra el contenido de una carpeta

FileSystemDirectoryHandle es un iterador asíncrono que debes iterar con un bucle for await…of. Como iterador asíncrono, también admite los métodos entries(), values() y keys(), que puedes elegir según la información que necesites:

for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}

Cómo mostrar de forma recurrente el contenido de una carpeta y todas sus subcarpetas

Gestionar con funciones y bucles asíncronos combinados con recursividad es fácil equivocarse. La siguiente función puede servir como punto de partida para mostrar el contenido de una carpeta y todas sus subcarpetas, incluidos todos los archivos y sus tamaños. Puedes simplificar la función si no necesitas los tamaños de archivo, donde dice directoryEntryPromises.push, sin enviar la promesa handle.getFile(), sino directamente handle.

  const getDirectoryEntriesRecursive = async (
    directoryHandle,
    relativePath = '.',
  ) => {
    const fileHandles = [];
    const directoryHandles = [];
    const entries = {};
    // Get an iterator of the files and folders in the directory.
    const directoryIterator = directoryHandle.values();
    const directoryEntryPromises = [];
    for await (const handle of directoryIterator) {
      const nestedPath = `${relativePath}/${handle.name}`;
      if (handle.kind === 'file') {
        fileHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          handle.getFile().then((file) => {
            return {
              name: handle.name,
              kind: handle.kind,
              size: file.size,
              type: file.type,
              lastModified: file.lastModified,
              relativePath: nestedPath,
              handle
            };
          }),
        );
      } else if (handle.kind === 'directory') {
        directoryHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          (async () => {
            return {
              name: handle.name,
              kind: handle.kind,
              relativePath: nestedPath,
              entries:
                  await getDirectoryEntriesRecursive(handle, nestedPath),
              handle,
            };
          })(),
        );
      }
    }
    const directoryEntries = await Promise.all(directoryEntryPromises);
    directoryEntries.forEach((directoryEntry) => {
      entries[directoryEntry.name] = directoryEntry;
    });
    return entries;
  };

Cómo usar el sistema de archivos privados de origen en un Web Worker

Como se describió anteriormente, los trabajadores web no pueden bloquear el subproceso principal; por eso, en este contexto, se permiten los métodos síncronos.

Obtén un controlador de acceso síncrono

El punto de entrada para las operaciones de archivo más rápidas posibles es un FileSystemSyncAccessHandle, que se obtiene de un FileSystemFileHandle normal llamando a createSyncAccessHandle().

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

Métodos síncronos de archivos locales

Una vez que tengas un controlador de acceso síncrono, obtendrás acceso a métodos de archivo locales rápidos, todos síncronos.

  • getSize(): Muestra el tamaño del archivo en bytes.
  • write(): Escribe el contenido de un búfer en el archivo, opcionalmente en un desplazamiento determinado, y muestra la cantidad de bytes escritos. Verificar la cantidad de bytes escritos que se muestran permite que los emisores detecten y manejen errores y escrituras parciales.
  • read(): Lee el contenido del archivo en un búfer, opcionalmente en un desplazamiento determinado.
  • truncate(): Cambia el tamaño del archivo al tamaño determinado.
  • flush(): Garantiza que el contenido del archivo contenga todas las modificaciones realizadas a través de write().
  • close(): Cierra el controlador de acceso.

En este ejemplo, se usan todos los métodos mencionados anteriormente.

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

Copia un archivo del sistema de archivos privados de origen al sistema de archivos visible para el usuario

Como se mencionó anteriormente, no es posible mover archivos del sistema de archivos privados de origen al sistema de archivos visible para el usuario, pero sí puedes copiar archivos. Como showSaveFilePicker() solo se expone en el subproceso principal, pero no en el subproceso de trabajo, asegúrate de ejecutar el código allí.

// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
  // Obtain a file handle to a new file in the user-visible file system
  // with the same name as the file in the origin private file system.
  const saveHandle = await showSaveFilePicker({
    suggestedName: fileHandle.name || ''
  });
  const writable = await saveHandle.createWritable();
  await writable.write(await fileHandle.getFile());
  await writable.close();
} catch (err) {
  console.error(err.name, err.message);
}

Depura el sistema de archivos privados de origen

Hasta que se agregue compatibilidad con Herramientas para desarrolladores integrada (consulta crbug/1284595), usa la extensión de Chrome OPFS Explorer para depurar el sistema de archivos privados de origen. Por cierto, la captura de pantalla anterior de la sección Cómo crear nuevos archivos y carpetas se tomó directamente de la extensión.

Extensión de OPFS Explorer para las Herramientas para desarrolladores de Chrome en Chrome Web Store.

Después de instalar la extensión, abre las Herramientas para desarrolladores de Chrome y selecciona la pestaña OPFS Explorer. Podrás inspeccionar la jerarquía de archivos. Para guardar archivos del sistema de archivos privados de origen en el sistema de archivos visible para el usuario, haz clic en el nombre del archivo y, luego, haz clic en el ícono de la papelera para borrar archivos y carpetas.

Demostración

Observa el sistema de archivos privados de origen en acción (si instalas la extensión OPFS Explorer) en una demostración que lo usa como backend para una base de datos SQLite compilada en WebAssembly. Asegúrate de consultar el código fuente en Glitch. Observa que la versión incorporada que aparece a continuación no utiliza el backend del sistema de archivos privados de origen (porque el iframe es de origen cruzado), pero cuando abres la demostración en una pestaña independiente, sí lo hace.

Conclusiones

El sistema de archivos privados de origen, según lo que especifica el mensaje de QUÉWG, ha dado forma al modo en que usamos los archivos en la Web y cómo interactuamos con ellos. Ha habilitado nuevos casos de uso que eran imposibles de lograr con el sistema de archivos visible para el usuario. Todos los principales proveedores de navegadores (Apple, Mozilla y Google) se incorporan y comparten una visión conjunta. El desarrollo del sistema de archivos privados de origen es un esfuerzo colaborativo y los comentarios de los desarrolladores y los usuarios son esenciales para su progreso. Mientras seguimos perfeccionando y mejorando el estándar, aceptamos comentarios sobre el repositorio whatwg/fs en forma de problemas o solicitudes de extracción.

Agradecimientos

Austin Sully, Etienne Noël y Rachel Andrew revisaron este artículo. Hero image de Christina Rumpf en Unsplash.