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 plástica a la izquierda. El fondo parece una escuela en un país en desarrollo.

En este caso de éxito, se explora cómo Kiwix, una organización sin fines de lucro, utiliza la tecnología de apps web progresivas y la API de File System Access 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 relacionado 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 brinda un mejor acceso a los archivos sin solicitar permisos. En el artículo, se analizan los desafíos y se destacan los posibles desarrollos futuros en este nuevo sistema de archivos.

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. Las personas en Kiwix, una organización sin fines de lucro con sede en Suiza, desarrollaron un ecosistema de apps y contenido de código abierto que tiene como objetivo poner el conocimiento a disposición de personas con acceso a Internet limitado o nulo. La idea es que, si no puedes acceder fácilmente a Internet, alguien puede descargar los recursos clave por ti, dónde y cuándo la conectividad está disponible, y almacenarlos de manera local para usarlos sin conexión más adelante. Muchos sitios vitales, como Wikipedia, el proyecto Gutenberg, Stack Exchange o incluso charlas TED, ahora se pueden convertir en archivos altamente comprimidos, llamados archivos ZIM, y leer al instante por el navegador Kiwix.

Los archivos de ZIM usan una compresión Zstandard (ZSTD) muy eficiente (las versiones anteriores usaban XZ), principalmente para almacenar HTML, JavaScript y CSS, mientras que las imágenes generalmente se convierten al formato WebP comprimido. Cada ZIM también incluye una URL y un índice de título. La compresión es clave aquí, ya que todo el contenido de 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 suena muy alto hasta que te des cuenta de que la suma de todo el conocimiento humano ahora cabe 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 orientadas a computadoras de escritorio (Windows/Linux/macOS) y a dispositivos móviles (iOS/Android). Sin embargo, este caso de éxito se enfocará en la app web progresiva (AWP) que busca ser una solución universal y simple para cualquier dispositivo que tenga un navegador actualizado.

Veremos los desafíos que plantea el desarrollo de una app web universal que necesita proporcionar acceso rápido a archivos de contenido de gran tamaño completamente sin conexión, además de 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 uso 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 desde los que accederán a su contenido. Algunos de estos dispositivos pueden ser lentos o desactualizados, en especial en áreas de bajos ingresos del mundo. Si bien Kiwix intenta abarcar tantos casos de uso como sea posible, la organización también se dio cuenta de que podría llegar a incluso más usuarios con la pieza de 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, eventualmente se escribirá en JavaScript, algunos desarrolladores de Kiwix, hace unos 10 años, configuraron la portabilidad del software Kiwix de C++ a JavaScript.

La primera versión de este puerto, llamada Kiwix HTML5, era para el SO Firefox y las extensiones del navegador ya desaparecidos. En esencia, era (y es) un motor de descompresión C++ (XZ y ZSTD) compilado en el lenguaje JavaScript intermedio de ASM.js y, luego, Wasm, o WebAssembly, con el compilador Emscripten. Más adelante, se cambió el nombre por Kiwix JS, y las extensiones del navegador todavía se desarrollan de forma activa.

Navegador sin conexión de Kiwix JS

Ingresa la app web progresiva (AWP). Al darse cuenta del potencial de esta tecnología, los desarrolladores de Kiwix compilaron una versión AWP dedicada de Kiwix JS y se decidieron por agregar integraciones del SO que permitirían que la app ofreciera capacidades similares a las nativas, especialmente en las áreas de uso sin conexión, instalación, manejo de archivos y acceso al sistema de archivos.

Las AWP que priorizan el uso sin conexión son muy livianas, por lo que 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: interceptan las solicitudes de recuperación del documento o artículo principal que se están viendo y las redireccionan al backend (JS) para extraer y construir una respuesta a partir del archivo ZIM.

Almacenamiento y almacenamiento en todas partes

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

En un principio, Kiwix JS podía leer archivos enormes de cientos de GB (uno de nuestros archivos de ZIM es de 166 GB), incluso en dispositivos con poca memoria, es la API de File. Esta API es compatible universalmente con cualquier navegador, incluso navegadores muy antiguos, por lo que actúa como resguardo universal para cuando las APIs más nuevas no sean compatibles. Es tan sencillo 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, en esencia, son metadatos que hacen referencia a los datos subyacentes en el almacenamiento. Técnicamente, el backend orientado a objetos de Kiwix, escrito en JavaScript del lado del cliente puro, lee pequeñas porciones del archivo grande según sea necesario. Si es necesario descomprimir esas partes, el backend las pasa al descompresor Wasm y obtiene más fragmentos si se solicita, hasta que se descomprime un BLOB completo (por lo general, un artículo o un recurso). Esto significa que el archivo grande nunca debe leerse por completo en la memoria.

Como es, la API de File tiene una desventaja que hizo que las apps de Kiwix JS parezcan complicadas y anticuadas en comparación con las nativas: requiere que el usuario elija los archivos con un selector de archivos o arrastra y suelta un archivo en la app cada vez que se inicia, ya que con esta API no hay manera de conservar los permisos de acceso de una sesión a la siguiente.

Para mitigar esta mala UX, como muchos desarrolladores, los desarrolladores de Kiwix JS al principio tomaron la ruta Electron. ElectronJS es un framework increíble que proporciona funciones potentes, incluido el acceso completo al sistema de archivos mediante las APIs de Node. Sin embargo, presenta algunas desventajas conocidas:

  • Solo se ejecuta en los sistemas operativos de escritorio.
  • 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, se compara de manera muy desfavorable con solo 5.1 MB para la AWP minimizada y empaquetada.

Entonces, ¿había alguna forma en que Kiwix mejorara 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 se sometía a una prueba de origen en Chrome 78, luego llamada API de Native File System. Promete la capacidad de obtener un controlador para un archivo o una carpeta y almacenarlos en una base de datos IndexedDB. Cabe destacar que este controlador persiste entre las sesiones de la app, por lo que el usuario no se ve obligado a elegir el archivo o la carpeta nuevamente cuando vuelve a iniciar la app (aunque debe responder a una solicitud de permiso rápida). Cuando llegó a producción, se le había cambiado el nombre por la API de File System Access, y las partes principales estandarizadas por WHG como la API del sistema de archivos (FSA).

Entonces, ¿cómo funciona la parte de acceso al sistema de archivos de la API? Algunos puntos importantes para tener en cuenta:

  • Es una API asíncrona (excepto para funciones especializadas en Web Workers).
  • Los selectores de archivos o directorios deben iniciarse de manera programática mediante la captura de un gesto del usuario (hacer clic o presionar un elemento de la IU).
  • A fin de que el usuario vuelva a otorgar permiso para acceder a un archivo seleccionado anteriormente (en una sesión nueva), también se necesita un gesto del usuario. De hecho, el navegador no mostrará la solicitud de permiso si no lo inició un gesto del usuario.

El código es relativamente sencillo, además de tener que usar la engorrosa API de IndexedDB para almacenar controladores de archivos y directorios. La buena noticia es que hay algunas 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 los selectores de archivos y directorios

Abrir un selector de archivos se ve 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, para simplificar, este código solo procesa el primer archivo seleccionado (y prohíbe elegir más de uno). En caso de que quieras permitir la selección de varios archivos con { multiple: true }, solo debes unir todas las promesas que procesen cada controlador 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 si se le solicita al usuario que elija el directorio que contiene esos archivos en lugar de los archivos individuales que contiene, 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 controlador de directorio o archivo

Una vez que tengas el controlador, deberás procesarlo para que la función processFileHandle se vea 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 controlador de archivo, no hay métodos convenientes para esto, a menos que uses una biblioteca de abstracción. La implementación de esto de Kiwix 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.

Procesar directorios es un poco más complicado, ya que debes iterar a través de las entradas en el directorio seleccionado con entries.next() asíncrono para encontrar los archivos o los tipos de archivos que deseas. Hay varias maneras de hacerlo, pero este es el código que se usa en Kiwix PWA:

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 necesites usarlo, o el equivalente mediante const file = await entry.getFile() en un async function.

¿Podemos ir más lejos?

El requisito de que el usuario otorgue permiso que se inicia con un gesto del usuario en los inicios posteriores de la app agrega una pequeña cantidad de inconvenientes al abrir y cerrar de carpetas y archivos, pero este proceso sigue siendo mucho más fluido que verse obligado a 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. Muchos desarrolladores de AWP solicitaron esta solución y se espera con ansias.

¿Y si no tenemos que esperar? Hace poco, los desarrolladores de Kiwix descubrieron que es posible eliminar todas las solicitudes de permisos en este momento mediante una nueva y brillante función de la API de File Access que es compatible con los navegadores Chromium y Firefox (y parcialmente compatible con Safari, pero aún falta FileSystemWritableFileStream). Esta nueva función es el Sistema de archivos privados de origen.

Lanzamiento completamente nativo: el sistema de archivos privados de origen

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

  • Se puede acceder a los archivos en OPFS sin solicitudes 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 ningún tipo de fricción.
  • Proporciona acceso altamente optimizado a los archivos almacenados en ella. En Android, se observan mejoras de velocidad entre cinco y diez veces más rápidas.

El acceso estándar a los archivos en Android a través de la API de File es muy lento, en especial (como suele suceder para 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á almacenar un archivo de 97 GB en el OPFS (que consume almacenamiento del dispositivo y no el de la tarjeta microSD), esta opción es perfecta para almacenar archivos de tamaño pequeño a mediano. ¿Deseas obtener la enciclopedia médica más completa de WikiProject Medicine? No hay problema. Con 1.7 GB, cabe fácilmente en el OPFS. (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, separado para cada origen, que se puede considerar similar al almacenamiento específico de 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 dentro del 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 OPFS al sistema de archivos visible para el usuario.

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

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

El controlador que obtienes de esto es el mismo tipo de FileSystemDirectoryHandle que obtienes del window.showDirectoryPicker() que se mencionó anteriormente, lo que significa que puedes volver a usar el código que controla eso (y, por suerte, no es necesario almacenarlo en indexedDB; solo debes obtenerlo cuando lo necesites). Supongamos que ya tienes algunos archivos en OPFS y quieres usarlos. Luego, con la función iterateAsyncDirEntries() que se mostró antes, puedes hacer algo como 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.

Importa archivos a OPFS

Entonces, ¿cómo puedes ingresar archivos a OPFS en primer lugar? ¡No tan rápido! Primero, debes estimar la cantidad de almacenamiento que tienes para trabajar y asegurarte de que los usuarios no intenten colocar un archivo de 97 GB si no encaja.

Obtener la cuota estimada es fácil: navigator.storage.estimate().then(function (estimate) { … });. Un poco más difícil es averiguar cómo mostrárselo al usuario. En la app de Kiwix, optamos por un pequeño panel en la app visible justo junto a la casilla de verificación que permite a los usuarios probar el 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 les permite a los usuarios agregar archivos al OPFS desde el sistema de archivos que lo ven. La buena noticia es que puedes usar la API de File para obtener el objeto (o los objetos) de archivo necesario que se importarán. De hecho, es importante no usar window.showOpenFilePicker() porque este método no es compatible con Firefox, mientras que OPFS sí lo es.

El botón visible Add file(s) que se ve en la captura de pantalla anterior no es un selector de archivos heredado, pero sí click() un selector heredado oculto (elemento <input type="file" multiple … />) cuando se hace clic en él o se lo presiona. Luego, la app solo captura el evento change de la entrada del 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 agregarlo:

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 que le pregunta al usuario si quiere agregar una lista de archivos .zim al sistema de archivos privados de origen.

Dado 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 descubrió cómo agregar un indicador de progreso para esta operación: si lo resuelves, crea respuestas 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 al OPFS. El navegador se encarga de todo el trabajo duro. En este caso, Kiwix usa Promesas 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 la pirámide de efecto doom.

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 una transmisión de archivos directamente en OPFS

Una variación de esto es la capacidad de transmitir un archivo desde Internet directamente a OPFS o a cualquier directorio para el que tengas un controlador 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. La Response.body resultante se canaliza al escritor del archivo nuevo dentro del OPFS.

En este caso, Kiwix puede contar los bytes que pasan por ReadableStream y, por lo tanto, proporciona un indicador de progreso al usuario y le advierte que no cierre la app durante la descarga. El código es demasiado complicado para mostrarlo aquí, pero como nuestra app es FOSS, puedes ver 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 con más frecuencia el panel Download progress):

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

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

Cómo implementar un administrador de archivos mini en la app

En este punto, los desarrolladores de AWP de Kiwix notaron que poder agregar archivos al OPFS no era suficiente. La app también necesitaba brindarles a los usuarios una forma de borrar los archivos que ya no necesitan de esta área de almacenamiento y, idealmente, también de exportar los archivos bloqueados en el OPFS al sistema de archivos visibles para el usuario. De hecho, se hizo necesario implementar un minisistema de administración de archivos dentro de la app.

Queremos anunciarte rápidamente la fabulosa extensión OPFS Explorer para Chrome (también funciona en Edge). En las herramientas para desarrolladores, se agrega una pestaña que te permite ver exactamente lo que contiene OPFS y borrar archivos no autorizados o con errores. Fue una herramienta muy útil para verificar si el código funcionaba, supervisar el comportamiento de las descargas y, en general, realizar una limpieza de nuestros experimentos de desarrollo.

La exportación de archivos depende de la capacidad de obtener un controlador de archivo en un archivo o directorio seleccionado en el que Kiwix guardará el archivo exportado, por lo que esto solo funciona en contextos en los que puede usar el método window.showSaveFilePicker(). Si los archivos de Kiwix tuvieran un tamaño inferior a varios GB, podríamos construir un BLOB en la memoria, proporcionarle una URL y, luego, descargarlo en el sistema de archivos visible para el usuario. Desafortunadamente, no es posible con archivos tan grandes. Si se admite, la exportación es bastante sencilla: prácticamente igual, a la inversa, como cuando guardas un archivo en OPFS (obtén un controlador del archivo que quieres 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, de modo que pudiéramos verificar que el archivo seleccionado exista primero y solicitar confirmación, pero tal vez 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 ofrecieran estas opciones y, en su lugar, colocar íconos pequeños directamente debajo de la lista de archivo. Si presionas uno de estos íconos, se cambiará el color de la lista de archivos, como una pista visual para el usuario sobre lo que va a hacer. Luego, el usuario hace clic en uno de los archivos o lo presiona, y se llevará a cabo la operación correspondiente (exportación o eliminación) (después de la confirmación).

Cuadro de diálogo en el que se le pregunta al usuario si desea borrar un archivo .zim

Por último, esta es una demostración en pantalla de todas las funciones de administración de archivos que se mencionaron anteriormente: agregar un archivo al OPFS, descargar un archivo directamente, borrarlo y exportarlo al sistema de archivos visible para el usuario.

El trabajo de un desarrollador nunca termina

El OPFS 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 apps web. Pero los desarrolladores son lamentables, nunca están tan satisfechos. OPFS es casi perfecto, pero no exactamente. Es genial que las funciones principales funcionen en los navegadores Chromium y Firefox, y que se implementen en Android y en computadoras de escritorio. Esperamos que el conjunto completo de funciones también se implemente pronto en Safari y en iOS. Los siguientes problemas persisten:

  • Actualmente, Firefox establece un límite de 10 GB en la cuota de OPFS, sin importar cuánto espacio subyacente en el disco haya. Si bien para la mayoría de los autores de AWP esto puede ser amplio, para Kiwix, es bastante restrictivo. Por suerte, los navegadores Chromium son mucho más generosos.
  • Por el momento, no es posible exportar archivos grandes desde OPFS al sistema de archivos visibles 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 quedan atrapados en el OPFS. Esto va en contra del espíritu de Kiwix de acceso abierto a contenido y la capacidad de compartir archivos entre usuarios, en especial en áreas de conectividad a Internet intermitente o costosa.
  • Los usuarios no pueden controlar qué almacenamiento consumirá el sistema de archivos virtuales 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 poco espacio de almacenamiento en el dispositivo.

Sin embargo, en general, se trata de pequeños problemas en lo que, de otro modo, representa un gran paso para acceder a los archivos en las AWP. El equipo de AWP de Kiwix está muy agradecido con los desarrolladores y los defensores de Chromium que propusieron y diseñaron por primera vez la API de File System Access, y por el arduo trabajo de lograr un consenso entre los proveedores de navegadores con respecto a la importancia del sistema de archivos privados de origen. En el caso de la AWP de JS de Kiwix, esta herramienta resolvió muchos de los problemas de UX que dificultaron el uso de la app en el pasado, y nos ayuda en nuestra búsqueda de mejorar la accesibilidad del contenido de Kiwix para todos. Prueba la AWP de Kiwix y cuéntales a los desarrolladores qué te parece.

Para consultar excelentes recursos sobre las funciones de la AWP, visita estos sitios: