Cómo la AWP de Kiwix permite a los usuarios almacenar gigabytes de datos de Internet para usarlos sin conexión

Varias personas se reúnen alrededor de una laptop que está sobre una mesa sencilla con una silla de plástico a la izquierda. El fondo parece una escuela de un país en desarrollo.

En este caso de éxito, se explora cómo Kiwix, una organización sin fines de lucro, usa la tecnología de apps web progresivas y la API de acceso al sistema de archivos para permitir que los usuarios descarguen y almacenen grandes archivos de Internet para usarlos sin conexión. Obtén información sobre la implementación técnica del código que se relaciona con el sistema de archivos privados de origen (OPFS), una nueva función del navegador dentro de la AWP de Kiwix que mejora la administración de archivos y proporciona un mejor acceso a los archivos sin solicitudes de permisos. En el artículo, se analizan los desafíos y se destacan los posibles desarrollos futuros en este nuevo sistema de archivos.

Información acerca de Kiwix

Más de 30 años después del nacimiento de la Web, un tercio de la población mundial aún espera un acceso confiable a Internet, según la Unión Internacional de Telecomunicaciones. ¿Es aquí donde termina la historia? Por supuesto que no. El equipo de Kiwix, una organización sin fines de lucro con sede en Suiza, desarrolló un ecosistema de apps y contenido de código abierto que tiene como objetivo poner el conocimiento a disposición de las personas que tienen acceso limitado a Internet o no tienen acceso. La idea es que, si no puedes acceder a Internet con facilidad, alguien puede descargar recursos clave por ti, dónde y cuándo haya conectividad disponible, y almacenarlos de forma local para usarlos sin conexión más adelante. Muchos sitios vitales, por ejemplo, Wikipedia, Project Gutenberg, Stack Exchange o incluso las charlas de TED, ahora se pueden convertir en archivos altamente comprimidos, llamados archivos ZIM, y leerse sobre la marcha con el navegador Kiwix.

Los archivos ZIM usan compresión Zstandard (ZSTD) altamente eficiente (las versiones anteriores usaban XZ), principalmente para almacenar HTML, JavaScript y CSS, mientras que las imágenes suelen convertirse al formato WebP comprimido. Cada ZIM también incluye una URL y un índice de títulos. La compresión es clave aquí, ya que toda la Wikipedia en inglés (6.4 millones de artículos, más imágenes) se comprime a 97 GB después de la conversión al formato ZIM, lo que parece mucho hasta que te das cuenta de que la suma de todo el conocimiento humano ahora puede caber en un teléfono Android de gama media. También se ofrecen muchos recursos más pequeños, incluidas versiones temáticas de Wikipedia, como matemáticas, medicina, etcétera.

Kiwix ofrece una variedad de apps nativas para computadoras (Windows/Linux/macOS) y dispositivos móviles (iOS/Android). Sin embargo, este caso de éxito se enfocará en la app web progresiva (AWP), que tiene como objetivo ser una solución universal y simple para cualquier dispositivo que tenga un navegador moderno.

Analizaremos los desafíos que plantea el desarrollo de una app web universal que debe proporcionar acceso rápido a grandes archivos de contenido completamente sin conexión y algunas APIs de JavaScript modernas, en particular la API de acceso al sistema de archivos y el sistema de archivos privados de origen, que proporcionan soluciones innovadoras y emocionantes para esos desafíos.

¿Una app web para usar sin conexión?

Los usuarios de Kiwix son un grupo ecléctico con muchas necesidades diferentes, y Kiwix tiene poco o ningún control sobre los dispositivos y sistemas operativos en los que accederán a su contenido. Es posible que algunos de estos dispositivos sean lentos o estén desactualizados, especialmente en áreas de bajos ingresos del mundo. Si bien Kiwix intenta abarcar la mayor cantidad posible de casos de uso, la organización también se dio cuenta de que podría llegar a aún más usuarios si usaba el software más universal en cualquier dispositivo: el navegador web. Por lo tanto, inspirados en la Ley de Atwood, que establece que cualquier aplicación que se pueda escribir en JavaScript, con el tiempo, se escribirá en JavaScript, hace alrededor de 10 años, algunos desarrolladores de Kiwix se propusieron portar el software de Kiwix de C++ a JavaScript.

La primera versión de este puerto, llamada Kiwix HTML5, era para Firefox OS, que ya no existe, y para extensiones de navegador. En su núcleo, era (y es) un motor de descompresión de C++ (XZ y ZSTD) compilado en el lenguaje intermedio de JavaScript de ASM.js y, más tarde, Wasm, o WebAssembly, con el compilador Emscripten. Más tarde, se cambió el nombre a Kiwix JS, y las extensiones del navegador aún se desarrollan de forma activa.

Navegador sin conexión de Kiwix JS

Ingresa a la app web progresiva (AWP). Cuando se dieron cuenta del potencial de esta tecnología, los desarrolladores de Kiwix compilaron una versión de PWA dedicada de Kiwix JS y comenzaron a agregar integraciones de SO que permitieran que la app ofreciera capacidades similares a las nativas, en particular en las áreas de uso sin conexión, instalación, manejo de archivos y acceso al sistema de archivos.

Las AWP que priorizan el modo sin conexión son muy livianas y, por lo tanto, son perfectas para contextos en los que hay Internet móvil intermitente o costosa. La tecnología detrás de esto es la API de Service Worker y la API de Cache relacionada, que usan todas las apps basadas en Kiwix JS. Estas APIs permiten que las apps actúen como un servidor, intercepten las solicitudes de recuperación del documento o artículo principal que se está viendo y las redireccionen al backend (JS) para extraer y construir una respuesta del archivo ZIM.

Almacenamiento en todas partes

Dado el gran tamaño de los archivos ZIM, el almacenamiento y el acceso a ellos, en especial en dispositivos móviles, es probablemente el mayor dolor de cabeza de los desarrolladores de Kiwix. Muchos usuarios finales de Kiwix descargan contenido en la app, cuando hay Internet disponible, para usarlo sin conexión más adelante. Otros usuarios descargan contenido en una PC con un torrent y, luego, lo transfieren a un dispositivo móvil o una tablet. Algunos intercambian contenido en memorias USB o discos duros portátiles en áreas con Internet móvil irregular o costosa. Kiwix JS y Kiwix PWA deben admitir todas estas formas de acceder al contenido desde ubicaciones arbitrarias a las que el usuario pueda acceder.

En un principio, lo que permitió que Kiwix JS leyera archivos enormes, de cientos de GB (uno de nuestros archivos ZIM es de 166 GB), incluso en dispositivos con poca memoria, fue la API de File. Esta API es compatible de forma universal con cualquier navegador, incluso con navegadores muy antiguos, por lo que actúa como resguardo universal cuando no se admiten APIs más nuevas. Es tan fácil como definir un elemento input en HTML, en el caso de Kiwix:

<input
  type="file"
  accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac, ..."
  value="Select folder with ZIM files"
  id="archiveFilesLegacy"
  multiple
/>

Una vez seleccionado, el elemento de entrada contiene los objetos File, que son, en esencia, metadatos que hacen referencia a los datos subyacentes en el almacenamiento. Técnicamente, el backend orientado a objetos de Kiwix, escrito en JavaScript puro del cliente, lee pequeñas porciones del gran archivo según sea necesario. Si esas secciones deben descomprimirse, el backend las pasa al descompresor de Wasm y obtiene más secciones si se solicita, hasta que se descomprime un blob completo (por lo general, un artículo o un activo). Esto significa que el archivo grande nunca se debe leer por completo en la memoria.

A pesar de ser universal, la API de File tiene un inconveniente que hace que las apps de Kiwix JS parezcan torpes y anticuadas en comparación con las nativas: requiere que el usuario elija archivos con un selector de archivos o arrastre y suelte un archivo en la app, cada vez que se inicia la app, porque con esta API, no hay forma de conservar los permisos de acceso de una sesión a la siguiente.

Para mitigar esta UX deficiente, al igual que muchos desarrolladores, los desarrolladores de Kiwix JS optaron inicialmente por la ruta de Electron. ElectronJS es un marco de trabajo asombroso que proporciona funciones potentes, incluido el acceso completo al sistema de archivos con las APIs de Node. Sin embargo, tiene algunas desventajas conocidas:

  • Solo se ejecuta en sistemas operativos de computadoras.
  • Es grande y pesado (entre 70 MB y 100 MB).

El tamaño de las apps de Electron, debido a que se incluye una copia completa de Chromium con cada app, se compara de manera muy desfavorable con los solo 5.1 MB de la AWP minimizada y empaquetada.

Entonces, ¿había alguna manera en que Kiwix pudiera mejorar la situación de los usuarios de la AWP?

La API de File System Access al rescate

Alrededor de 2019, Kiwix se enteró de una API emergente que estaba en una prueba de origen en Chrome 78, que luego se llamó API de Native File System. Prometía la posibilidad de obtener un identificador de archivo para un archivo o una carpeta y almacenarlo en una base de datos de IndexedDB. Es fundamental que este identificador persista entre las sesiones de la app, de modo que el usuario no se vea obligado a volver a elegir el archivo o la carpeta cuando reinicie la app (aunque sí debe responder un mensaje de permiso rápido). Cuando llegó a producción, se le cambió el nombre a API de File System Access, y las partes principales se estandarizaron como API de File System (FSA) por parte de WHATWG.

Entonces, ¿cómo funciona la parte de File System Access de la API? Ten en cuenta los siguientes aspectos importantes:

  • Es una API asíncrona (excepto por las funciones especializadas en Web Workers).
  • Los selectores de archivos o directorios se deben iniciar de forma programática capturando un gesto del usuario (hacer clic o presionar un elemento de la IU).
  • Para que el usuario vuelva a otorgar permiso para acceder a un archivo elegido anteriormente (en una sesión nueva), también se necesita un gesto del usuario. De hecho, el navegador se negará a mostrar el mensaje de permiso si no se inicia con un gesto del usuario.

El código es relativamente sencillo, aparte de tener que usar la engorrosa API de IndexedDB para almacenar los controladores de archivos y directorios. La buena noticia es que hay un par de bibliotecas que hacen gran parte del trabajo pesado por ti, como browser-fs-access. En Kiwix JS, decidimos trabajar directamente con las APIs, que están muy bien documentadas.

Cómo abrir selectores de archivos y directorios

La apertura de un selector de archivos se ve de la siguiente manera (aquí se usan promesas, pero si prefieres la sintaxis de async/await, consulta el instructivo de Chrome para desarrolladores):

return window
  .showOpenFilePicker({ multiple: false })
  .then(function (fileHandles) {
    return processFileHandle(fileHandles[0]);
  })
  .catch(function (err) {
    // This is normal if app is launching
    console.warn(
      'User cancelled, or cannot access fs without user gesture',
      err,
    );
  });

Ten en cuenta que, por motivos de simplicidad, este código solo procesa el primer archivo elegido (y prohíbe elegir más de uno). En caso de que desees permitir la selección de varios archivos con { multiple: true }, simplemente une todas las promesas que procesan cada control en una sentencia Promise.all().then(...), por ejemplo:

let promisesForFiles = fileHandles.map(function (fileHandle) {
    return processFileHandle(fileHandle);
});
return Promise.all(promisesForFiles).then(function (arrayOfFiles) {
    // Do something with the files array
    console.log(arrayOfFiles);
}).catch(function (err) {
    // Handle any errors that occurred during processing
    console.error('Error processing file handles!', err);
)};

