Jak PWA Kiwix umożliwia użytkownikom przechowywanie gigabajtów danych z internetu do użytku offline

Ludzie gromadzący się wokół laptopa, stojący na prostym stole z plastikowym krzesłem po lewej stronie. Tło wygląda jak szkoła w kraju rozwijającym się.

To studium przypadku pokazuje, jak Kiwix, organizacja non-profit, wykorzystuje technologię Progressive Web App i File System Access API do pobierania i przechowywania dużych archiwów internetowych do użytku offline. Dowiedz się więcej o technicznej implementacji kodu obsługującego prywatny system plików źródła (OPFS) – nową funkcję przeglądarki w PWA Kiwix, która usprawnia zarządzanie plikami i zapewnia lepszy dostęp do archiwów bez pytania o zgodę. Omawiamy w nim wyzwania i wyróżniamy potencjalne ulepszenia, które pojawią się w tym nowym systemie plików.

Kiwix – informacje

Po 30 latach od narodzin internetu jedna trzecia populacji świata nadal czeka na niezawodny dostęp do internetu według Międzynarodowego Związku Telekomunikacji. Czy to koniec historii? Oczywiście nie. Szwajcarska organizacja non-profit Kiwix stworzyła ekosystem aplikacji i treści open source, których celem jest udostępnianie wiedzy osobom z ograniczonym lub brakującym dostępem do internetu. Chodzi o to, że jeśli Ty nie masz łatwego dostępu do internetu, ktoś może pobrać dla Ciebie najważniejsze zasoby, z dowolnego miejsca i o czasie, gdy będzie dostępna łączność, i zapisać je lokalnie do późniejszego użycia offline. Wiele ważnych witryn, takich jak Wikipedia, Project Gutenberg, Stack Exchange, a nawet wykłady na platformie TED, można teraz przekształcić w wysoce skompresowane archiwa nazywane plikami ZIM i czytać je w dowolnym momencie za pomocą przeglądarki Kiwix.

Archiwa ZIM wykorzystują bardzo wydajną kompresję Zstandard (ZSTD) (starsze wersje używały XZ), głównie do przechowywania kodu HTML, JavaScript i CSS, natomiast obrazy są zwykle konwertowane do skompresowanego formatu WebP. Każdy kod ZIM zawiera też adres URL i indeks tytułu. Kompresja ma tutaj kluczowe znaczenie, ponieważ cała angielska Wikipedia (6,4 mln artykułów plus obrazy) jest skompresowana do 97 GB po konwersji na format ZIM.To z kolei sporo, dopóki nie zdasz sobie sprawy, że cała ludzka wiedza może się teraz zmieścić na średniej klasy telefonie z Androidem. Znajdziesz tam również wiele mniejszych zasobów, np. tematyczne wersje Wikipedii, np. matematyka, medycyna itd.

Kiwix oferuje wiele aplikacji natywnych kierowanych do użytkowników komputerów (Windows, Linux i macOS) oraz urządzeń mobilnych (iOS i Android). Studium przypadku skupia się jednak na progresywnej aplikacji internetowej (PWA), która ma być uniwersalnym i prostym rozwiązaniem dla każdego urządzenia z nowoczesną przeglądarką.

Przyjrzymy się wyzwaniom związanym z opracowaniem uniwersalnej aplikacji internetowej, która ma zapewniać szybki dostęp do dużych archiwów treści w trybie offline, a także omówimy niektóre nowoczesne interfejsy API JavaScript, w szczególności File System Access API i Origin Private File System, które zapewniają innowacyjne i ekscytujące rozwiązania tych wyzwań.

Aplikacja internetowa do użytku w trybie offline?

Użytkownicy Kiwix to eklektyczna grupa o wielu różnych potrzebach. Kiwix nie ma lub nie ma kontroli nad urządzeniami i systemami operacyjnymi, na których będą korzystać z treści. Niektóre z tych urządzeń mogą działać wolno lub starzeć się, zwłaszcza w krajach o niskich dochodach. Kiwix stara się uwzględniać jak najwięcej przypadków użycia, ale organizacja zdała sobie też sprawę, że może dotrzeć do jeszcze większej liczby użytkowników, stosując najbardziej uniwersalne oprogramowanie na każdym urządzeniu, czyli przeglądarkę. Zatem, zgodnie z prawem Atwooda, zgodnie z którym każde aplikacje, które można pisać w języku JavaScript, będzie można je tworzyć w języku JavaScript. Część programistów Kiwix (ok. 10 lat temu) postanowiła przenieść oprogramowanie Kiwix z C++ do JavaScriptu.

Pierwsza wersja tego portu, Kiwix HTML5, była przeznaczona dla niedziałającego już systemu Firefox OS i rozszerzeń przeglądarki. Jego podstawą był (i jest) mechanizm dekompresji C++ (XZ i ZSTD) skompilowany do pośredniego języka JavaScriptu ASM.js, a później Wasm, czyli WebAssembly, przy użyciu kompilatora Emscripten. Rozszerzenia przeglądarki, które później zmieniły nazwę na Kiwix JS, wciąż są opracowywane.

Przeglądarka offline Kiwix JS

Otwórz progresywną aplikację internetową (PWA). Zdając sobie sprawę z potencjału tej technologii, programiści Kiwix stworzyli specjalną wersję PWA biblioteki Kiwix JS i postanowili dodać integracje z systemem operacyjnym, dzięki którym aplikacja będzie oferować funkcje typowe dla aplikacji natywnych, zwłaszcza w obszarach użytkowania offline, instalacji, obsługi plików i dostępu do systemu plików.

Progresywne aplikacje PWA tworzone w trybie offline są wyjątkowo lekkie, dlatego świetnie sprawdzają się w sytuacjach, w których internet mobilny jest drogi lub nie ma połączenia z internetem. Technologia, z której korzysta, to Service Worker API i powiązany interfejs Cache API, z których korzystają wszystkie aplikacje oparte na Kiwix JS. Te interfejsy API umożliwiają aplikacjom działanie jak serwer, przechwytując żądania pobierania z głównego dokumentu lub artykułu i przekierowując je do backendu (JS) w celu wyodrębnienia i utworzenia odpowiedzi z archiwum ZIM.

Miejsce na dane, gdziekolwiek jesteś

Ze względu na duży rozmiar archiwów ZIM, miejsce na dane i dostęp do nich, zwłaszcza na urządzeniach mobilnych, prawdopodobnie najbardziej borykają się z nimi programiści Kiwix. Wielu użytkowników Kiwix pobiera treści z aplikacji, gdy internet jest dostępny, aby korzystać z nich później w trybie offline. Inni pobierają treści na komputer za pomocą torrenta, a potem przenoszą je na telefon komórkowy lub tablet. Niektórzy wymieniają treści na pamięciach USB lub przenośnych dyskach twardych w miejscach, gdzie internet jest niestabilny lub zbyt drogi. Wszystkie te sposoby uzyskiwania dostępu do treści z dowolnych lokalizacji dostępnych dla użytkowników muszą być obsługiwane przez Kiwix JS i Kiwix PWA.

To, co początkowo umożliwiło Kiwix JS odczytywanie setek GB archiwów (jedno z naszych archiwów ZIM ma 166 GB!), nawet na urządzeniach z małą ilością pamięci jest interfejs File API. Ten interfejs API jest obsługiwany przez wszystkie przeglądarki, nawet bardzo stare przeglądarki, dzięki czemu jest uniwersalnym rozwiązaniem zastępczym, które wyświetla się, gdy nowsze interfejsy API nie są obsługiwane. Jest to równie proste jak zdefiniowanie elementu input w kodzie HTML, w przypadku Kiwix:

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

Po wybraniu element wejściowy będzie zawierać obiekty File, które są metadanymi odwołującymi się do danych bazowych przechowywanych w pamięci. Technicznie rzecz biorąc, backend Kiwix zorientowany na obiekty, napisany wyłącznie w języku JavaScript po stronie klienta, odczytuje w razie potrzeby małe wycinki dużego archiwum. Jeśli trzeba zdekompresować te wycinki, backend przekazuje je do dekompresora Wasm, uzyskując kolejne wycinki w razie potrzeby do momentu dekompresji całego obiektu blob (zwykle artykułu lub zasobu). Oznacza to, że tak dużego archiwum nie da się nigdy odczytać w pamięci.

Interfejs File API ma jednak wadą, przez co aplikacje Kiwix JS wydają się niezgrabne i staroświeckie w porównaniu z aplikacjami natywnymi: użytkownik musi wybierać pliki archiwalne za pomocą selektora plików lub przeciągać i upuszczać plik do aplikacji przy każdym uruchomieniu aplikacji, ponieważ ten interfejs API nie pozwala zachować uprawnień dostępu w kolejnych sesjach.

Aby zniwelować słabą wygodę korzystania z platformy (podobnie jak wielu innych programistów), programiści Kiwix JS początkowo poszli na Electron. ElectronJS to niesamowita platforma oferująca zaawansowane funkcje, w tym pełny dostęp do systemu plików za pomocą interfejsów API Node. Ma jednak kilka znanych wad:

  • Działa tylko w systemach operacyjnych na komputery.
  • Jest duży i ciężki (70–100 MB).

Rozmiar aplikacji Electron, ponieważ każda aplikacja zawiera pełną kopię Chromium, jest niekorzystna w porównaniu do zaledwie 5,1 MB w przypadku zminimalizowanej i dołączonej aplikacji PWA.

Czy było możliwe, że Kiwix mogłoby poprawić sytuację użytkowników PWA?

Na pomoc przychodzi interfejs File System Access API

Około 2019 roku firma Kiwix dowiedziała się o powstającym w Chrome 78 interfejsie API, który jest w trakcie testowania origin. Następnie nazywany jest interfejsem Native File System API. Obiecała możliwość uzyskania uchwytu dla pliku lub folderu i zapisania go w bazie danych IndexedDB. Co ważne, ten nick nie jest utrzymywany między sesjami aplikacji, więc użytkownik nie musi ponownie wybierać pliku ani folderu przy ponownym uruchomieniu aplikacji (chociaż musi odpowiedzieć na krótką prośbę o przyznanie uprawnień). Zanim trafił do środowiska produkcyjnego, jego nazwa została zmieniona na File System Access API. Główne części ustandaryzowane przez WhatWG jako File System API (FSA).

Jak więc działa dostęp do systemu plików interfejsu API? Kilka ważnych informacji:

  • To asynchroniczny interfejs API (oprócz specjalistycznych funkcji w narzędziach Web Workers).
  • Selektory plików i katalogów muszą być uruchamiane automatycznie przez rejestrowanie gestu użytkownika (kliknięcia lub dotknięcie elementu interfejsu).
  • Aby użytkownik mógł ponownie zezwolić na dostęp do wcześniej wybranego pliku (w nowej sesji), wymagany jest również gest użytkownika – przeglądarka odmówi wyświetlenia prośby o przyznanie uprawnień, jeśli nie zostanie zainicjowana gestem użytkownika.

Kod jest stosunkowo prosty poza tym, że do przechowywania nicków plików i katalogów potrzebny jest niezły interfejs IndexedDB API. Na szczęście istnieje kilka bibliotek, które wykonują większość pracy za Ciebie, np. browser-fs-access. W Kiwix JS zdecydowaliśmy się na bezpośrednią współpracę z interfejsami API, które są bardzo dobrze udokumentowane.

Otwieram selektory plików i katalogów

Otwieranie selektora plików wygląda mniej więcej tak (w tym miejscu przy użyciu narzędzia Promises, ale jeśli wolisz użyć cukru async/await, przeczytaj samouczek Chrome dla deweloperów):

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,
    );
  });

Dla uproszczenia pamiętaj, że ten kod przetwarza tylko pierwszy wybrany plik (i uniemożliwia wybranie więcej niż 1 pliku). Jeśli chcesz zezwolić na wybieranie wielu plików za pomocą funkcji { multiple: true }, musisz po prostu opakować wszystkie obietnice przetwarzające każdy nick w instrukcję Promise.all().then(...), np.:

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);
)};

