API de Acceso al Sistema de Archivos en la Práctica: Arquitectura de Lectura/Escritura de Archivos Locales en el Navegador
La Evolución del Acceso a Archivos en el Navegador
| Enfoque | Lectura/Escritura | Persistencia | Modelo de Permisos | Rendimiento |
|---|---|---|---|---|
<input type="file"> |
Solo lectura | ❌ | Ninguno | Medio |
| File + FileReader | Solo lectura | ❌ | Ninguno | Medio |
| IndexedDB | Leer/escribir bytes | ✅ | Mismo origen | Bajo (serialización) |
| File System Access | Leer/escribir | ✅ | Concedido por el usuario | Alto |
| OPFS | Leer/escribir | ✅ | Aislamiento de mismo origen | El más alto |
La API de Acceso al Sistema de Archivos otorga a los navegadores verdaderas capacidades de lectura/escritura de archivos a nivel de aplicación nativa.
Selectores de Archivos: showOpenFilePicker / showSaveFilePicker
Abrir un Archivo
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: 'Image files',
accept: { 'image/*': ['.png', '.jpg', '.webp'] },
}],
multiple: false,
});
const file = await fileHandle.getFile();
const arrayBuffer = await file.arrayBuffer();
Guardar un Archivo
const handle = await window.showSaveFilePicker({
suggestedName: 'compressed.webp',
types: [{
description: 'WebP Image',
accept: { 'image/webp': ['.webp'] },
}],
});
const writable = await handle.createWritable();
await writable.write(compressedBlob);
await writable.close();
FileSystemFileHandle: Lectura/Escritura y Permisos
Leer el Contenido de un Archivo
const fileHandle: FileSystemFileHandle = handle;
const file = await fileHandle.getFile();
// Multiple read methods
const text = await file.text(); // Text
const buffer = await file.arrayBuffer(); // Binary
const stream = file.stream(); // Streaming
Escribir en un Archivo
async function writeFile(handle: FileSystemFileHandle, content: Blob | string | BufferSource) {
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}
Consulta y Solicitud de Permisos
async function verifyPermission(handle: FileSystemFileHandle, readWrite = true) {
const options: FileSystemHandlePermissionDescriptor = { mode: readWrite ? 'readwrite' : 'read' };
if ((await handle.queryPermission(options)) === 'granted') return true;
if ((await handle.requestPermission(options)) === 'granted') return true;
return false;
}
Modelo de permisos: Los handles obtenidos mediante el selector obtienen automáticamente permiso de lectura. El permiso de escritura requiere autorización explícita del usuario (requestPermission muestra un diálogo).
Acceso a Directorios y Recorrido
const dirHandle = await window.showDirectoryPicker();
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
const file = await handle.getFile();
console.log(`File: ${name}, Size: ${file.size}`);
} else if (handle.kind === 'directory') {
console.log(`Directory: ${name}`);
}
}
Búsqueda Recursiva de Archivos
async function* findFiles(dirHandle: FileSystemDirectoryHandle, pattern: RegExp) {
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file' && pattern.test(name)) {
yield { name, handle };
} else if (handle.kind === 'directory') {
yield* findFiles(handle, pattern);
}
}
}
// Find all PDF files
for await (const pdf of findFiles(dirHandle, /\.pdf$/)) {
console.log(pdf.name);
}
Origin Private File System (OPFS)
OPFS es un sistema de archivos privado y aislado proporcionado por el navegador para cada origen—no se necesita autorización del usuario:
const opfsRoot = await navigator.storage.getDirectory();
// Create a file
const fileHandle = await opfsRoot.getFileHandle('work-data.bin', { create: true });
// Create a subdirectory
const subDir = await opfsRoot.getDirectoryHandle('cache', { create: true });
// Delete a file
await opfsRoot.removeEntry('work-data.bin');
OPFS vs IndexedDB
| Característica | IndexedDB | OPFS |
|---|---|---|
| Tipo de almacenamiento | Datos estructurados | Archivos (binario) |
| Modo de lectura/escritura | Transacciones + serialización | E/S de archivo directa |
| Rendimiento con archivos grandes | Deficiente (sobrecarga de serialización) | Excelente |
| Acceso aleatorio | ❌ | ✅ (Access Handle) |
| Capacidad | Límite de mismo origen | Límite de mismo origen |
| Soporte en Workers | ✅ | ✅ |
Access Handle: Lectura/Escritura Aleatoria de Alto Rendimiento
Los archivos OPFS admiten createSyncAccessHandle(), que proporciona lectura/escritura de archivos síncrona y basada en posición—rendimiento cercano al sistema de archivos nativo:
const fileHandle = await opfsRoot.getFileHandle('large-data.bin', { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();
// Write data at a specific position
const writeBuffer = new Uint8Array([1, 2, 3, 4, 5]);
accessHandle.write(writeBuffer, { at: 0 });
// Read data at a specific position
const readBuffer = new Uint8Array(5);
accessHandle.read(readBuffer, { at: 0 });
// Get file size
const fileSize = accessHandle.getSize();
// Flush to disk
accessHandle.flush();
// Close handle
accessHandle.close();
Ventaja clave: Access Handle es síncrono, utilizable en Workers sin async/await—10-100 veces más rápido que WritableStream.
Práctica: Arquitectura de Lectura/Escritura de Archivos para Fusión de PDF
Fusión de PDF de ToolsKu utiliza la API de Acceso al Sistema de Archivos para un flujo de trabajo completo de "abrir → procesar → guardar":
async function mergePdfWorkflow() {
// 1. User selects multiple PDFs
const handles = await window.showOpenFilePicker({
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
multiple: true,
});
// 2. Read all files
const buffers: ArrayBuffer[] = [];
for (const handle of handles) {
const file = await handle.getFile();
buffers.push(await file.arrayBuffer());
}
// 3. Merge processing
const mergedPdf = await mergePdfs(buffers);
// 4. Save result
const saveHandle = await window.showSaveFilePicker({
suggestedName: 'merged.pdf',
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
});
const writable = await saveHandle.createWritable();
await writable.write(mergedPdf);
await writable.close();
}
Persistir Handles de Archivos
Guarde los handles en IndexedDB para restaurar el acceso en la próxima visita:
async function saveHandle(key: string, handle: FileSystemFileHandle) {
const db = await openDB('file-handles', 1, {
upgrade(db) { db.createObjectStore('handles'); },
});
await db.put('handles', handle, key);
}
async function loadHandle(key: string) {
const db = await openDB('file-handles', 1);
const handle: FileSystemFileHandle = await db.get('handles', key);
if (await verifyPermission(handle, true)) {
return handle;
}
return null;
}
Preguntas Frecuentes
¿Soporte del navegador para la API de Acceso al Sistema de Archivos?
Chrome/Edge 86+ tienen soporte completo. Firefox y Safari no la soportan. Se requiere detección de funcionalidades y manejo de alternativas.
¿Límites de capacidad de almacenamiento de OPFS?
OPFS comparte la cuota de almacenamiento de mismo origen con Cache API e IndexedDB (típicamente 50%+ del espacio disponible en disco). Consulte con navigator.storage.estimate().
¿Se puede usar Access Handle en el hilo principal?
Sí, pero la E/S síncrona bloquea el hilo principal. Mejor práctica: use Access Handle en un Worker para evitar bloquear la interfaz de usuario.
¿Cómo manejar la escritura de archivos grandes?
Use escritura fragmentada con WritableStream para evitar cargar todo el archivo en memoria:
const writable = await handle.createWritable();
for await (const chunk of readableStream) {
await writable.write(chunk);
}
await writable.close();
Resumen
La API de Acceso al Sistema de Archivos otorga a los navegadores capacidades de lectura/escritura de archivos a nivel de aplicación nativa. showOpenFilePicker/showSaveFilePicker proporcionan acceso a archivos autorizado por el usuario, FileSystemFileHandle gestiona los permisos de lectura/escritura, OPFS proporciona almacenamiento aislado sin autorización, y Access Handle permite lectura/escritura aleatoria síncrona de alto rendimiento. Esta es la infraestructura central para construir herramientas de procesamiento de archivos en el navegador.
Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →