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

Ludzie zebrani wokół laptopa stojącego na prostym stole. Po lewej stronie plastikowe krzesło. Tło wygląda jak szkoła w kraju rozwijającym się.

W tym studium przypadku omawiamy, jak organizacja non-profit Kiwix korzysta z technologii progresywnych aplikacji internetowych i interfejsu File System Access API, aby umożliwić użytkownikom pobieranie i przechowywanie dużych archiwów internetowych do użytku offline. Dowiedz się więcej o technicznej implementacji kodu związanego z systemem plików Origin Private File System (OPFS), czyli nowej funkcji przeglądarki w ramach aplikacji internetowej Kiwix PWA, która umożliwia lepsze zarządzanie plikami i lepszy dostęp do archiwów bez konieczności uzyskiwania uprawnień. W artykule omówiono wyzwania i potencjalne przyszłe zmiany w tym nowym systemie plików.

Informacje o Kiwix

Po ponad 30 latach od powstania sieci trzecia część światowej populacji wciąż czeka na niezawodny dostęp do internetu – wynika z danych Międzynarodowego Związku Telekomunikacyjnego. Czy to koniec tej historii? Oczywiście, że nie. Pracownicy stowarzyszenia non-profit Kiwix ze Szwajcarii opracowali ekosystem aplikacji i treści open source, który ma na celu udostępnianie wiedzy osobom z ograniczonym lub bez dostępu do internetu. Ich pomysł polega na tym, że jeśli Ty nie masz łatwego dostępu do internetu, ktoś może pobrać dla Ciebie kluczowe zasoby, gdy i gdzie jest dostępny internet, i przechowywać je lokalnie na potrzeby późniejszego korzystania w trybie offline. Wiele ważnych witryn, takich jak Wikipedia, Project Gutenberg, Stack Exchange, a nawet wykłady TED, można teraz przekonwertować na bardzo skompresowane archiwa, zwane plikami ZIM, i odczytywać na bieżąco za pomocą przeglądarki Kiwix.

Archiwa ZIM korzystają z bardzo wydajnej kompresji Zstandard (ZSTD) (starsze wersje używają XZ), głównie do przechowywania HTML, JavaScript i CSS, podczas gdy obrazy są zwykle konwertowane do skompresowanego formatu WebP. Każdy kod ZIM zawiera też adres URL i indeks tytułów. Kluczową rolę odgrywa tu kompresja, ponieważ cała Wikipedia w języku angielskim (6,4 mln artykułów plus obrazy) jest skompresowana do 97 GB po przekonwertowaniu do formatu ZIM. Brzmi to dużo, ale dopiero gdy zdasz sobie sprawę, że cała wiedza ludzkości mieści się na średniej klasy telefonie z Androidem. Dostępne są też liczne mniejsze zasoby, w tym tematyczne wersje Wikipedii, np. z matematyki czy medycyny.

Kiwix oferuje szereg aplikacji natywnych przeznaczonych do użytku na komputerach (Windows/Linux/macOS) oraz na urządzeniach mobilnych (iOS/Android). W tym przypadku skupimy się na progresywnej aplikacji internetowej (PWA), która ma być uniwersalnym i prostym rozwiązaniem dla dowolnego urządzenia z nowoczesną przeglądarką.

Przyjrzymy się wyzwaniom związanym z tworzeniem uniwersalnej aplikacji internetowej, która musi zapewniać szybki dostęp do dużych archiwów treści offline, a także niektóre nowoczesne interfejsy API JavaScript, w szczególności File System Access API i Origin Private File System, które stanowią innowacyjne i ciekawe rozwiązania tych problemów.

Aplikacja internetowa do użytku offline?