Sin embargo, es mejor elegir varios archivos pidiéndole al usuario que elija el directorio que contiene esos archivos en lugar de los archivos individuales, en especial, porque los usuarios de Kiwix tienden a organizar todos sus archivos ZIM en el mismo directorio. El código para iniciar el selector de directorios es casi el mismo que el anterior, excepto que usas window.showDirectoryPicker.then(function (dirHandle) { … });.

Procesa el identificador de archivo o directorio

Una vez que tengas el control, debes procesarlo, por lo que la función processFileHandle podría verse de la siguiente manera:

function processFileHandle(fileHandle) {
  // Serialize fileHandle to indexedDB
  serializeFSHandletoIdxDB('pickedFSHandle', fileHandle, function (val) {
    console.debug('IndexedDB responded with ' + val);
  });
  return fileHandle.getFile().then(function (file) {
    // Do something with the file
    return file;
  });
}

Ten en cuenta que debes proporcionar la función para almacenar el identificador de archivo. No hay métodos convenientes para esto, a menos que uses una biblioteca de abstracción. La implementación de Kiwix de esto se puede ver en el archivo cache.js, pero se podría simplificar considerablemente si solo se usara para almacenar y recuperar un identificador de archivo o carpeta.

El procesamiento de directorios es un poco más complicado, ya que debes iterar por las entradas del directorio elegido con entries.next() asíncrono para encontrar los archivos o tipos de archivos que deseas. Existen varias formas de hacerlo, pero este es el código que se usa en la AWP de Kiwix, en forma de esquema:

let iterableEntryList = dirHandle.entries();
return iterateAsyncDirEntries(iterableEntryList, []).then(function (entryList) {
  // Do something with the entry list
  return entryList;
});

/**
 * Iterates FileSystemDirectoryHandle iterator and adds entries to an array
 * @param {Iterator} entries An asynchronous iterator of entries
 * @param {Array} archives An array to which to add the entries (may be empty)
 * @return {Promise<Array>} A Promise for an array of entries in the directory
 */
function iterateAsyncDirEntries(entries, archives) {
  return entries
    .next()
    .then(function (result) {
      if (!result.done) {
        let entry = result.value[1];
        // Filter for the files you want
        if (/\.zim(\w\w)?$/i.test(entry.name)) {
          archives.push(entry);
        }
        return iterateAsyncDirEntryArray(entries, archives);
      } else {
        // We've processed all the entries
        if (!archives.length) {
          console.warn('No archives found in the picked directory!');
        }
        return archives;
      }
    })
    .catch(function (err) {
      console.error('There was an error processing the directory!', err);
    });
}

Ten en cuenta que, para cada entrada en entryList, más adelante deberás obtener el archivo con entry.getFile().then(function (file) { … }) cuando lo necesites, o el equivalente con const file = await entry.getFile() en un async function.

¿Podemos ir más allá?

El requisito de que el usuario otorgue permiso iniciado con un gesto del usuario en los inicios posteriores de la app agrega una pequeña cantidad de fricción a la apertura (nueva) de archivos y carpetas, pero sigue siendo mucho más fluida que tener que volver a elegir un archivo. Actualmente, los desarrolladores de Chromium están finalizando el código que permitiría permisos persistentes para las AWP instaladas. Esto es algo que muchos desarrolladores de AWPs han estado solicitando y se espera con ansias.

Pero ¿y si no tenemos que esperar? Recientemente, los desarrolladores de Kiwix descubrieron que es posible eliminar todas las solicitudes de permisos en este momento con una nueva función de la API de acceso a archivos que es compatible con los navegadores Chromium y Firefox (y es compatible de forma parcial con Safari, pero aún falta FileSystemWritableFileStream). Esta nueva función es el sistema de archivos privado de Origin.

Cómo pasar a ser completamente nativo: el sistema de archivos privados de Origin

El sistema de archivos privados de origen (OPFS) sigue siendo una función experimental en la AWP de Kiwix, pero el equipo está muy entusiasmado por alentar a los usuarios a probarlo, ya que cierra la brecha entre las apps nativas y las web. Estos son los beneficios clave:

  • Se puede acceder a los archivos del sistema de archivos en línea sin mensajes de permisos, incluso durante el inicio. Los usuarios pueden reanudar la lectura de un artículo y la navegación por un archivo desde donde lo dejaron en una sesión anterior, sin ningún inconveniente.
  • Proporciona acceso altamente optimizado a los archivos almacenados en él: en Android, vemos mejoras de velocidad de entre cinco y diez veces más rápidas.

El acceso estándar a los archivos en Android con la API de File es muy lento, en especial (como suele ser el caso de los usuarios de Kiwix) si los archivos grandes se almacenan en una tarjeta microSD en lugar del almacenamiento del dispositivo. Todo eso cambia con esta nueva API. Si bien la mayoría de los usuarios no podrán almacenar un archivo de 97 GB en el sistema de archivos OPFS (que consume el almacenamiento del dispositivo, no el de la tarjeta microSD), es perfecto para almacenar archivos de tamaño pequeño a mediano. ¿Quieres la enciclopedia médica más completa de WikiProject Medicine? No hay problema, con 1.7 GB, cabe fácilmente en el sistema de archivos de objetos. (Sugerencia: busca othermdwiki_en_all_maxi en la biblioteca de la app).

Cómo funciona el sistema de archivos en objetos

El OPFS es un sistema de archivos que proporciona el navegador, independiente para cada origen, que se puede considerar similar al almacenamiento centrado en la app en Android. Los archivos se pueden importar al OPFS desde el sistema de archivos visible para el usuario o se pueden descargar directamente en él (la API también permite crear archivos en el OPFS). Una vez que están en el OPFS, se aíslan del resto del dispositivo. En los navegadores basados en Chromium para computadoras de escritorio, también es posible exportar archivos desde el sistema de archivos en caché al sistema de archivos visible para el usuario.

Para usar el OPFS, el primer paso es solicitar acceso a él con navigator.storage.getDirectory() (una vez más, si prefieres ver el código con await, lee El sistema de archivos privados de Origin):

return navigator.storage
  .getDirectory()
  .then(function (handle) {
    return processDirHandle(handle);
  })
  .catch(function (err) {
    console.warn('Unable to get the OPFS directory entry', err);
  });

El identificador que obtienes de esto es el mismo tipo de FileSystemDirectoryHandle que obtienes de window.showDirectoryPicker() que se mencionó anteriormente, lo que significa que puedes volver a usar el código que lo controla (y, por fortuna, no es necesario almacenarlo en indexedDB; simplemente obténlo cuando lo necesites). Supongamos que ya tienes algunos archivos en el sistema de archivos de objetos y quieres usarlos. Luego, con la función iterateAsyncDirEntries() que se mostró anteriormente, podrías hacer lo siguiente:

return navigator.storage.getDirectory().then(function (dirHandle) {
  let entries = dirHandle.entries();
  return iterateAsyncDirEntries(entries, [])
    .then(function (archiveList) {
      return archiveList;
    })
    .catch(function (err) {
      console.error('Unable to iterate OPFS entries', err);
    });
});

No olvides que aún debes usar getFile() en cualquier entrada con la que quieras trabajar desde el array archiveList.

Cómo importar archivos al sistema de archivos OPFS

Entonces, ¿cómo se ingresan archivos en el sistema de archivos en primer lugar? ¡No tan rápido! Primero, debes estimar la cantidad de almacenamiento con la que tienes que trabajar y asegurarte de que los usuarios no intenten colocar un archivo de 97 GB si no cabe.

Obtener la cuota estimada es fácil: navigator.storage.estimate().then(function (estimate) { … });. Lo más difícil es descubrir cómo mostrarle esto al usuario. En la app de Kiwix, optamos por un pequeño panel en la app visible junto a la casilla de verificación que permite a los usuarios probar la OPFS:

Panel que muestra el almacenamiento utilizado en porcentaje y el almacenamiento disponible restante en gigabytes.

El panel se propaga con estimate.quota y estimate.usage, por ejemplo:

let OPFSQuota; // Global variable, so we don't have to keep checking it
return navigator.storage.estimate().then(function (estimate) {
  const percent = ((estimate.usage / estimate.quota) * 100).toFixed(2);
  OPFSQuota = estimate.quota - estimate.usage;
  document.getElementById('OPFSQuota').innerHTML =
    '<b>OPFS storage quota:</b><br />Used:&nbsp;<b>' +
    percent +
    '%</b>; ' +
    'Remaining:&nbsp;<b>' +
    (OPFSQuota / 1024 / 1024 / 1024).toFixed(2) +
    '&nbsp;GB</b>';
});

Como puedes ver, también hay un botón que permite a los usuarios agregar archivos al OPFS desde el sistema de archivos visible para el usuario. La buena noticia es que puedes usar la API de File para obtener el objeto File (o los objetos) necesarios que se importarán. De hecho, es importante no usar window.showOpenFilePicker() porque Firefox no admite este método, mientras que el OPFS es compatible.

El botón visible Agregar archivos que se ve en la captura de pantalla anterior no es un selector de archivos heredado, pero click() un selector heredado oculto (elemento <input type="file" multiple … />) cuando se hace clic en él. Luego, la app solo captura el evento change de la entrada de archivo oculto, verifica el tamaño de los archivos y los rechaza si son demasiado grandes para la cuota. Si todo está bien, pregúntale al usuario si quiere agregarlos:

archiveFilesLegacy.addEventListener('change', function (files) {
  const filesArray = Array.from(files.target.files);
  // Abort if user didn't select any files
  if (filesArray.length === 0) return;
  // Calculate the size of the picked files
  let filesSize = 0;
  filesArray.forEach(function (file) {
    filesSize += file.size;
  });
  // Check the size of the files does not exceed the quota
  if (filesSize > OPFSQuota) {
    // Oh no, files are too big! Tell user...
    console.log('Files would exceed the OPFS quota!');
  } else {
    // Ask user if they're sure... if user said yes...
    return importOPFSEntries(filesArray)
      .then(function () {
        // Tell user we successfully imported the archives
      })
      .catch(function (err) {
        // Tell user there was an error (error catching is important!)
      });
  }
});

Diálogo en el que se le pregunta al usuario si desea agregar una lista de archivos .zim al sistema de archivos privado de origen.

Debido a que, en algunos sistemas operativos, como Android, importar archivos no es la operación más rápida, Kiwix también muestra un banner y un ícono giratorio pequeño mientras se importan los archivos. El equipo no pudo agregar un indicador de progreso para esta operación. Si lo logras, responde en una postal.

Entonces, ¿cómo implementó Kiwix la función importOPFSEntries()? Esto implica usar el método fileHandle.createWriteable(), que permite que cada archivo se transmita a la OPFS. El navegador se encarga de todo el trabajo duro. (Kiwix usa promesas aquí por motivos relacionados con nuestra base de código heredada, pero hay que decir que, en este caso, await produce una sintaxis más simple y evita el efecto de pirámide de la muerte).

function importOPFSEntries(files) {
  // Get a handle on the OPFS directory
  return navigator.storage
    .getDirectory()
    .then(function (dir) {
      // Collect the promises for each file that we want to write
      let promises = files.map(function (file) {
        // Create the file and get a writeable handle on it
        return dir
          .getFileHandle(file.name, { create: true })
          .then(function (fileHandle) {
            // Get a writer for the file
            return fileHandle.createWritable().then(function (writer) {
              // Show a banner / spinner, then write the file
              return writer
                .write(file)
                .then(function () {
                  // Finished with this writer
                  return writer.close();
                })
                .catch(function (err) {
                  console.error('There was an error writing to the OPFS!', err);
                });
            });
          })
          .catch(function (err) {
            console.error('Unable to get file handle from OPFS!', err);
          });
      });
      // Return a promise that resolves when all the files have been written
      return Promise.all(promises);
    })
    .catch(function (err) {
      console.error('Unable to get a handle on the OPFS directory!', err);
    });
}

Descargar un flujo de archivos directamente en el OPFS

Una variación de esto es la capacidad de transmitir un archivo desde Internet directamente al OPFS o a cualquier directorio para el que tengas un identificador de directorio (es decir, directorios elegidos con window.showDirectoryPicker()). Usa los mismos principios que el código anterior, pero construye un Response que consta de un ReadableStream y un controlador que pone en cola los bytes leídos del archivo remoto. Luego, el Response.body resultante se envía al escritor del archivo nuevo dentro del OPFS.

En este caso, Kiwix puede contar los bytes que pasan por ReadableStream y, así, proporcionarle al usuario un indicador de progreso y también advertirle que no salga de la app durante la descarga. El código es un poco demasiado complicado para mostrarlo aquí, pero como nuestra app es de software libre y de código abierto, puedes consultar la fuente si te interesa hacer algo similar. Así es como se ve la IU de Kiwix (los diferentes valores de progreso que se muestran a continuación se deben a que solo se actualiza el banner cuando cambia el porcentaje, pero se actualiza el panel Download progress con más regularidad):

Interfaz de usuario de Kiwix con una barra en la parte inferior que le advierte al usuario que no salga de la app y que muestra el progreso de la descarga del archivo .zim.

Dado que la descarga puede ser una operación bastante larga, Kiwix permite que los usuarios usen la app libremente durante la operación, pero se asegura de que el banner siempre se muestre, de modo que se les recuerde a los usuarios que no cierren la app hasta que se complete la operación de descarga.

Implementa un miniadministrador de archivos en la app

En este punto, los desarrolladores de la AWP de Kiwix se dieron cuenta de que no es suficiente poder agregar archivos a la OPFS. La app también debía brindarles a los usuarios una forma de borrar los archivos que ya no necesitaban de esta área de almacenamiento y, de manera ideal, también exportar los archivos bloqueados en el sistema de archivos de OPFS al sistema de archivos visible para el usuario. En efecto, fue necesario implementar un mini sistema de administración de archivos dentro de la app.

Quiero hacer una breve mención a la fantástica extensión OPFS Explorer para Chrome (también funciona en Edge). Agrega una pestaña en las herramientas para desarrolladores que te permite ver exactamente lo que contiene el sistema de archivos OPFS y también borrar archivos no deseados o con errores. Fue invaluable para verificar si el código funcionaba, supervisar el comportamiento de las descargas y, en general, limpiar nuestros experimentos de desarrollo.

La exportación de archivos depende de la capacidad de obtener un identificador de archivo en un archivo o directorio elegido en el que Kiwix guardará el archivo exportado, por lo que solo funciona en contextos en los que se puede usar el método window.showSaveFilePicker(). Si los archivos de Kiwix fueran más pequeños que varios GB, podríamos crear un blob en la memoria, asignarle una URL y, luego, descargarlo en el sistema de archivos visible para el usuario. Lamentablemente, eso no es posible con archivos tan grandes. Si es compatible, la exportación es bastante sencilla: es prácticamente lo mismo, a la inversa, que guardar un archivo en el sistema de archivos en objetos (obtén un identificador del archivo que se guardará, pídele al usuario que elija una ubicación para guardarlo con window.showSaveFilePicker() y, luego, usa createWriteable() en saveHandle). Puedes ver el código en el repositorio.

La eliminación de archivos es compatible con todos los navegadores y se puede lograr con un dirHandle.removeEntry('filename') simple. En el caso de Kiwix, preferimos iterar las entradas de OPFS como lo hicimos anteriormente para verificar primero que el archivo seleccionado exista y solicitar confirmación, pero es posible que no sea necesario para todos. Una vez más, puedes examinar nuestro código si te interesa.

Se decidió no desordenar la IU de Kiwix con botones que ofrezcan estas opciones y, en su lugar, colocar íconos pequeños directamente debajo de la lista de archivos. Si presionas uno de estos íconos, cambiará el color de la lista de archivos, como una pista visual para el usuario sobre lo que hará. Luego, el usuario hace clic o presiona uno de los archivos, y se realiza la operación correspondiente (exportar o borrar) (después de la confirmación).

Diálogo en el que se le pregunta al usuario si quiere borrar un archivo .zim.

Por último, aquí tienes una demostración de presentación en pantalla de todas las funciones de administración de archivos que se analizaron anteriormente: agregar un archivo al sistema de archivos en objetos, descargarlo directamente en él, borrarlo y exportarlo al sistema de archivos visible para el usuario.

El trabajo de un desarrollador nunca termina

El sistema de archivos en línea es una gran innovación para los desarrolladores de AWP, ya que proporciona funciones de administración de archivos realmente potentes que ayudan a cerrar la brecha entre las apps nativas y las web. Pero los desarrolladores son un grupo desdichado, nunca están del todo satisfechos. El sistema de archivos en línea es casi perfecto, pero no del todo. Es genial que las funciones principales funcionen en los navegadores Chromium y Firefox, y que se implementen en Android y computadoras de escritorio. Esperamos que el conjunto completo de funciones también se implemente pronto en Safari y iOS. Aún persisten los siguientes problemas:

  • Actualmente, Firefox establece un límite de 10 GB en la cuota de OPFS, sin importar la cantidad de espacio en el disco subyacente. Si bien para la mayoría de los autores de AWP esto puede ser suficiente, para Kiwix es bastante restrictivo. Por fortuna, los navegadores de Chromium son mucho más generosos.
  • Actualmente, no es posible exportar archivos grandes del OPFS al sistema de archivos visible para el usuario en navegadores para dispositivos móviles ni en Firefox para computadoras de escritorio, ya que no se implementó window.showSaveFilePicker(). En estos navegadores, los archivos grandes se quedan atrapados de manera efectiva en el sistema de archivos en caché. Esto va en contra del espíritu de Kiwix, que es el acceso abierto al contenido y la capacidad de compartir archivos entre usuarios, especialmente en áreas con conectividad a Internet intermitente o costosa.
  • El usuario no puede controlar qué almacenamiento consumirá el sistema de archivos virtual de OPFS. Esto es particularmente problemático en dispositivos móviles, en los que los usuarios pueden tener grandes cantidades de espacio en una tarjeta microSD, pero una cantidad muy pequeña en el almacenamiento del dispositivo.

Pero, en general, estos son problemas menores en lo que, de otro modo, es un gran paso adelante para el acceso a archivos en las AWP. El equipo de la AWP de Kiwix agradece mucho a los desarrolladores y defensores de Chromium que propusieron y diseñaron por primera vez la API de acceso al sistema de archivos, y por el arduo trabajo de lograr un consenso entre los proveedores de navegadores sobre la importancia del sistema de archivos privados de origen. En el caso de la AWP de Kiwix JS, resolvió muchos de los problemas de UX que afectaron a la app en el pasado y nos ayuda en nuestra búsqueda para mejorar la accesibilidad del contenido de Kiwix para todos. Prueba la PWA de Kiwix y comunícate con los desarrolladores para contarles tu opinión.

Si quieres obtener excelentes recursos sobre las funciones de las AWP, consulta estos sitios: