El sistema de archivos privados de origen

El estándar de sistemas de archivos presenta un sistema de archivos privado 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 que está altamente optimizado para el rendimiento.

Navegadores compatibles

Los navegadores modernos admiten el sistema de archivos privados de origen, que el Grupo de trabajo de tecnología de aplicaciones de hipertexto web (WHATWG) estandariza en el estándar dinámico de sistemas de archivos.

Navegadores compatibles

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

Origen

Motivación

Cuando piensas en los archivos de tu computadora, probablemente pienses 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 podría estar 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 carpetas. En Windows, “C:” representa el directorio raíz de la unidad.

Forma tradicional de trabajar con archivos en la Web

Para editar la lista de tareas pendientes en una aplicación web, este es el flujo habitual:

  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 click() de forma programática a través de JavaScript.
  3. Para abrir carpetas, usa un atributo especial en <input type="file" webkitdirectory>, que, a pesar de su nombre propietario, tiene compatibilidad prácticamente universal con los navegadores.

Una forma moderna de trabajar con archivos en la Web

Este flujo no representa la forma en que los usuarios piensan en editar archivos y significa que los usuarios terminan con copias descargadas de sus archivos de entrada. Por lo tanto, la API de File System Access presentó tres métodos de selector: showOpenFilePicker(), showSaveFilePicker() y showDirectoryPicker(), que hacen exactamente lo que su nombre sugiere. Permiten 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 de archivos.
  3. Modifica el archivo y, luego, llama a requestPermission({mode: 'readwrite'}) en el control.
  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 anteriormente, se sobrescribirá su contenido). Para guardar archivos de forma repetida, puedes mantener el identificador de archivo para no tener que volver a mostrar el diálogo de guardado de archivos.

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 encuentran en lo que se puede llamar el sistema de archivos visible para el usuario. Los archivos guardados desde la Web, y en especial los ejecutables, se marcan con la marca de la Web, por lo que el sistema operativo puede mostrar una advertencia adicional 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, para simplificar y en el contexto de este artículo, puedes considerar como 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 realizan en el lugar, sino que usan un archivo temporal. El archivo en sí no se modifica, a menos que supere todas estas verificaciones de seguridad. Como puedes imaginar, este trabajo hace que las operaciones de archivos sean relativamente lentas, a pesar de las mejoras aplicadas siempre que es posible, por ejemplo, en macOS. Sin embargo, cada llamada a write() es independiente, por lo que, en segundo plano, abre el archivo, busca el desplazamiento determinado y, por último, escribe los datos.

Los archivos como 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 las cuales es una representación de resolución progresivamente más baja de la anterior, lo que hace que muchas operaciones, como el zoom, sean más rápidas. 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 privado visible para el usuario en comparación con el original

A diferencia del sistema de archivos visible para el usuario que se explora con el explorador de archivos del sistema operativo, con archivos y carpetas que puedes leer, escribir, mover y cambiar de nombre, el sistema de archivos privado de origen no está diseñado para que los usuarios lo vean. Los archivos y las carpetas del sistema de archivos privados de origen, como su nombre lo indica, son privados y, más concretamente, privados para el origen de un sitio. Para descubrir el origen de una página, escribe location.origin en la consola de DevTools. Por ejemplo, el origen de la página https://developer.chrome.com/articles/ es https://developer.chrome.com (es decir, la parte /articles no forma parte del origen). Puedes obtener más información sobre la teoría de los orígenes en Información sobre "mismo sitio" y "mismo origen". Todas las páginas que comparten el mismo origen pueden ver los mismos datos del sistema de archivos privados del origen, por lo que https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ puede ver los mismos detalles que en el ejemplo anterior. Cada origen tiene su propio sistema de archivos privado independiente, lo que significa que el sistema de archivos privado 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 privado del 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 ver una comparación entre el sistema de archivos visible para el usuario y el sistema de archivos privado 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 ordenar según sea necesario para tus necesidades de datos y almacenamiento.

Diagrama del sistema de archivos visible para el usuario y el sistema de archivos privado 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, y el punto de entrada del sistema de archivos privado de origen llama al método &quot;navigator.storage.getDirectory&quot;.

Detalles 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 del origen está sujeto a restricciones de cuota del navegador. Cuando un usuario borra todos los datos de navegación o todos los datos del sitio, también se borrará el sistema de archivos privado 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 deseas ver la entrada fileSystem específicamente. Dado que el usuario no puede ver el sistema de archivos privado de origen, no hay solicitudes de permisos ni verificaciones de Navegación segura.

Cómo obtener acceso al directorio raíz

Para obtener acceso al directorio raíz, ejecuta el siguiente comando. Obtendrás un identificador 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 Web Worker

Existen dos formas de usar el sistema de archivos privado de origen: en el subproceso principal o en un trabajador web. 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, por lo general, no se permite 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 archivos más rápidas posibles o trabajas con WebAssembly, desplázate hasta Cómo usar el sistema de archivos privado de origen en un trabajador web. De lo contrario, puedes seguir leyendo.

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

Cómo crear archivos y carpetas nuevos

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. Para crear una jerarquía de archivos, llama 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 del ejemplo de código anterior.

Cómo acceder a archivos y carpetas existentes

Si conoces su nombre, llama a los métodos getFileHandle() o getDirectoryHandle() para acceder a los archivos y carpetas creados anteriormente y pasa el nombre del archivo o la carpeta.

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

Cómo obtener el archivo asociado con un identificador de archivo para leerlo

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 lo deseas, obtener un File de un FileSystemFileHandle “libera” los datos, de modo 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());

Cómo escribir en un archivo mediante transmisión

Para transmitir datos a un archivo, llama a createWritable(), que crea un FileSystemWritableFileStream al que luego write() el contenido. Al final, debes 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

Para borrar archivos y carpetas, llama al método remove() particular del control de archivo o directorio. Para borrar una carpeta con todas sus 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á en un directorio, usa el método removeEntry().

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

Cómo 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 se pueden realizar juntos o por separado.

// 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 acceso de un archivo o una carpeta

Para saber dónde se encuentra un archivo o una carpeta en relación con un directorio de referencia, usa el método resolve() y pásale un FileSystemHandle como argumento. Para obtener la ruta de acceso completa de un archivo o una carpeta en el sistema de archivos privado de origen, usa el directorio raíz como el directorio de referencia que se obtiene 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 iteras con un bucle for await…of. Como iterador asíncrono, también admite los métodos entries(), values() y keys(), entre los 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()) {}

Muestra de forma recursiva el contenido de una carpeta y todas sus subcarpetas

Es fácil cometer errores cuando se trabaja con bucles y funciones asíncronos combinados con la recursividad. La siguiente función puede servir como punto de partida para enumerar 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. Para ello, donde dice directoryEntryPromises.push, no envíes la promesa handle.getFile(), sino handle directamente.

  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;
  };

Usa el sistema de archivos privado de origen en un trabajador web

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

Cómo obtener un identificador de acceso síncrono

El punto de entrada a las operaciones de archivos 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 de archivos in situ síncronos

Una vez que tengas un identificador de acceso síncrono, obtendrás acceso a métodos de archivos rápidos en su lugar que son todos síncronos.

  • getSize(): Muestra el tamaño del archivo en bytes.
  • write(): Escribe el contenido de un búfer en el archivo, de forma opcional en un desplazamiento determinado, y muestra la cantidad de bytes escritos. La verificación de la cantidad de bytes escritos que se muestra permite que los llamadores detecten y controlen errores y operaciones de escritura parciales.
  • read(): Lee el contenido del archivo en un búfer, de forma opcional, en un desplazamiento determinado.
  • truncate(): Cambia el tamaño del archivo al tamaño especificado.
  • flush(): Garantiza que el contenido del archivo contenga todas las modificaciones realizadas a través de write().
  • close(): Cierra el identificador de acceso.

Este es un ejemplo que usa 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 privado de origen al sistema de archivos visible para el usuario

Como se mencionó anteriormente, no es posible mover archivos del sistema de archivos privado de origen al sistema de archivos visible para el usuario, pero puedes copiarlos. Dado que 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);
}

Cómo depurar el sistema de archivos privado de origen

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

La extensión de herramientas para desarrolladores de Chrome de OPFS Explorer en Chrome Web Store.

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

Demostración

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

Conclusiones

El sistema de archivos privados de origen, como lo especifica WHATWG, ha definido la forma en que usamos y también interactuamos con los archivos en la Web. Habilita nuevos casos de uso que eran imposibles de lograr con el sistema de archivos visible para el usuario. Todos los proveedores principales de navegadores (Apple, Mozilla y Google) participan 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. A medida que seguimos definiendo y mejorando el estándar, recibimos con gusto los 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. Imagen hero de Christina Rumpf en Unsplash.