Użytkownicy Kiwix to bardzo zróżnicowana grupa o różnych potrzebach, a firma Kiwix ma niewielką lub żadną kontrolę nad urządzeniami i systemami operacyjnymi, z których użytkownicy będą korzystać. Niektóre z tych urządzeń mogą być powolne lub przestarzałe, zwłaszcza w regionach o niskich dochodach. Organizacja Kiwix starała się uwzględnić jak najwięcej przypadków użycia, ale zdała sobie też sprawę, że może dotrzeć do jeszcze większej liczby użytkowników, korzystając z najbardziej uniwersalnego oprogramowania na każdym urządzeniu – przeglądarki. Zainspirowani prawem Atwooda, które mówi, że każda aplikacja, którą można napisać w JavaScript, zostanie w końcu napisana w tym języku, niektórzy deweloperzy Kiwix około 10 lat temu zaczęli przenosić oprogramowanie Kiwix z C++ do JavaScriptu.

Pierwsza wersja tego portu, Kiwix HTML5, była przeznaczona dla niedziałającego już systemu operacyjnego Firefox i dla rozszerzeń przeglądarki. W jądrze znajdował się (i nadal znajduje) silnik kompresji C++ (XZ i ZSTD) skompilowany do pośredniego języka JavaScript ASM.js, a później Wasm lub WebAssembly, za pomocą kompilatora Emscripten. Później zmieniono nazwę na Kiwix JS, a rozszerzenia przeglądarki są nadal aktywnie rozwijane.

Przeglądarka offline Kiwix JS

Wpisz progresywną aplikację internetową (PWA). Rozumiejąc potencjał tej technologii, deweloperzy Kiwix stworzyli dedykowaną wersję PWA aplikacji Kiwix JS i zaczęli dodawać integracje z systemem operacyjnym, które umożliwiłyby aplikacji oferowanie funkcji podobnych do natywnych, zwłaszcza w zakresie korzystania z aplikacji w trybie offline, instalacji, obsługi plików i dostępu do systemu plików.

Aplikacje PWA z funkcją offline są bardzo lekkie, dlatego doskonale sprawdzają się w kontekstach, w których występuje przerywany lub drogi internet mobilny. Technologia, na której to polega, to Service Worker API oraz powiązane Cache API, używane przez wszystkie aplikacje oparte na Kiwix JS. Te interfejsy API umożliwiają aplikacjom działanie jako serwer, przechwytywanie żądań pobierania z głównego dokumentu lub wyświetlanego artykułu i przekierowywanie ich do zaplecza (JS), aby wyodrębnić i utworzyć odpowiedź z archiwum ZIM.

Miejsce na dane wszędzie

Ze względu na duży rozmiar archiwów ZIM, przechowywanie i dostęp do nich, zwłaszcza na urządzeniach mobilnych, jest prawdopodobnie największym problemem dla deweloperów Kiwix. Wielu użytkowników platformy Kiwix pobiera treści z aplikacji, gdy jest dostępny internet, aby później korzystać z nich offline. Inni użytkownicy pobierają pliki na komputerze za pomocą torrenta, a potem przenoszą je na urządzenie mobilne lub tablet. Niektórzy wymieniają treści na pendrive’ach lub przenośnych dyskach twardych w miejscach z niestabilnym lub drogim internetem mobilnym. Wszystkie te sposoby uzyskiwania dostępu do treści z dowolnych lokalizacji dostępnych dla użytkownika muszą być obsługiwane przez Kiwix JS i Kiwix PWA.

Na początku Kiwix JS umożliwiał odczyt ogromnych archiwów o wielokrotności setek GB (jedno z naszych archiwów ZIM ma 166 GB), nawet na urządzeniach o małej ilości pamięci, dzięki interfejsowi File API. Ten interfejs API jest obsługiwany we wszystkich przeglądarkach, nawet bardzo starych, dlatego działa jako uniwersalny mechanizm zastępczy, gdy nowsze interfejsy API nie są obsługiwane. Wystarczy zdefiniować element input w kodzie HTML. W przypadku Kiwix wygląda to tak:

<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 zawiera obiekty File, które są w istocie metadanymi odwołującymi się do danych bazowych w magazynie. Technicznie rzecz biorąc, backend obiektowy Kiwix, napisany w czystym kodzie JavaScript po stronie klienta, odczytuje małe fragmenty dużego archiwum w miarę potrzeby. Jeśli te wycinki muszą zostać zdekompresowane, backend przekazuje je do dekompresora Wasm, uzyskując w razie potrzeby kolejne wycinki, aż do zdekompresowania pełnego bloba (zwykle artykułu lub zasobu). Oznacza to, że duże archiwum nigdy nie musi być w całości odczytywane do pamięci.

