El sistema de archivos privados de origen

El estándar de sistema de archivos presenta un sistema de archivos privados de origen (OPFS) como un extremo de almacenamiento privado al origen de la página y no visible para el usuario, que proporciona acceso opcional a un tipo especial de archivo altamente optimizado para el 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 (WHWG) del File System Living Standard.

Navegadores compatibles

  • 86
  • 86
  • 111
  • 15.2

Origen

Motivación

Cuando piensas en archivos de tu computadora, es probable que 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 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 los nombres de las carpetas. En Windows, “C:” 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 estos pasos:

  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ú de manera programática click() 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, tiene una compatibilidad prácticamente universal con los navegadores.

Es una forma moderna de trabajar con archivos en la Web.

Este flujo no representa la forma en que los usuarios piensan sobre editar 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 selector, 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, llama al método getFile() del controlador del archivo para obtener un File.
  3. Modifica el archivo y, luego, llama a requestPermission({mode: 'readwrite'}) en el controlador.
  4. Si el usuario acepta la solicitud de permiso, vuelve a guardar 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 reemplazará su contenido). Para guardar el archivo de forma repetida, puedes conservar el controlador de archivo, de modo que no tengas que volver a mostrar el diálogo de guardado del archivo.

Restricciones para trabajar con archivos en la Web

Los archivos y las carpetas a los que se puede acceder con estos métodos se alojan en lo que se puede llamar el sistema de archivos visibles 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 hay 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 cuestiones de simplicidad y en el contexto de este artículo, se puede considerar 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, pero usan un archivo temporal. El archivo en sí no se modifica, a menos que pase todos estos controles de seguridad. Como puedes imaginar, este trabajo hace que las operaciones de archivos sean relativamente lentas, a pesar de las mejoras aplicadas siempre que sea posible, por ejemplo, en macOS. Cada llamada a write() es independiente, por lo que, de forma interna, abre el archivo, busca el 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 la resolución anterior con una resolución cada vez más baja, lo que agiliza muchas operaciones, como el zoom. 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.

Comparación entre el sistema de archivos privados de origen y el visible para el usuario

A diferencia del sistema de archivos visible para el usuario explorado mediante 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. Los archivos y las carpetas del sistema de archivos privados de origen, como su nombre lo indica, son privados y, más concretamente, son 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 forma parte del origen). Puedes obtener más información sobre la teoría de los orígenes en Información sobre el "mismo sitio" y el "mismo-origen". 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. Esto significa que el sistema de archivos privados de origen de https://developer.chrome.com es completamente distinto del de 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 mediante una llamada al método asíncrono navigator.storage.getDirectory(). Si deseas ver una comparación entre el 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 de ejemplo. El punto de entrada para el sistema de archivos visible para el usuario es un disco duro simbólico, y el punto de entrada para el sistema de archivos privados de origen llama al método “navigator.storage.getDirectory”.

Especificaciones del sistema de archivos privados de origen

Al igual que otros mecanismos de almacenamiento del 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 del sitio, también se borra el sistema de archivos privados de origen. Llama a navigator.storage.estimate() y, en el objeto de respuesta resultante, observa 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 específicamente la entrada de fileSystem. Dado que el sistema de archivos privados de origen no es visible para el usuario, no se muestran solicitudes de permisos ni verificaciones de Navegación segura.

Obtén acceso al directorio raíz

Para obtener acceso al directorio raíz, ejecuta el siguiente comando. Terminas con 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

Hay dos maneras de usar el sistema de archivos privados 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 generalmente no se permite en el subproceso principal. Las APIs síncronas pueden ser más rápidas, ya que evitan tener que lidiar con las promesas. Además, 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, ve a Usar el sistema de archivos privados de origen en un Web Worker. De lo contrario, sigue 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 aún no existen. Genera 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 los archivos y carpetas creados anteriormente mediante una llamada a los métodos getFileHandle() o getDirectoryHandle(), 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 controlador de archivo para leer

Un FileSystemFileHandle representa un archivo en el sistema. 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 hacer un Blob. En particular, FileReader, URL.createObjectURL(), createImageBitmap() y XMLHttpRequest.send() aceptan Blobs y Files. Si lo deseas, la obtención de 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 y, 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() específico del controlador de archivos o directorios. Para borrar una carpeta que incluya todas las subcarpetas, pasa la opción {recursive: true}.

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

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

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

Mueve archivos y carpetas, y cambia sus nombres

Cambia el nombre de archivos y carpetas y muévelos con el método move(). El traslado 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');

Resolver la ruta de un archivo o carpeta

Para saber dónde se encuentra un archivo o una carpeta determinados en relación con un directorio de referencia, usa el método resolve() y pásale un elemento FileSystemHandle como argumento. Para obtener la ruta de acceso 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 controladores 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 se itera con un bucle for await…of. Como iterador asíncrono, también admite los métodos entries(), values() y keys(), los cuales 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 una lista recursiva del contenido de una carpeta y de todas las subcarpetas

Tratar con bucles y funciones asíncronos vinculados con la recursividad es fácil de 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;
  };

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

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

Obtén un controlador de acceso síncrono

El punto de entrada a 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 de archivos locales síncronos

Una vez que tengas un controlador de acceso síncrono, podrás acceder a métodos de archivos 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 muestra 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 incluya todas las modificaciones realizadas mediante write().
  • close(): Cierra el controlador de acceso.

A continuación, se muestra un ejemplo en el que 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);

Copiar 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 puedes copiar archivos. Dado que showSaveFilePicker() solo se expone en el subproceso principal, pero no en el de Worker, 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 integrada con Herramientas para desarrolladores (consulta crbug/1284595), usa la extensión de Chrome OPFS Explorer para depurar el sistema de archivos privados de origen. La captura de pantalla anterior de la sección Cómo crear archivos y carpetas nuevos se toma directamente de la extensión.

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

Después de instalar la extensión, abre las Herramientas para desarrolladores de Chrome, selecciona la pestaña Explorador de OPFS y estará todo listo para 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 los archivos y las 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. Ten en cuenta que 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 cuando abres la demostración en otra pestaña, sí.

Conclusiones

El sistema de archivos privados de origen, tal como lo especifica WHG, ha definido la manera en que usamos e interactuamos con los archivos en la web. 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, están integrados y comparten una visión conjunta. El desarrollo del sistema de archivos privados de origen es en gran medida un esfuerzo colaborativo, y los comentarios de los desarrolladores y usuarios son esenciales para su progreso. A medida que continuamos perfeccionando y mejorando el estándar, aceptamos comentarios sobre el repositorio de 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.