El sistema de archivos privados de origen

El estándar del sistema 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

El sistema de archivos privado de origen es compatible con los navegadores modernos y está estandarizado por el Web Hypertext Application Technology Working Group (WHATWG) en el estándar activo del sistema de archivos.

Browser Support

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

Source

Motivación

Cuando piensas en archivos en 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. `C:` en Windows representa el directorio raíz de la unidad.

Forma típica 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 insertado <a download="ToDo.txt> que programas de forma programática click() con JavaScript.
  3. Para abrir carpetas, usas un atributo especial en <input type="file" webkitdirectory>, que, a pesar de su nombre propietario, tiene compatibilidad con navegadores prácticamente universal.

Forma moderna de trabajar con archivos en la Web

Este flujo no es representativo de cómo 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 introdujo tres métodos de selector:

Permiten un flujo de la siguiente manera:

  1. Abre ToDo.txt con showOpenFilePicker() y obtén un FileSystemFileHandle objeto.
  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 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 anteriormente, se sobrescribirá su contenido). Para guardar repeticiones, puedes mantener el controlador de archivos, de modo que no tengas 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 con 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 los archivos ejecutables específicamente, 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 Navegación segura, que, por motivos de simplicidad y en el contexto de este documento, 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 escrituras no se realizan en el lugar, sino que usan un archivo temporal. El archivo en sí no se modifica, a menos que pase todas estas verificaciones de seguridad.

Este trabajo hace que las operaciones de archivos sean relativamente lentas, a pesar de las mejoras aplicadas cuando es posible, por ejemplo, en macOS. Aun así, cada write() llamada es independiente, por lo que, en segundo plano, abre el archivo, busca el desplazamiento determinado y, por último, escribe datos.

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 precalculadas y optimizadas, 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 privado de origen.

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

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 privado 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. Obtén más información sobre la teoría de los orígenes en Comprende "same-site" y "same-origin".

Todas las páginas que comparten el mismo origen pueden ver los mismos datos del sistema de archivos privado 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 privado de origen 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 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 comparar 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 organizar según sea necesario para tus datos y necesidades de almacenamiento.

Diagrama del sistema de archivos visible para el usuario y el sistema de archivos privado del origen con dos jerarquías de archivos ejemplares.
El punto de entrada para el 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 es la llamada del método navigator.storage.getDirectory.

Detalles del sistema de archivos privado de origen

Al igual que otros mecanismos de almacenamiento en el navegador (por ejemplo, localStorage o IndexedDB), el sistema de archivos privado de 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 usage entrada para ver cuánto almacenamiento ya consume tu app, que se desglosa por mecanismo de almacenamiento en el usageDetails objeto, en el que deseas observar la fileSystem entrada específicamente. Como el sistema de archivos privado de origen no es visible para el usuario, no hay mensajes 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. 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 Web Worker

Hay dos formas de usar el sistema de archivos privado de origen: en el subproceso principal o en un Web Worker. Los Web Workers 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, ve a Usar el sistema de archivos privado de origen en un Web Worker.

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

Crea archivos y carpetas nuevos

Una vez que tengas una carpeta raíz, crea archivos y carpetas con los getFileHandle() y los getDirectoryHandle() métodos, respectivamente. Si pasas {create: true}, se creará el archivo o la carpeta si no existe. Crea 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.

Accede a archivos y carpetas existentes

Si conoces su nombre, accede a los archivos y carpetas creados anteriormente 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');

Cómo obtener el archivo asociado con un controlador de archivos para la lectura

Un FileSystemFileHandle representa un archivo en el sistema de archivos. Para obtener el asociado File, usa el getFile() método. Un objeto File es un tipo específico de Blob y se puede usar en cualquier contexto en el que se pueda usar un Blob.

En particular, FileReader, URL.createObjectURL(), createImageBitmap(), y XMLHttpRequest.send() aceptan Blobs y Files. Obtener un File de un FileSystemFileHandle "libera" los datos, por lo que puedes 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());

Escribe en un archivo mediante la transmisión

Transmite datos a un archivo llamando 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 particular remove() de su controlador de archivos o directorios. Para borrar una carpeta, incluidas todas las subcarpetas, pasa la {recursive: true} opción.

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 removeEntry() método.

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

Mueve y cambia el nombre de archivos y carpetas

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

Resuelve la ruta de acceso de un archivo o una carpeta

Para saber dónde se encuentra un archivo o una carpeta determinados en relación con un directorio de referencia, usa el resolve() método, 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 obtenido con 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 controladores y no sabes si apuntan al mismo archivo o carpeta. Para verificar si este es el caso, usa el isSameEntry() método.

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

Muestra el contenido de una carpeta

FileSystemDirectoryHandle es un iterador asíncrono sobre el que iteras con un bucle for await... of. Como iterador asíncrono, también admite los entries(), los values(), y los keys() métodos, 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 las subcarpetas

Es fácil equivocarse al tratar con bucles y funciones asíncronos combinados con la recursión. 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 handle.getFile() promesa, sino el 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 Web Worker

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

Cómo obtener un controlador 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 síncronos en el lugar

Una vez que tengas un controlador de acceso síncrono, obtendrás acceso a métodos de archivos rápidos en el 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. Verificar la cantidad de bytes escritos que se muestran permite a los llamadores detectar y controlar errores y escrituras 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 determinado.
  • flush(): Asegura que el contenido del archivo contenga todas las modificaciones realizadas a través de write().
  • close(): Cierra el controlador de acceso.

Este es un ejemplo que usa cada método.

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. Como showSaveFilePicker() solo se expone en el subproceso principal, pero no en el subproceso de trabajador, 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 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. Por cierto, la captura de pantalla de la sección Cómo crear archivos y carpetas se toma directamente de la extensión.

La extensión OPFS Explorer de Herramientas para desarrolladores de Chrome 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, luego, podrás 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 la papelera.

Demostración

Consulta el sistema de archivos privado 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 GitHub. Ten en cuenta que la versión incorporada no usa el backend del sistema de archivos privado de origen (porque el iframe es de origen cruzado), pero sí lo hace cuando abres la demostración en una pestaña separada.

Conclusión

El sistema de archivos privado de origen, como lo especifica WHATWG, dio forma a la manera en que usamos e interactuamos con los archivos en la Web. Permitió nuevos casos de uso que eran imposibles de lograr con el sistema de archivos visible para el usuario. Todos los proveedores de navegadores principales (Apple, Mozilla y Google) están a bordo y comparten una visión conjunta. El desarrollo del sistema de archivos privado de origen es un esfuerzo de colaboración, y los comentarios de los desarrolladores y usuarios son esenciales para su progreso.

A medida que seguimos refinando y mejorando el estándar, se agradecen los comentarios sobre el repositorio whatwg/fs en forma de problemas o solicitudes de extracción.

Agradecimientos

Este documento fue revisado por Austin Sully, Etienne Noël y Rachel Andrew.