Jednak wybranie wielu plików jest prawdopodobnie lepszym rozwiązaniem przez poproszenie użytkownika o wskazanie katalogu zawierającego te pliki, a nie poszczególnych plików, zwłaszcza że użytkownicy Kiwix często organizują wszystkie pliki ZIM w tym samym katalogu. Kod uruchamiania selektora katalogów jest prawie taki sam jak powyżej, z wyjątkiem użycia window.showDirectoryPicker.then(function (dirHandle) { … });.

Przetwarzam uchwyt pliku lub katalogu

Gdy masz już nick, musisz go przetworzyć, aby funkcja processFileHandle mogła wyglądać tak:

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;
  });
}

Pamiętaj, że musisz podać funkcję zapisującą uchwyt pliku. Nie ma do tego żadnych udogodnień, chyba że korzystasz z biblioteki abstrakcji. Implementację tej funkcji w Kiwix widać w pliku cache.js, ale można to znacznie uprościć, jeśli jest ona używana tylko do przechowywania i pobierania uchwytu pliku lub folderu.

Przetwarzanie katalogów jest nieco bardziej skomplikowane, ponieważ w wybranym katalogu trzeba iterować wpisy za pomocą asynchronicznego entries.next(), aby znaleźć odpowiednie pliki lub typy plików. Można to zrobić na różne sposoby, ale w skrócie jest to kod używany w PWA Kiwix:

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);
    });
}

Pamiętaj, że w przypadku każdego wpisu w tabeli entryList musisz później pobrać plik z atrybutem entry.getFile().then(function (file) { … }), jeśli chcesz go użyć, lub z jego odpowiednikiem za pomocą funkcji const file = await entry.getFile() w tagu async function.

Czy możemy pójść dalej?

Wymóg udzielenia uprawnień zainicjowanych za pomocą gestu użytkownika przy kolejnych uruchomieniach aplikacji utrudnia jego (ponowne) otwarcie pliku i folderu, ale i tak działa znacznie płynniej niż konieczność ponownego wyboru pliku. Deweloperzy Chromium finalizują kod, który umożliwiłby przyznanie trwałych uprawnień do zainstalowanych PWA. Zależało im na tym wielu deweloperów PWA.

A co, jeśli nie trzeba czekać? Twórcy oprogramowania Kiwix odkryli niedawno, że można obecnie wyeliminować wszystkie prośby o przyznanie uprawnień. Można to zrobić za pomocą nowej funkcji interfejsu File Access API, która jest obsługiwana zarówno w przeglądarkach Chromium, jak i Firefoksie (i częściowo obsługiwana przez Safari, ale wciąż brakuje FileSystemWritableFileStream). Tą nową funkcją jest Origin Private File System.

Pełny system natywny: prywatny system plików źródła

Prywatny system plików źródła (OPFS) jest nadal funkcją eksperymentalną w PWA Kiwix, ale zespół z radością zachęca użytkowników do jego wypróbowania, ponieważ w dużym stopniu wypełnia lukę między aplikacjami natywnymi a aplikacjami internetowymi. Oto główne korzyści:

  • Dostęp do archiwów w OPFS można uzyskać bez pytania o zgodę nawet po uruchomieniu aplikacji. Użytkownicy mogą bez trudu wznowić czytanie artykułu i przeglądanie archiwum od miejsca, w którym przerwieli w poprzedniej sesji.
  • Zapewnia wysoce zoptymalizowany dostęp do przechowywanych w niej plików. Na Androidzie widzimy poprawę szybkości od 5 do 10 razy szybciej.

Standardowy dostęp do plików na Androidzie za pomocą interfejsu File API jest strasznie powolny, zwłaszcza jeśli duże archiwa są przechowywane na karcie microSD, a nie w pamięci urządzenia (jak często ma to miejsce w przypadku użytkowników Kiwix). Wszystko się zmieni po wprowadzeniu nowego interfejsu API. Większość użytkowników nie może zapisać pliku 97 GB w OPFS (co zajmuje miejsce na urządzeniu, a nie na karcie microSD), ale to idealne rozwiązanie do przechowywania archiwów małych i średnich. Szukasz najpełniejszej encyklopedii medycznej w serwisie WikiProject Medicine? W porządku. Ma 1,7 GB, z łatwością mieści się w OPFS. (Wskazówka: w bibliotece w aplikacji poszukaj pozycji othermdwiki_en_all_maxi).

Jak działa OPFS

OPFS to system plików udostępniany przez przeglądarkę, oddzielnie dla każdego źródła. Można go traktować jako system plików ograniczony do aplikacji na Androidzie. Pliki można zaimportować do OPFS z systemu plików widocznego dla użytkownika lub pobrać je bezpośrednio do niego (interfejs API umożliwia też tworzenie plików w OPFS). Po przejściu do OPFS są odizolowane od reszty urządzenia. W przeglądarkach opartych na Chromium na komputerze można też eksportować pliki z OPFS do systemu plików widocznego dla użytkowników.

Jeśli chcesz użyć OPFS, najpierw musisz poprosić o dostęp do niego za pomocą navigator.storage.getDirectory(). Jeśli wolisz wyświetlać kod w await, przeczytaj artykuł na temat prywatnego systemu plików źródła:

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

Nick, który otrzymujesz z tego elementu: FileSystemDirectoryHandle jest taki sam jak wspomniany wyżej element window.showDirectoryPicker(). Oznacza to, że możesz ponownie użyć kodu, który to obsługuje (nie trzeba go zapisywać w usłudze indexedDB – wystarczy go pobrać w razie potrzeby). Załóżmy, że masz już w OPFS pewne pliki i chcesz ich użyć. Następnie za pomocą pokazanej wcześniej funkcji iterateAsyncDirEntries() możesz zrobić coś takiego:

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);
    });
});

Pamiętaj, że nadal musisz używać getFile() przy każdym wpisie z tablicy archiveList, nad którym chcesz pracować.

Importowanie plików do OPFS

Jak więc przenieść pliki do OPFS? Nie tak szybko! Po pierwsze, musisz oszacować ilość miejsca na dane, z jaką będzie pracować, i upewnij się, że użytkownicy nie próbują umieszczać w nim pliku 97 GB, jeśli nie mieści się w danym miejscu.

Uzyskanie szacowanego limitu jest proste: navigator.storage.estimate().then(function (estimate) { … });. Nieco trudniej jest wypracować sposób pokazania tego użytkownikowi. W aplikacji Kiwix postawiliśmy na mały panel w aplikacji widoczny obok pola wyboru, który umożliwia użytkownikom wypróbowanie OPFS:

Panel przedstawiający wykorzystanie miejsca na dane w procentach i pozostałe dostępne miejsce w gigabajtach.

Panel jest wypełniany przy użyciu znaczników estimate.quota i estimate.usage, na przykład:

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>';
});

Dostępny jest też przycisk, który pozwala użytkownikom dodawać do OPFS pliki z systemu plików widocznego dla użytkowników. Dobra wiadomość jest taka, że możesz użyć interfejsu File API, aby uzyskać potrzebny obiekt (lub obiekty) do zaimportowania. Wręcz przeciwnie, nie należy używać window.showOpenFilePicker(), bo ta metoda nie jest obsługiwana przez Firefoxa, natomiast OPFS jest jak najbardziej.

Widoczny przycisk Dodaj pliki widoczny na zrzucie ekranu powyżej nie jest starszego typu selektorem plików, ale po jego kliknięciu click()ukrywa on ukryty selektor (element <input type="file" multiple … />). Aplikacja po prostu przechwytuje zdarzenie change dotyczące ukrytego pliku wejściowego, sprawdza rozmiar plików i odrzuca je, jeśli są za duże. Jeśli wszystko jest w porządku, zapytaj użytkownika, czy chce je dodać:

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!)
      });
  }
});

Okno z pytaniem, czy użytkownik chce dodać listę plików .zim do prywatnego systemu plików origin.

Ponieważ w niektórych systemach operacyjnych, np. w Androidzie, importowanie archiwów nie jest najkrótszą operacją, więc podczas importowania archiwów Kiwix wyświetla też baner i mały wskaźnik postępu. Członkowie zespołu nie wymyślili, jak dodać wskaźnik postępu dla tej operacji. Jeśli uda Ci się rozwiązać problem, prześlij odpowiedzi na pocztówkę.

W jaki sposób Kiwix wdrożył funkcję importOPFSEntries()? Wymaga to użycia metody fileHandle.createWriteable(), która efektywnie umożliwia przesyłanie każdego pliku strumieniowo do OPFS. Całą ciężką pracę wykonuje przeglądarka. (Kiwix używa tutaj obietnic ze względu na starszą bazę kodu, ale trzeba pamiętać, że w tym przypadku await tworzy prostszą składnię i uniknie piramidy efektu zagłady).

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);
    });
}

pobieranie strumienia plików bezpośrednio do OPFS;

Odmianą tej metody jest możliwość strumieniowego przesyłania pliku z internetu bezpośrednio do OPFS lub dowolnego katalogu, dla którego masz uchwyt katalogu (czyli katalogi wybrane za pomocą window.showDirectoryPicker()). Zasada działania jest taka sama jak w kodzie powyżej, ale konstruowana jest Response obejmująca ReadableStream i kontroler, który dodaje do kolejki bajty odczytane z pliku zdalnego. Otrzymany Response.body jest następnie przesyłany potokiem do zapisującego nowego pliku w OPFS.

W takim przypadku Kiwix może zliczać bajty przesyłane przez ReadableStream, więc dawać użytkownikowi wskaźnik postępu i ostrzegać go, aby nie zamykał aplikacji podczas pobierania. Kod jest zbyt złożony, aby go tu przedstawić, ale skoro to aplikacja FOSS, możesz zajrzeć do źródła, jeśli chcesz zrobić coś podobnego. Tak wygląda interfejs Kiwix (różne wartości postępu są widoczne poniżej, ponieważ aktualizuje baner tylko w przypadku zmiany wartości procentowej, ale bardziej regularnie aktualizuje panel Postęp pobierania):

Interfejs Kiwix z u dołu paskiem, który ostrzega użytkownika, aby nie zamykał aplikacji, i wyświetla postęp pobierania archiwum .zim.

Pobieranie może być dość długie, dlatego Kiwix umożliwia swobodne korzystanie z aplikacji podczas jej trwania, ale jednocześnie dba o to, aby baner zawsze wyświetlał się w całości, dzięki czemu użytkownicy są informowani o tym, aby nie zamykać aplikacji do momentu jej zakończenia.

Wdrażanie minimenedżera plików w aplikacji

W tym momencie twórcy aplikacji PWA Kiwix zdali sobie sprawę, że nie wystarczy mieć możliwości dodawania plików do OPFS. Aplikacja musiała też dać użytkownikom możliwość usuwania z tego obszaru plików, których już nie potrzebują, oraz eksportowania plików zablokowanych w OPFS z powrotem do systemu plików widocznego dla użytkowników. W rezultacie konieczne stało się wdrożenie w aplikacji mini systemu zarządzania plikami.

Dziękuję za wspaniałe rozszerzenie OPFS Explorer do Chrome (działa ono też w Edge). Dodaje w narzędziach dla programistów kartę, która pozwala dokładnie sprawdzać, co jest w OPFS, i usuwać pliki nieuczciwe lub nieudane. Były one nieocenione za sprawdzenie, czy kod działa, monitorowanie pobierania i ogólne czyszczenie eksperymentów programistycznych.

Eksport pliku zależy od możliwości uzyskania uchwytu w przypadku wybranego pliku lub katalogu, w którym Kiwix ma zapisać ten plik. Ta metoda działa tylko w kontekstach, w których można używać metody window.showSaveFilePicker(). Gdyby pliki Kiwix były mniejsze niż kilka GB, moglibyśmy utworzyć obiekt blob w pamięci, podać mu adres URL i pobrać go do widocznego dla użytkowników systemu plików. Niestety, przy tak dużych archiwach jest to niemożliwe. Jeśli eksport jest obsługiwany, eksport jest praktycznie taki sam, odwrotnie, jak zapisanie pliku w OPFS (uzyskasz nick pliku do zapisania, poproś użytkownika o wybranie lokalizacji, w której ma go zapisać w window.showSaveFilePicker(), a następnie użycie createWriteable() w saveHandle). Kod możesz zobaczyć w repozytorium.

Usuwanie plików jest obsługiwane przez wszystkie przeglądarki i można je osiągnąć za pomocą prostych dirHandle.removeEntry('filename'). W przypadku Kiwix preferowaliśmy iterację wpisów OPFS tak jak powyżej, żeby sprawdzić, czy wybrany plik istnieje jako pierwszy, i poprosić o potwierdzenie, ale nie wszyscy muszą to robić. Jeśli chcesz, możesz sprawdzić nasz kod.

Postanowiliśmy nie zaśmiecać interfejsu Kiwix przyciskami oferującymi takie opcje i umieścić małe ikony bezpośrednio pod listą archiwów. Kliknięcie jednej z tych ikon zmieni kolor listy archiwów, co stanowi wizualną wskazówkę dla użytkownika o tym, co może zrobić. Następnie użytkownik klika jedno z archiwów, a po potwierdzeniu wykonywana jest odpowiednia operacja (wyeksportowanie lub usunięcie).

Okno z pytaniem, czy użytkownik chce usunąć plik .zim.

Na koniec przedstawiamy prezentację wszystkich funkcji zarządzania plikami omówionych powyżej – dodanie pliku do OPFS, bezpośrednie pobranie go do niego, usunięcie go i wyeksportowanie do systemu plików widocznego dla użytkownika.

Praca programisty nigdy się nie kończy

OPFS to świetna innowacja dla deweloperów aplikacji PWA, która zapewnia naprawdę zaawansowane funkcje zarządzania plikami, które znacznie usprawniają pracę z aplikacjami natywnymi i internetowymi. Ale programiści mają nieszczęśliwą sytuację – nigdy nie są zadowoleni! Narzędzie OPFS jest prawie idealne, ale niezupełnie... Świetnie, że główne funkcje działają zarówno w przeglądarkach Chromium, jak i Firefox, oraz że są zaimplementowane na urządzeniach z Androidem i na komputerach. Mamy nadzieję, że pełny zestaw funkcji zostanie wkrótce wdrożony w Safari i iOS. Pozostały jednak następujące problemy:

  • Obecnie w Firefoksie limit OPFS wynosi 10 GB, niezależnie od ilości dostępnego miejsca na dysku. Dla większości twórców PWA może to wystarczać, ale w przypadku Kiwix ten limit jest dość ograniczony. Na szczęście przeglądarki Chromium są znacznie bardziej rozbudowane.
  • Obecnie nie można eksportować dużych plików z OPFS do systemu plików widocznego dla użytkowników w przeglądarkach mobilnych ani w przeglądarce Firefox na komputerach, ponieważ usługa window.showSaveFilePicker() nie jest zaimplementowana. W takich przeglądarkach duże pliki są skutecznie uchwycone w OPFS. Jest to sprzeczne z zasadami Kiwix, czyli otwartym dostępem do treści i możliwością udostępniania archiwów między użytkownikami, zwłaszcza w obszarach, gdzie połączenie z internetem jest niestabilne lub drogie.
  • Nie ma możliwości kontrolowania, jaką pamięć masową wirtualny system plików OPFS. Jest to szczególnie problematyczne na urządzeniach mobilnych, na których użytkownicy mogą mieć dużo miejsca na karcie microSD, ale bardzo mało pamięci na urządzeniu.

W sumie są to jednak drobne usterki, które stanowią ogromny krok naprzód w zakresie dostępu do plików w PWA. Zespół Kiwix PWA jest bardzo wdzięczny programistom Chromium i osobom, które jako pierwsze zaproponowały i zaprojektowały interfejs File System Access API, oraz za ciężką pracę nad wypracowaniem konsensusu wśród dostawców przeglądarek co do znaczenia Origin Private File System. W przypadku Kiwix JS PWA rozwiązało ono wiele problemów z obsługą aplikacji, które sprawiały trudności w przeszłości, i pomogły nam w zwiększeniu dostępności treści Kiwix dla wszystkich. Wypróbuj progresywną aplikację internetową Kiwix i powiedz deweloperom, co o niej myślisz.

Przydatne materiały o funkcjach PWA znajdziesz na tych stronach: