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.
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:
- 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, 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:
- 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 control. - 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 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.
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});
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 dewrite()
.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.
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.
Vínculos relacionados
- Especificación estándar del sistema de archivos
- Repositorio estándar del sistema de archivos
- La API de File System con la publicación de WebKit del sistema de archivos privados de origen
- Extensión de OPFS Explorer
Agradecimientos
Austin Sully, Etienne Noël y Rachel Andrew revisaron este artículo. Imagen hero de Christina Rumpf en Unsplash.