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 plastikowy fotel. Tło wygląda jak szkoła w kraju rozwijającym się.

To studium przypadku pokazuje, 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 na potrzeby korzystania w trybie 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 PWA Kiwix, która umożliwia lepsze zarządzanie plikami i lepszy dostęp do archiwów bez konieczności udzielania uprawnień. W artykule omawiamy 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, jak podaje Międzynarodowy Związek Telekomunikacyjny. Czy na tym kończy się ta historia? Oczywiście, że nie. Pracownicy stowarzyszenia Kiwix ze Szwajcarii stworzyli ekosystem aplikacji i treści open source, aby udostępnić wiedzę 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, a obrazy są zwykle konwertowane do skompresowanego formatu WebP. Każdy plik 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ą.

Omówimy wyzwania związane z tworzeniem uniwersalnej aplikacji internetowej, która musi zapewniać szybki dostęp do dużych archiwów treści w trybie całkowicie offline, oraz niektóre nowoczesne interfejsy JavaScript API, w tym File System Access APIOrigin Private File System, które zapewniają 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. Chociaż Kiwix stara się uwzględniać jak najwięcej przypadków użycia, organizacja zdała sobie sprawę, że może dotrzeć do jeszcze większej liczby użytkowników, korzystając z najbardziej uniwersalnego oprogramowania na dowolnym urządzeniu: przeglądarki internetowej. 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 do nieistniejącego już systemu operacyjnego Firefox i 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 adres progresywnej aplikacji internetowej (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, dzięki czemu 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 Kiwix pobiera treści w aplikacji, gdy jest dostępna sieć, aby później korzystać z nich w trybie 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 miejsc 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 źródłowych 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 fragmenty wymagają dekompresji, backend przekazuje je do dekompresora Wasm, uzyskując kolejne fragmenty (jeśli to konieczne), aż do dekompresji pełnego bloba (zazwyczaj 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 aplikacji Kiwix JS początkowo zdecydowali się na wykorzystanie Electron, tak jak wielu innych programistów. 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 do zaledwie 5,1 MB zajmowanego 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). W czasie, gdy trafił do wersji produkcyjnej, został przemianowany na File System Access API, a jego główne części zostały skodyfikowane 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).
  • Wybieranie plików lub katalogów musi być uruchamiane programowo przez rejestrowanie gestów użytkownika (kliknięcie lub dotknięcie elementu interfejsu).
  • Aby użytkownik ponownie zezwolił na dostęp do wcześniej wybranego pliku (w nowej sesji), wymagany jest również gest użytkownika. Przeglądarka nie wyświetli prośby o udzielenie uprawnień, jeśli nie zostanie ona zainicjowana przez 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 używamy obietnic, ale jeśli wolisz async/await sugar, zapoznaj się z samouczkiem 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 tą różnicą, że używasz instrukcjiwindow.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. Wdrożenie tego w Kiwix można zobaczyć w pliku cache.js, ale można je znacznie uprościć, jeśli będzie ono służyć 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 pliku entryList musisz później pobrać plik z wartością entry.getFile().then(function (file) { … }), gdy chcesz go użyć, lub odpowiednik z wartością const file = await entry.getFile() w pliku async function.

Czy możemy jeszcze coś zrobić?

Wymaganie od użytkownika przyznania uprawnień poprzedzone gesturem użytkownika podczas kolejnych uruchamiań aplikacji powoduje niewielkie opóźnienie podczas otwierania (ponownego) plików i folderów, ale jest to nadal bardziej płynne 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.

A co, jeśli nie musimy czekać? 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 prywatny system plików 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 wypełnia ona lukę między aplikacjami natywnymi a aplikacją internetową. Oto najważniejsze zalety:

  • Do archiwów w OPFS można uzyskać dostęp bez wyświetlenia 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 prędkość jest 5–10 razy większa.

Standardowy dostęp do plików na Androidzie za pomocą interfejsu File API jest bardzo powolny, zwłaszcza (jak to często ma miejsce w przypadku użytkowników Kiwix) w przypadku dużych archiwów przechowywanych na karcie microSD, a nie na pamięci urządzenia. To wszystko zmienia nowy interfejs API. Większość użytkowników nie będzie mogła przechowywać pliku o rozmiarach 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. Chcesz zobaczyć najbardziej kompletną encyklopedię medyczną z WikiProject Medicine? Nie ma problemu. Plik o rozmiarze 1, 7 GB zmieści się w 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). Po umieszczeniu w OPFS są one izolowane 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 The Origin Private File System):

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

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ż w OPFS pewne pliki i chcesz z nich skorzystać. Korzystając z funkcji iterateAsyncDirEntries() pokazanej wcześniej, możesz wykonać takie czynności:

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

Jak widać, jest też przycisk, który umożliwia użytkownikom dodawanie plików do OPFS z widocznego dla użytkownika systemu plikó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.

Widzialny przycisk Dodaj pliki na powyższym zrzucie ekranu nie jest starszą wersją selektora plików, ale po kliknięciu click() uruchamia ukryty selektor starszej wersji (element <input type="file" multiple … />). 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 się zgadza, zapytaj użytkownika, czy chce dodać te elementy:

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 zapewnia 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

Inną możliwością jest przesyłanie strumieniowe pliku z Internetu bezpośrednio do OPFS lub do dowolnego katalogu, do którego masz uchwyt katalogu (czyli katalogów wybranych za pomocą window.showDirectoryPicker()). Ta metoda wykorzystuje te same zasady co kod powyżej, ale tworzy Response składający się z ReadableStream i kontrolera, który umieszcza w kolejce bajty odczytane z dalekiego pliku. 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 u dołu, który ostrzega użytkownika przed zamykaniem aplikacji i wyświetla postęp 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 przypominać użytkownikom, aby nie zamykali aplikacji, dopóki nie zakończy się pobieranie.

Implementacja w aplikacji mini menedżera plików

W tym momencie deweloperzy PWA w Kiwix zdali sobie sprawę, że dodawanie plików do OPFS to za mało. Aplikacja musiała też umożliwić użytkownikom usuwanie plików, których nie potrzebują już z tego obszaru pamięci, a także eksportować wszystkie pliki zablokowane w OPFS z powrotem do widocznego dla użytkownika systemu plików. W praktyce okazało się konieczne wdrożenie w aplikacji mini systemu 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(). Jeśli pliki Kiwix byłyby mniejsze niż kilka GB, moglibyśmy utworzyć bloba w pamięci, nadać mu adres URL, a następnie pobrać do widocznego dla użytkownika systemu plików. Niestety nie jest to możliwe w przypadku tak dużych archiwów. Jeśli jest obsługiwane, eksportowanie jest dość proste: jest to praktycznie odwrotność zapisywania pliku na OPFS (uzyskaj uchwyt pliku, poproś użytkownika o wybranie lokalizacji zapisu za pomocą window.showSaveFilePicker(), a potem użyj createWriteable() na saveHandle). Zobacz kod 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. Jeśli chcesz, możesz przeanalizować nasz 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 klika jedno z archiwów, a po potwierdzeniu wykonuje odpowiednią operację (eksportowanie lub usuwanie).

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. Ale deweloperzy to nieszczęśnicy – nigdy nie są w pełni 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. Nadal występują 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 OPFS do widocznego dla użytkownika systemu plików w przeglądarkach mobilnych ani w przeglądarce Firefox na komputerze, ponieważ nie została zaimplementowana funkcja window.showSaveFilePicker(). W tych przeglądarkach duże pliki są skutecznie przechowywane w 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 wszystkim użytkownikom dostęp do treści Kiwix. Wypróbuj aplikację PWA Kiwixpowiedz deweloperom, co o niej myślisz.

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