Mimo że interfejs File API jest uniwersalny, ma jedną wadę, która sprawia, że aplikacje Kiwix JS wydają się toporne i staroświeckie w porównaniu z aplikacją natywnych: wymagają one od użytkownika wybrania archiwów za pomocą selektora plików lub przeciągnięcia i upuszczenia pliku do aplikacji za każdym razem, gdy aplikacja jest uruchamiana, ponieważ w tym interfejsie API nie ma możliwości trwałego przechowywania uprawnień dostępu z jednej sesji do następnej.

Aby złagodzić ten problem, twórcy Kiwix JS, podobnie jak wielu innych programistów, początkowo zdecydowali się na wykorzystanie Electron. ElectronJS to wspaniały framework, który zapewnia zaawansowane funkcje, w tym pełny dostęp do systemu plików za pomocą interfejsów Node API. Ma on jednak kilka znanych wad:

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

Rozmiar aplikacji Electron jest bardzo duży, ponieważ każda aplikacja zawiera pełną kopię Chromium, w porównaniu z zaledwie 5,1 MB zajmowanego miejsca przez zminimalizowaną i złączoną PWA.

Czy istnieje sposób, aby Kiwix mógł poprawić sytuację użytkowników PWA?

Interfejs File System Access API na ratunek

W 2019 roku Kiwix dowiedziała się o nowym interfejsie API, który był testowany w Chrome 78 (wówczas nazywał się Native File System API). Obiecuje ona możliwość uzyskania uchwytu pliku lub folderu i zapisania go w bazie danych IndexedDB. Co ważne, ten identyfikator jest zachowywany między sesjami aplikacji, dzięki czemu użytkownik nie musi ponownie wybierać pliku ani folderu po ponownym uruchomieniu aplikacji (musi jednak odpowiedzieć na krótkie pytanie o uprawnienia). Do momentu wprowadzenia wersji produkcyjnej nazwa została zmieniona na File System Access API, a jej podstawowe elementy zostały ustandaryzowane przez WhatWG jako File System API (FSA).

Jak działa interfejs API File System Access? Pamiętaj o kilku ważnych kwestiach:

  • Jest to asynchroniczny interfejs API (z wyjątkiem wyspecjalizowanych funkcji w Web Workers).
  • Selektory plików lub katalogów muszą być uruchamiane automatycznie przez wykonanie gestu użytkownika (kliknięcie lub dotknięcie elementu interfejsu).
  • Aby użytkownik ponownie przyznał uprawnienia dostępu do wcześniej wybranego pliku (w ramach nowej sesji), wymagany jest również gest użytkownika. W rzeczywistości przeglądarka odmówi wyświetlenia prośby o przyznanie uprawnień, jeśli nie zostanie zainicjowana gestem użytkownika.

Kod jest stosunkowo prosty, z wyjątkiem konieczności użycia nieporęcznego interfejsu IndexedDB API do przechowywania uchwytów plików i katalogów. Dobra wiadomość jest taka, że istnieje kilka bibliotek, które wykonują za Ciebie większość ciężkiej pracy, np. browser-fs-access. W Kiwix JS zdecydowaliśmy się pracować bezpośrednio z interfejsami API, które są bardzo dobrze udokumentowane.

Otwieranie selektorów plików i katalogów

Otwieranie selektora plików wygląda mniej więcej tak (tutaj przy użyciu funkcji Promises, ale jeśli wolisz async/await cukier, zobacz samouczek Chrome dla programistó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,
    );
  });

Pamiętaj, że ze względu na prostotę ten kod przetwarza tylko pierwszy wybrany plik (i nie pozwala wybrać więcej niż 1 pliku). Jeśli chcesz umożliwić wybór wielu plików za pomocą funkcji { multiple: true }, po prostu owiń wszystkie obietnice, które przetwarzają każde uchwyt, w instrukcji Promise.all().then(...), na przykład:

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

