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

Geoffrey Kantaris
Geoffrey Kantaris
Stéphane Coillet-Matillon
Stéphane Coillet-Matillon

Personas reunidas alrededor de una laptop de pie 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

Según la Unión Internacional de Telecomunicaciones, 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. ¿Aquí es 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 cuyo objetivo es 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, además de 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 necesita proporcionar un acceso rápido a archivos de contenido grandes sin conexión y algunas APIs modernas de JavaScript, en particular la API de File System Access y el Origin Private File System, que proporcionan soluciones innovadoras y emocionantes a 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 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 de casos de uso posible, la organización también se dio cuenta de que podía llegar a más usuarios con 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, denominada Kiwix HTML5, era para el SO Firefox, que ya no existe, y para las extensiones del navegador. En el núcleo, se encontraba (y es) un motor de descompresión de C++ (XZ y ZSTD) compilado en el lenguaje JavaScript intermedio de ASM.js. Luego, Wasm, o WebAssembly, usa 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 permitirían 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 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 a través de un torrent y, luego, lo transfieren a un dispositivo móvil o 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, Kiwix JS pudo leer archivos enormes de cientos de GB (uno de nuestros archivos de ZIM es de 166 GB), incluso en dispositivos con poca memoria, la API de File. Esta API es universalmente compatible con cualquier navegador, incluso en navegadores muy antiguos, por lo que actúa como un resguardo universal para cuando las APIs más nuevas no son compatibles. 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 al hecho de que se incluye una copia completa de Chromium con cada app, es muy desfavorable con respecto a solo 5.1 MB para la AWP minimizada y empaquetada.

Entonces, ¿existe alguna manera de que Kiwix pudiera mejorar la situación para 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 prueba de origen en Chrome 78, que luego se llamó API de Native File System. Prometía la capacidad de obtener un controlador para un archivo o una carpeta y almacenarlos en una base de datos 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). Para el momento en que llegó a producción, se le había cambiado el nombre por API de File System Access, y las partes principales estandarizaron por QUÉWG como la API de File System (FSA).

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

Abrir un selector de archivos se verá de la siguiente manera (aquí con las promesas, pero si prefieres el azúcar 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 controlador, deberás procesarlo, de modo 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 usa para almacenar y recuperar un controlador de archivo o carpeta.

El procesamiento de directorios es un poco más complicado, ya que debes iterarlos por las entradas del directorio seleccionado con entries.next() asíncrono para encontrar los archivos o los 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.

¿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 parcialmente compatible con Safari, pero aún falta FileSystemWritableFileStream). Esta nueva función es el sistema de archivos privados de Origin.

Nueva versión completamente nativa: Origin Private File System

El sistema de archivos privados de origen (OPFS) aún es una función experimental de la AWP de Kiwix, pero el equipo se complace en alentar a los usuarios a que lo prueben, ya que cierra en gran medida 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 exploración de un archivo desde donde lo dejaron en una sesión anterior, sin ninguna fricción.
  • Proporciona acceso altamente optimizado a los archivos almacenados: en Android, vemos mejoras de velocidad 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 de en el 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 OPFS

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, también es posible exportar archivos del sistema de archivos en caché al sistema de archivos visible para el usuario.

Para usar OPFS, el primer paso es solicitar acceso a él mediante navigator.storage.getDirectory() (de nuevo, si prefieres ver el código que usa await, lee The Origin Private File System):

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 a OPFS

Entonces, ¿cómo se colocan archivos en OPFS en primer lugar? ¡No tan rápido! En primer lugar, debes estimar la cantidad de almacenamiento con el 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 encontrar la forma de agregar un indicador de progreso para esta operación. Si lo descubres, responde en una postal.

Entonces, ¿cómo implementó Kiwix la función importOPFSEntries()? Esto implica usar el método fileHandle.createWriteable(), que permite de manera efectiva que cada archivo se transmita a 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);
    });
}

Cómo descargar una transmisión de archivos directamente en OPFS

Una variación de esto es la capacidad de transmitir un archivo de Internet directamente a OPFS o a cualquier directorio para el que tengas un controlador de directorio (es decir, directorios seleccionados 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 canaliza al escritor del archivo nuevo dentro de OPFS.

En este caso, Kiwix puede contar los bytes que pasan por ReadableStream y, así, proporcionarle un indicador de progreso al usuario 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 código abierto, puedes consultar la fuente si te interesa hacer algo similar. Así se ve la IU de Kiwix (los diferentes valores de progreso que se muestran a continuación se deben a que solo actualiza el banner cuando cambia el porcentaje, pero actualiza el panel Progreso de la descarga con mayor frecuencia):

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. De hecho, se hizo 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 caché, 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 suerte, los navegadores Chromium son mucho más generosos.
  • Por el momento, 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 window.showSaveFilePicker() no está implementado. 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, donde 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, esta solución resolvió muchos de los problemas de UX que obstruían la app en el pasado y nos ayuda en nuestra búsqueda de mejorar la accesibilidad del contenido de Kiwix para todo el mundo. Prueba la AWP 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: