El estándar del sistema de archivos introduce un sistema de archivos privado del 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 el rendimiento.
Navegadores compatibles
El sistema de archivos privados 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.
Motivación
Cuando piensas en los archivos de 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 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:
- El usuario sube el archivo a un servidor o lo abre en el cliente con
<input type="file">
. - El usuario realiza los cambios y, luego, descarga el archivo resultante con un
<a download="ToDo.txt>
insertado queclick()
de forma programática a través de JavaScript. - Para abrir carpetas, usas un atributo especial en
<input type="file" webkitdirectory>
que, a pesar de su nombre propietario, tiene compatibilidad con prácticamente todos los navegadores.
Una forma moderna de trabajar con archivos en la Web
Este flujo no representa cómo los usuarios piensan en la edición de archivos, lo que 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: showOpenFilePicker()
, showSaveFilePicker()
y showDirectoryPicker()
, que hacen exactamente lo que su nombre sugiere. Permiten un flujo de la siguiente manera:
- Abre
ToDo.txt
conshowOpenFilePicker()
y obtén un objetoFileSystemFileHandle
. - Desde el objeto
FileSystemFileHandle
, obtén unFile
llamando al métodogetFile()
del controlador de archivos. - Modifica el archivo y, luego, llama a
requestPermission({mode: 'readwrite'})
en el identificador. - Si el usuario acepta la solicitud de permiso, guarda los cambios en el archivo original.
- Como alternativa, llama a
showSaveFilePicker()
y permite que el usuario elija un archivo nuevo. (Si el usuario elige un archivo que ya abrió, se sobrescribirá su contenido). Para los guardados repetidos, puedes mantener el identificador de archivo, 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 a través de estos métodos se encuentran en lo que se puede denominar el sistema de archivos visible para el usuario. Los archivos guardados desde la Web, y los archivos ejecutables en particular, 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 escrituras no se realizan en el lugar, sino que se usa 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 cuando es posible, por ejemplo, en macOS. Sin embargo, cada llamada a write()
es independiente, por lo que, en segundo plano, abre el archivo, busca la compensación determinada y, finalmente, 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 precalculadas, cada una de las cuales es una representación con una resolución progresivamente menor 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 del origen.
El sistema de archivos privado de origen en comparación con el visible para el usuario
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 del origen no está diseñado para que lo vean los usuarios. Como sugiere el nombre, los archivos y las carpetas del sistema de archivos privados del origen 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 Herramientas para desarrolladores. 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 leer más sobre la teoría de los orígenes en Understanding "same-site" and "same-origin". 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 del origen independiente, lo que significa que el sistema de archivos privado del 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()
.
En el siguiente diagrama, se compara el sistema de archivos visible para el usuario y el sistema de archivos privado del origen. 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.
Detalles del sistema de archivos privados del origen
Al igual que otros mecanismos de almacenamiento en el navegador (por ejemplo, localStorage o IndexedDB), el sistema de archivos privado 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 borra el sistema de archivos privados del 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 debes consultar específicamente la entrada fileSystem
. Dado que el usuario no puede ver el sistema de archivos privado del origen, no hay mensajes de permisos ni verificaciones de Navegación segura.
Cómo obtener acceso al directorio raíz
Para acceder al directorio raíz, ejecuta el siguiente comando. Obtendrás 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
Existen dos formas de usar el sistema de archivos privado del 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 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 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 Cómo usar el sistema de archivos privado del origen en un Web Worker. De lo contrario, puedes seguir leyendo.
Cómo usar el sistema de archivos privado del origen en el subproceso principal
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. 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});
Accede a archivos y carpetas existentes
Si conoces su nombre, llama a los métodos getFileHandle()
o getDirectoryHandle()
y pasa el nombre del archivo o la carpeta para acceder a los archivos y las carpetas creados anteriormente.
const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
.getDirectoryHandle('my first folder');
Cómo obtener el archivo asociado a un identificador de archivo 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 en el que se pueda usar un Blob
. En particular, FileReader
, URL.createObjectURL()
, createImageBitmap()
y XMLHttpRequest.send()
aceptan Blobs
y Files
. Si lo haces, 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());
Cómo escribir en un archivo por 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();
Cómo borrar archivos y carpetas
Borra archivos y carpetas llamando al método remove()
particular de su identificador de archivo 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á en un directorio, usa el método removeEntry()
.
directoryHandle.removeEntry('my first nested file');
Cómo mover y cambiar el nombre de archivos y carpetas
Cambia el nombre de archivos y carpetas, y muévelos con el método move()
. El cambio de nombre y el movimiento pueden ocurrir 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 determinados 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 privados del 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`.
Enumera 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()
, 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()) {}
Enumera de forma recursiva el contenido de una carpeta y todas las subcarpetas
Es fácil equivocarse cuando se trabaja con bucles y funciones asíncronos combinados con recursión. 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 los archivos. Para ello, donde dice directoryEntryPromises.push
, no envíes la promesa handle.getFile()
, 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;
};
Cómo usar el sistema de archivos privado del origen en un Web Worker
Como se mencionó antes, los Web Workers 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 para 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 in situ
Una vez que tienes un identificador de acceso síncrono, obtienes acceso a métodos de archivos rápidos y locales que son todos síncronos.
getSize()
: Devuelve 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 devuelve la cantidad de bytes escritos. Verificar la cantidad de bytes escritos que se devolvió permite que las entidades que llaman detecten y controlen 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 indicado.flush()
: Garantiza que el contenido del archivo incluya todas las modificaciones realizadas a través dewrite()
.close()
: Cierra el identificador 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);
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 privados de origen al sistema de archivos visible para el usuario, pero sí puedes copiarlos. Dado que showSaveFilePicker()
solo se expone en el subproceso principal, pero no en el subproceso 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 privado del origen
Hasta que se agregue la compatibilidad integrada con las Herramientas para desarrolladores (consulta crbug/1284595), usa la extensión de Chrome OPFS Explorer para depurar el sistema de archivos privado del origen. Por cierto, la captura de pantalla anterior de la sección Creación de archivos y carpetas nuevos se tomó directamente de la extensión.
Después de instalar la extensión, abre las Herramientas para desarrolladores de Chrome, selecciona la pestaña OPFS Explorer y podrás inspeccionar la jerarquía de archivos. Haz clic en el nombre del archivo para guardarlo desde el sistema de archivos privado del origen en el sistema de archivos visible para el usuario y haz clic en el ícono de papelera para borrar archivos y carpetas.
Demostración
Mira el sistema de archivos privados del 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 cómo la versión incorporada que se muestra a continuación no usa el backend del sistema de archivos privado del origen (porque el iframe es de origen cruzado), pero sí lo hace cuando abres la demostración en una pestaña separada.
Conclusiones
El sistema de archivos privados de origen, según lo especifica WHATWG, moldeó la forma en que usamos los archivos en la Web y cómo interactuamos con ellos. Esto habilitó 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 a bordo 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 fundamentales para su progreso. A medida que seguimos definiendo y mejorando el estándar, agradecemos los comentarios sobre el repositorio whatwg/fs en forma de problemas o solicitudes de extracción.
Vínculos relacionados
- Especificación estándar del sistema de archivos
- Repositorio estándar del sistema de archivos
- Publicación de WebKit sobre la API de File System con Origin Private File System
- Extensión OPFS Explorer
Agradecimientos
Austin Sully, Etienne Noël y Rachel Andrew revisaron este artículo. Imagen hero de Christina Rumpf en Unsplash.