Wybieranie wielu plików jest jednak łatwiejsze, jeśli użytkownik wybierze katalog zawierający te pliki, a nie poszczególne pliki, zwłaszcza że użytkownicy Kiwix zwykle porządkują wszystkie pliki ZIM w tym samym katalogu. Kod uruchamiający selektor katalogów jest prawie taki sam jak powyżej, z tym że używasz instrukcji window.showDirectoryPicker.then(function (dirHandle) { … });.

Przetwarzanie uchwytu pliku lub katalogu

Gdy już masz uchwyt, musisz go przetworzyć, więc funkcja processFileHandle może 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ę do przechowywania uchwytu pliku. Nie ma do tego żadnych metod ułatwiających, chyba że używasz biblioteki abstrakcji. Implementację tego klucza w Kiwix widać w pliku cache.js, ale można to znacznie uprościć, jeśli służy on tylko do przechowywania i pobierania uchwytu pliku lub folderu.

Przetwarzanie katalogów jest nieco bardziej skomplikowane, ponieważ musisz przejść przez wpisy w wybranym katalogu za pomocą asynchronicznego entries.next(), aby znaleźć odpowiednie pliki lub typy plików. Można to zrobić na różne sposoby, ale oto 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 elemencie entryList musisz później pobrać plik z parametrem entry.getFile().then(function (file) { … }), gdy musisz go użyć, lub jego odpowiednikiem używającym const file = await entry.getFile() w async function.

Czy możemy pójść dalej?

Wymóg przyznania uprawnień inicjowanego gestem użytkownika podczas kolejnych uruchomień aplikacji utrudnia (ponowne) otwieranie plików i folderów, ale i tak jest o wiele bardziej płynny niż konieczność ponownego wybrania pliku. Deweloperzy Chromium obecnie finalizują kod, który umożliwi trwałe uprawnienia dla zainstalowanych Progressive Web Apps. Jest to coś, o co prosi wielu deweloperów aplikacji internetowych, i co jest bardzo oczekiwane.

Ale czy nie możemy tego zrobić bez czekania? Deweloperzy Kiwix niedawno odkryli, że można teraz wyeliminować wszystkie prośby o przyznanie uprawnień, korzystając z nowej funkcji interfejsu API dostępu do plików, która jest obsługiwana zarówno przez przeglądarki Chromium, jak i Firefox (a częściowo przez Safari, ale nadal brak FileSystemWritableFileStream). Ta nowa funkcja to system plików prywatnych Origin.

Przejście na system plików natywnych: prywatny system plików Origin

System plików Origin Private File System (OPFS) jest nadal funkcją eksperymentalną w PWA Kiwix, ale zespół chętnie zachęca użytkowników do wypróbowania tej funkcji, ponieważ w dużej mierze zaciera ona różnice między natywną aplikacją a aplikacją internetową. Oto najważniejsze zalety:

  • Do archiwów w OPFS można uzyskać dostęp bez prośby o przyznanie uprawnień, nawet podczas uruchamiania. Użytkownicy mogą wznowić czytanie artykułu i przeglądanie archiwum od miejsca, w którym przerwali w poprzedniej sesji, bez żadnych problemów.
  • Zapewnia wysoce zoptymalizowany dostęp do plików przechowywanych w nim: na Androidzie odnotowaliśmy wzrost szybkości od 5 do 10 razy.

Standardowy dostęp do plików na Androidzie za pomocą interfejsu File API jest niezwykle powolny, zwłaszcza gdy 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). To wszystko zmienia nowy interfejs API. Większość użytkowników nie będzie mogła przechowywać pliku o rozmiary 97 GB w systemie plików OPFS (który zużywa miejsce na urządzeniu, a nie na karcie microSD), ale jest on idealny do przechowywania małych i średnich archiwów. Szukasz najpełniejszej encyklopedii medycznej z WikiProject Medicine? Nie ma problemu – rozmiar 1, 7 GB zmieści się w pliku OPFS. (Wskazówka: poszukaj inne → mdwiki_pl_all_maxibibliotece w aplikacji).

Jak działa OPFS

OPFS to system plików udostępniany przez przeglądarkę, oddzielnie dla każdej domeny. Można go traktować jako podobny do pamięci na Androidzie ograniczonej do aplikacji. Pliki można importować do OPFS z widocznego dla użytkownika systemu plików lub pobierać bezpośrednio do niego (interfejs API umożliwia też tworzenie plików w OPFS). W pliku OPFS są odizolowane od reszty urządzenia. W przeglądarkach na komputerach z systemem operacyjnym Chromium można też eksportować pliki z OPFS do widocznego dla użytkownika systemu plików.

Aby korzystać z OPFS, musisz najpierw poprosić o dostęp za pomocą funkcji navigator.storage.getDirectory() (jeśli wolisz zobaczyć kod za pomocą funkcji await, przeczytaj System plików 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);
  });

Uzyskany w ten sposób identyfikator jest tego samego typu, co FileSystemDirectoryHandle uzyskany z wymienionego powyżej window.showDirectoryPicker(), co oznacza, że możesz ponownie użyć kodu, który go obsługuje (na szczęście nie musisz przechowywać tego w indexedDB – po prostu pobierz go, gdy go potrzebujesz). Załóżmy, że masz już jakieś pliki w pliku OPFS i chcesz ich użyć, a potem korzystając z pokazanej wcześniej funkcji iterateAsyncDirEntries(), możesz wykonać 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ć funkcji getFile() do każdego elementu, z którym chcesz pracować w tablicy archiveList.

Importowanie plików do OPFS

Jak w ogóle dodać pliki do OPFS? Nie tak szybko! Najpierw musisz oszacować ilość miejsca na dane, z którego chcesz korzystać, i zadbać o to, aby użytkownicy nie próbowali przesłać pliku o rozmiarach 97 GB, jeśli nie mieści się on w ramach dostępnego miejsca.

Aby uzyskać szacowany limit: navigator.storage.estimate().then(function (estimate) { … });. Nieco trudniejszym zadaniem jest ustalenie, jak wyświetlić te informacje użytkownikowi. W aplikacji Kiwix zdecydowaliśmy się na mały panel w aplikacji widoczny tuż obok pola wyboru, który pozwala użytkownikom wypróbować OPFS:

Panel pokazujący wykorzystane miejsce na dane w procentach i pozostałe dostępne miejsce w gigabajtach.

Panel jest wypełniany za pomocą zmiennych 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>';
});

Jak widać, dostępny jest również przycisk, który pozwala użytkownikom dodawać pliki do pliku OPFS z poziomu systemu plików widocznego dla użytkowników. Dobra wiadomość jest taka, że możesz po prostu użyć interfejsu File API, aby pobrać potrzebne obiekty File, które mają zostać zaimportowane. W fakcie nie należy używać window.showOpenFilePicker(), ponieważ ta metoda nie jest obsługiwana przez Firefoxa, a OPFS jest zdecydowanie obsługiwana.

Widoczny na zrzucie ekranu powyżej przycisk Dodaj pliki nie jest starszym selektorem plików, ale click()ukrytym starszym selektorem (element <input type="file" multiple … />) po kliknięciu. Aplikacja rejestruje zdarzenie change dla ukrytego pliku wejściowego, sprawdza rozmiar plików i odrzuca je, jeśli są za duże w stosunku do limitu. Jeśli wszystko jest w porządku, zapytaj użytkownika, czy chce go 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 dialogowe z pytaniem, czy użytkownik chce dodać listę plików .zim do prywatnego systemu plików źródłowego.

W niektórych systemach operacyjnych, np. Androidzie, importowanie archiwów nie jest najszybszą operacją, dlatego Kiwix wyświetla baner i małe kółko, gdy importuje archiwa. Zespół nie wiedział, jak dodać wskaźnik postępu dla tej operacji. Jeśli wiesz, jak to zrobić, napisz do nas.

Jak Kiwix zaimplementował funkcję importOPFSEntries()? Polega to na użyciu metody fileHandle.createWriteable(), która umożliwia przesyłanie każdego pliku do systemu plików OPFS. Cała ciężka praca jest wykonywana przez przeglądarkę. (Kiwix używa tu obietnic ze względu na naszą starszą bazę kodu, ale należy zauważyć, że w tym przypadku await tworzy prostszą składnię i unika efektu piramidy zniszczenia).

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

Wariantem tej opcji jest możliwość przesyłania strumieniowego pliku z internetu bezpośrednio do pliku OPFS lub do dowolnego katalogu, dla którego masz uchwyt katalogu (czyli katalogów wybranych za pomocą atrybutu window.showDirectoryPicker()). Stosuje się w nim te same zasady co w powyższym kodzie, ale konstruuje element Response składający się z ReadableStream i kontroler, który dodaje do kolejki bajty odczytane z pliku zdalnego. Uzyskana wartość Response.body jest następnie przekierowywana do nowej funkcji zapisu pliku w OPFS.

W takim przypadku Kiwix może zliczać bajty przechodzące przez ReadableStream, aby wyświetlać użytkownikowi wskaźnik postępu i ostrzegać go przed zamykaniem aplikacji podczas pobierania. Kod jest zbyt skomplikowany, aby go tu prezentować, ale ponieważ nasza aplikacja jest wolnym oprogramowaniem, możesz zapoznać się z jej źródłem, jeśli chcesz zrobić coś podobnego. Interfejs Kiwix wygląda tak (różne wartości postępu wyświetlane poniżej są spowodowane tym, że baner jest aktualizowany tylko wtedy, gdy zmienia się odsetek, ale panel Postęp pobierania jest aktualizowany częściej):

Interfejs Kiwix z paskiem na dole ostrzeżeniem, aby nie zamykać aplikacji, oraz informacją o postępie pobierania archiwum Zim.

Ponieważ pobieranie może być dość czasochłonną operacją, Kiwix pozwala użytkownikom na swobodne korzystanie z aplikacji podczas jej trwania, ale zapewnia, że baner jest zawsze wyświetlany, aby użytkownicy nie zamykali aplikacji, dopóki pobieranie nie zostanie ukończone.

Wdrożenie minimenedżera plików w aplikacji

Na tym etapie twórcy aplikacji Kiwix PWA zdali sobie sprawę, że to za mało, aby dodać pliki do pliku OPFS. Aplikacja musiała też umożliwić użytkownikom usuwanie plików, których nie potrzebują już z tego obszaru pamięci, oraz w idealnym przypadku eksportowanie wszystkich plików zablokowanych w OPFS z powrotem do widocznego dla użytkownika systemu plików. W praktyce okazało się, że w aplikacji trzeba wdrożyć mini system zarządzania plikami.

Tutaj krótka wzmianka o świetnym rozszerzeniu OPFS Explorer do Chrome (działa też w Edge). Dodaje ona kartę w narzędziach dla deweloperów, która umożliwia wyświetlanie zawartości OPFS oraz usuwanie nieprawidłowych lub uszkodzonych plików. Było to nieocenione narzędzie do sprawdzania, czy kod działa, monitorowania zachowania pobierania i ogólnego porządkowania eksperymentów związanych z rozwojem.

Eksportowanie pliku zależy od możliwości uzyskania uchwytu pliku w wybranym pliku lub katalogu, w którym Kiwix ma zapisać wyeksportowany plik. Działa to tylko w kontekstach, w których można użyć metody window.showSaveFilePicker(). Gdyby pliki Kiwix były mniejsze niż kilka GB, moglibyśmy utworzyć obiekt blob w pamięci, podać mu adres URL, a następnie pobrać go do systemu plików widocznego dla użytkowników. Niestety, nie jest to możliwe przy tak dużych archiwach. Jeśli eksport jest obsługiwany, eksportowanie jest praktycznie takie samo jak w odwrotnym przypadku, jak zapisanie pliku w formacie OPFS (uzyskaj nick pliku do zapisania, poproś użytkownika o wybranie lokalizacji, w której zostanie zapisany za pomocą window.showSaveFilePicker(), a następnie użyj polecenia createWriteable() w saveHandle). Kod znajdziesz w repozytorium.

Usuwanie plików jest obsługiwane przez wszystkie przeglądarki i może być wykonywane za pomocą prostego dirHandle.removeEntry('filename'). W przypadku Kiwix preferowaliśmy iterację wpisów OPFS, aby najpierw sprawdzić, czy wybrany plik istnieje, i poprosić o potwierdzenie, ale nie jest to konieczne dla wszystkich. W razie potrzeby możesz sprawdzić kod.

Postanowiliśmy nie zaśmiecać interfejsu Kiwix przyciskami oferującymi te opcje, a zamiast tego umieścić małe ikony bezpośrednio pod listą archiwów. Kliknięcie jednej z tych ikon spowoduje zmianę koloru listy archiwów, co będzie wizualną wskazówką dla użytkownika na temat tego, co zamierza zrobić. Użytkownik następnie klika jedno z archiwów, a odpowiednia operacja (eksportowanie lub usuwanie) zostanie wykonana (po potwierdzeniu).

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

Na koniec przedstawiamy film demonstrujący wszystkie funkcje zarządzania plikami omówione powyżej: dodawanie pliku do OPFS, bezpośrednie pobieranie pliku do tego systemu, usuwanie pliku oraz eksportowanie do widocznego dla użytkownika systemu plików.

Praca dewelopera nigdy się nie kończy

OPFS to świetna innowacja dla programistów aplikacji PWA. Zapewnia ona bardzo zaawansowane funkcje zarządzania plikami, które znacznie zmniejszają różnice między aplikacjami natywnymi a internetowymi. Programiści to jednak tragiczni – nigdy nie są zbyt zadowoleni. OPFS jest prawie idealny, ale nie do końca… Świetnie, że główne funkcje działają zarówno w przeglądarkach Chromium, jak i Firefox, oraz że są one dostępne na Androidzie i komputerach. Mamy nadzieję, że wkrótce udostępnimy pełny zestaw funkcji również w Safari i iOS. Pozostały te problemy:

  • Firefox obecnie ogranicza limit OPFS do 10 GB niezależnie od ilości dostępnego miejsca na dysku. Chociaż dla większości autorów PWA może to być wystarczające, w przypadku Kiwix jest to dość restrykcyjne. Na szczęście przeglądarki oparte na Chromium są znacznie bardziej szczodre.
  • Obecnie nie można eksportować dużych plików z formatu OPFS do systemu plików widocznego dla użytkowników w przeglądarkach mobilnych ani w Firefoksie na komputerze, ponieważ nie zaimplementowano window.showSaveFilePicker(). Duże pliki w takich przeglądarkach są uwięzione w plikach OPFS. Jest to sprzeczne z ideą Kiwix, która zakłada otwarty dostęp do treści i możliwość udostępniania archiwów innym użytkownikom, zwłaszcza w miejscach o przerywanej lub drogiej łączności z internetem.
  • Użytkownik nie może kontrolować, jakiej pamięci będzie używać 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 na pamięci urządzenia.

Ogólnie rzecz biorąc, są to drobne niedogodności w ogólnym ujęciu, które jest ogromnym krokiem naprzód w przypadku dostępu do plików w aplikacji internetowej. Zespół Kiwix PWA dziękuje programistom i proponującym Chromium, którzy jako pierwsi zaproponowali i zaprojektowali interfejs API dostępu do systemu plików. Dziękujemy też za ciężką pracę nad osiągnięciem konsensusu wśród dostawców przeglądarek w kwestii znaczenia systemu plików Origin Private. W przypadku aplikacji Kiwix JS PWA rozwiązaliśmy wiele problemów z UX, które w przeszłości utrudniały korzystanie z aplikacji, oraz ułatwiliśmy dostęp do treści Kiwix wszystkim użytkownikom. Wypróbuj aplikację PWA Kiwix i powiedz deweloperom, co o niej myślisz.

Warto zapoznać się z tymi przydatnymi materiałami na temat możliwości aplikacji PWA: