Kiwix PWA でインターネットからギガバイト単位のデータをオフライン保存する方法

左側にプラスチック製の椅子があり、シンプルなテーブルの上に置かれたノートパソコンの周りに集まっている人々。背景は発展途上国の学校のような見た目です。

このケーススタディでは、非営利団体の Kiwix が、プログレッシブ ウェブアプリ テクノロジーと File System Access API を使用して、ユーザーが大規模なインターネット アーカイブをダウンロードして保存し、オフラインで使用できるようにする方法を紹介します。Origin Private File System(OPFS)を扱うコードの技術的な実装について学びます。OPFS は Kiwix PWA の新しいブラウザ機能で、ファイル管理を強化し、権限プロンプトなしでアーカイブにアクセスできるようにします。この記事では、この新しいファイル システムの課題と、将来の開発の可能性について説明します。

Kiwix について

国際電気通信連合によると、ウェブ誕生から 30 年以上経った現在でも、世界人口の 3 分の 1 が信頼性の高いインターネット接続を待っている状況です。これで物語は終わりますか?答えはもちろん「ノー」であり、スイス拠点の非営利団体である Kiwix は、インターネットにアクセスできない、またはアクセスが制限されている人々に知識を提供する目的で、オープンソースのアプリとコンテンツのエコシステムを開発しました。ユーザーがインターネットに簡単にアクセスできない場合、接続が可能な場所や時間に、誰かが重要なリソースをダウンロードし、ローカルに保存して、後でオフラインで使用できるようにするという考え方です。Wikipedia、Project Gutenberg、Stack Exchange、TED の講演など、多くの重要なサイトを ZIM ファイルと呼ばれる高圧縮アーカイブに変換し、Kwix ブラウザでその場で読むことができるようになりました。

ZIM アーカイブは、主に HTML、JavaScript、CSS の保存に、非常に効率的な Zstandard(ZSTD)圧縮(以前のバージョンでは XZ を使用)を使用します。画像は通常、圧縮された WebP 形式に変換されます。各 ZIM には URL とタイトル インデックスも含まれています。ここでは圧縮が重要です。英語の Wikipedia 全体(640 万件の記事と画像)は、ZIM 形式に変換すると 97 GB に圧縮されます。これは、人間の知識のすべてがミッドレンジの Android スマートフォンに収まることを考えると、かなりの量に思えます。数学や医学など、テーマ別のウィキペディアなど、多くの小規模なリソースも提供されています。

Kiwix には、デスクトップ(Windows / Linux / macOS)とモバイル(iOS / Android)向けのさまざまなネイティブ アプリが用意されています。このケーススタディでは、最新のブラウザを搭載したすべてのデバイスに対応するユニバーサルでシンプルなソリューションを目指すプログレッシブ ウェブアプリ(PWA)に焦点を当てます。

完全にオフラインで大規模なコンテンツ アーカイブに高速にアクセスする必要があるユニバーサル ウェブアプリの開発に伴う課題と、これらの課題に革新的で魅力的なソリューションを提供する最新の JavaScript API(特に File System Access APIオリジンのプライベート ファイル システム)について説明します。

オフライン ウェブアプリ

Kiwix のユーザーは、さまざまなニーズを持つ多様な集団であり、ユーザーがコンテンツにアクセスするデバイスとオペレーティング システムを Kiwix が管理することはほとんどありません。特に低所得地域では、これらのデバイスの一部が遅かったり、古くなっていたりすることがあります。Kiwix は、できるだけ多くのユースケースに対応しようとしていますが、あらゆるデバイスで最も汎用性の高いソフトウェアであるウェブブラウザを使用することで、さらに多くのユーザーにリーチできることにも気づきました。そのため、JavaScript で記述できるアプリケーションは、最終的には JavaScript で記述されるというアトウッドの法則に触発され、一部の Kiwix デベロッパーは 10 年ほど前に、Kiwix ソフトウェアを C++ から JavaScript に移植することにしました。

このポートの最初のバージョンである Kiwix HTML5 は、現在はサポートが終了している Firefox OS とブラウザ拡張機能向けでした。中核となるのは、Emscripten コンパイラを使用して、ASM.js の中間 JavaScript 言語にコンパイルされた C++ 圧縮エンジン(XZ と ZSTD)です。これは現在も同じです。その後、Wasm または WebAssembly にコンパイルされました。後に Kiwix JS という名前が付けられましたが、ブラウザ拡張機能は現在も開発が続けられています。

Kiwix JS オフライン ブラウザ

プログレッシブ ウェブアプリ(PWA)を入力します。この技術の可能性を認識した Kiwix デベロッパーは、Kiwix JS の専用の PWA バージョンを構築し、特にオフライン使用、インストール、ファイル処理、ファイル システム アクセスの分野で、アプリがネイティブのような機能を提供できるように OS 統合を追加しました。

オフライン ファーストの PWA は非常に軽量であるため、モバイルインターネットが断続的であるか高価なコンテキストに最適です。この背後にあるテクノロジーは、Service Worker API と関連する Cache API です。これは、Kiwix JS をベースとするすべてのアプリで使用されます。これらの API を使用すると、アプリはサーバーとして機能し、表示されているメインのドキュメントまたは記事から取得リクエストをインターセプトして(JS)バックエンドにリダイレクトし、ZIM アーカイブからレスポンスを抽出して作成できます。

ストレージ、場所を問わないストレージ

ZIM アーカイブのサイズが大きいため、特にモバイル デバイスでの保存とアクセスは、Kiwix デベロッパーにとって最大の悩みの種でしょう。多くの Kiwix エンドユーザーは、インターネットが利用できるときに、後でオフラインで使用するために、アプリ内でコンテンツをダウンロードします。他のユーザーは、パソコンでトレントを使用したダウンロード後にモバイル デバイスやタブレット デバイスに転送します。また、モバイル インターネットが不安定な地域や高額な地域では、USB メモリやポータブル ハードドライブでコンテンツを交換することもあります。ユーザーがアクセス可能な任意の場所からコンテンツにアクセスする方法はすべて、Kiwix JS と Kiwix PWA でサポートされている必要があります。

当初、Kiwix JS が低メモリのデバイスでも数百 GB もの巨大なアーカイブ(ZIM アーカイブの1 つは 166 GB)を読み取れたのは、File API のおかげです。この API は、非常に古いブラウザでも、すべてのブラウザでサポートされているため、新しい API がサポートされていない場合に、ユニバーサル フォールバックとして機能します。これは、HTML で input 要素を定義するのと同じくらい簡単です。Kiwix の場合は次のようになります。

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

選択すると、入力要素は File オブジェクトを保持します。これは、基本的にストレージ内の基盤となるデータを参照するメタデータです。技術的には、純粋なクライアントサイド JavaScript で記述された Kiwix のオブジェクト指向バックエンドが、必要に応じて大規模なアーカイブの小さなスライスを読み取ります。これらのスライスを解凍する必要がある場合、バックエンドはそれらを Wasm 解凍ツールに渡し、リクエストに応じてさらにスライスを取得して、完全な blob(通常は記事またはアセット)を解凍します。つまり、大規模なアーカイブをメモリに完全に読み込む必要はありません。

File API には、ネイティブ アプリに比べて、Kiwix JS アプリが動作がぎこちなく、古く感じられるという欠点があります。この API では、あるセッションから次のセッションへのアクセス権限を保持する方法がないため、ユーザーはファイル選択ツールを使用してアーカイブを選択するか、アプリを起動するたびにアプリにファイルをドラッグ&ドロップする必要があります。

この UX の低下を軽減するために、多くのデベロッパーと同様に、Kiwix JS デベロッパーは最初は Electron のルートを使用しました。ElectronJS は、Node API を使用したファイル システムへの完全アクセスなど、強力な機能を提供する優れたフレームワークです。ただし、次のようなよく知られた欠点があります。

  • デスクトップ オペレーティング システムでのみ動作します。
  • ファイルサイズは大きくなります(70MB ~ 100MB)。

Electron アプリのサイズは、すべてのアプリに Chromium の完全なコピーが含まれているため、最小化されたバンドルの PWA のわずか 5.1 MB と比べて非常に大きくなります。

では、Kwix が PWA のユーザーの状況を改善できる方法はあるでしょうか。

File System Access API の登場

2019 年頃、Kwix は、Chrome 78 でオリジン トライアルが進行中で、Native File System API と呼ばれる新たな API に気づきました。ファイルまたはフォルダのファイル ハンドルを取得して IndexedDB データベースに保存する機能が保証されています。重要なのは、このハンドルはアプリ セッション間で保持されるため、アプリを再起動してもユーザーがファイルやフォルダを再度選択する必要がないことです(ただし、簡単な権限プロンプトに応答する必要があります)。製品版になるまでに、名前が File System Access API に変更され、コア部分は WHATWG によって File System API(FSA)として標準化されました。

では、API のファイル システム アクセス部分はどのように機能するのでしょうか。重要な留意点:

  • 非同期 API である(ウェブ ワーカーの特殊な関数は除く)。
  • ファイルまたはディレクトリの選択ツールは、ユーザー操作(UI 要素のクリックまたはタップ)をキャプチャして、プログラムで起動する必要があります。
  • ユーザーが(新しいセッションで)以前に選択したファイルにアクセスするための権限を再度付与するには、ユーザー操作も必要です。実際、ブラウザは、ユーザー操作で開始されなければ、権限プロンプトの表示を拒否します。

ファイルとディレクトリのハンドルを保存するために、扱いにくい IndexedDB API を使用する必要があることを除けば、コードは比較的簡単です。ただし、browser-fs-access など、面倒な処理をしてくれるライブラリがいくつかあります。 Kiwix JS では、非常によくドキュメント化されている API を直接使用することにしました。

ファイルとディレクトリの選択ツールを開く

ファイル選択ツールを開くには、次のようにします(ここでは Promise を使用していますが、async/await 砂糖を使用する場合は、Chrome for Developers チュートリアルをご覧ください)。

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

わかりやすくするため、このコードでは選択された最初のファイルのみを処理します(複数のファイルを選択することはできません)。{ multiple: true } を使用して複数のファイルを選択できるようにするには、各ハンドルを処理するすべての Promise を Promise.all().then(...) ステートメントでラップするだけです。次に例を示します。

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

ただし、複数のファイルを選択する場合は、Kiwix ユーザーはすべての ZIM ファイルを同じディレクトリに整理する傾向があるため、個々のファイルではなく、それらのファイルを含むディレクトリを選択するようユーザーに求めるほうが適切です。ディレクトリ選択ツールを起動するコードは、window.showDirectoryPicker.then(function (dirHandle) { … }); を使用する点を除き、上記とほぼ同じです。

ファイルまたはディレクトリ ハンドルの処理

ハンドルを取得したら、それを処理する必要があります。そのため、processFileHandle 関数は次のようになります。

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

ファイル ハンドルを保存する関数を用意する必要があります。抽象化ライブラリを使用する場合を除き、これを行うための便利なメソッドはありません。Kiwix でのこの実装はファイル cache.js で確認できますが、ファイルまたはフォルダ ハンドルの保存と取得にのみ使用する場合、大幅に簡素化できます。

ディレクトリの処理は、選択したディレクトリ内のエントリを非同期 entries.next() で反復処理して、目的のファイルまたはファイル形式を見つける必要があるため、少し複雑です。方法はいくつかありますが、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);
    });
}

entryList のエントリごとに、後で使用するときに entry.getFile().then(function (file) { … }) でファイルを取得する必要があります。または、async functionconst file = await entry.getFile() を使用して同等のファイルを取得する必要があります。

さらに進めてもらえますか?

アプリの起動時にユーザー ジェスチャーで開始される権限を付与する必要があるため、ファイルやフォルダの(再)開きに若干の煩わしさが生じますが、ファイルを再選択する必要がある場合よりもはるかにスムーズです。現在、Chromium デベロッパーは、インストールされた PWA の永続的な権限を可能にするコードの最終調整を行っています。これは、多くの PWA デベロッパーが求めていた機能であり、多くのデベロッパーが待ち望んでいた機能です。

でも、待たなくてもいい場合もあるの?Kiwix の開発チームは、Chromium と Firefox の両方のブラウザでサポートされている File Access API の新機能を使用することで、すべての権限プロンプトを今すぐ削除できることを発見しました(Safari で部分的にサポートされていますが、FileSystemWritableFileStream が不足しています)。この新機能は、オリジン プライベート ファイル システムです。

完全にネイティブ化: 送信元の非公開ファイル システム

オリジンの非公開ファイル システム(OPFS)は、Kiwix PWA の試験運用版機能ですが、ネイティブ アプリとウェブアプリのギャップを大幅に埋めるため、チームはユーザーにぜひ試していただくようおすすめしています。主なメリットは次のとおりです。

  • OPFS 内のアーカイブには、起動時でも権限プロンプトなしでアクセスできます。ユーザーは、前のセッションで中断したところから、記事の読み直しやアーカイブのブラウジングをスムーズに再開できます。
  • 保存されているファイルへのアクセスを高度に最適化します。Android では、速度が 5~10 倍向上します。

Android で File API を使用して標準のファイルにアクセスすると、特に大規模なアーカイブがデバイスのストレージではなく microSD カードに保存されている場合(Kiwix ユーザーによくあるケースです)、非常に遅くなります。この新しい API では、この点がすべて変わります。ほとんどのユーザーは、OPFS(microSD カードの保存容量ではなくデバイスの保存容量を使用する)に 97 GB のファイルを保存することはできませんが、小規模から中規模のアーカイブの保存には最適です。WikiProject Medicine の最も包括的な医学百科事典をご覧になりたいですか?1.7 GB なので、OPFS に簡単に収まります。(ヒント: アプリ内ライブラリothermdwiki_en_all_maxi を探します)。

OPFS の仕組み

OPFS は、ブラウザが提供するファイル システムで、オリジンごとに分離されています。これは、Android のアプリ スコープ ストレージに似ていると見なすことができます。ファイルは、ユーザーに表示されるファイル システムから OPFS にインポートすることも、OPFS に直接ダウンロードすることもできます(API では OPFS にファイルを作成することもできます)。OPFS に保存されたデータは、デバイスの他の部分から分離されます。PC の Chromium ベースのブラウザでは、OPFS からユーザーに表示されるファイル システムにファイルをエクスポートすることもできます。

OPFS を使用するには、まず navigator.storage.getDirectory() を使用して OPFS へのアクセスをリクエストします(繰り返しになりますが、await を使用してコードを確認する場合は、送信元のプライベート ファイル システムをご覧ください)。

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

このハンドルから得られるハンドルは、前述の window.showDirectoryPicker() から取得する FileSystemDirectoryHandle とまったく同じタイプです。つまり、そのハンドルを処理するコードを再利用できます(これを indexedDB に保存する必要はありません。必要なときだけ取得してください)。OPFS にすでにいくつかのファイルがあり、それらを使用するとします。次に、前述の iterateAsyncDirEntries() 関数を使用して、次のようにします。

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

archiveList 配列から操作するエントリには、引き続き getFile() を使用する必要があります。

OPFS へのファイルのインポート

そもそも、ファイルを OPFS に取り込むにはどうすればよいのでしょうか。まだ間に合いません。まず、作業する必要があるストレージ容量を見積もり、収まらない 97 GB のファイルをユーザーが保存しないようにする必要があります。

割り当ての見積もりは簡単に取得できます: navigator.storage.estimate().then(function (estimate) { … });。少し難しいのは、これをユーザーに表示する方法です。Kiwix アプリでは、チェックボックスのすぐ横に小さなアプリ内パネルを表示し、ユーザーが OPFS を試すようにしました。

使用済みストレージの割合と残りの空き容量がギガバイト単位で表示されたパネル。

パネルには、estimate.quotaestimate.usage を使用して値が入力されます。次に例を示します。

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

ご覧のとおり、ユーザーがユーザーに表示されるファイル システムから OPFS にファイルを追加できるボタンもあります。幸い、File API を使用して、インポートする必要な File オブジェクトを取得できます。実際、window.showOpenFilePicker() は使用しないことが重要です。この方法は Firefox ではサポートされていませんが、OPFS は確実にサポートされています。

上のスクリーンショットに表示されている [ファイルを追加] ボタンは、以前のファイル選択ツールではありませんが、クリックまたはタップすると、非表示の以前の選択ツール(<input type="file" multiple … /> 要素)をclick()表示します。アプリは、非表示のファイル入力の change イベントをキャプチャし、ファイルのサイズをチェックし、割り当てを超えている場合は拒否します。問題がなければ、追加するかどうかをユーザーに確認します。

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

元の非公開ファイル システムに .zim ファイルのリストを追加するかどうかをユーザーに尋ねるダイアログ。

Android などの一部のオペレーティング システムでは、アーカイブのインポートが最も高速な操作ではないため、Kiwix ではアーカイブのインポート中にバナーと小さなスピナーも表示されます。このオペレーションの進行状況インジケーターを追加する方法は、チームでは考えられませんでした。解決できた場合は、ハガキで回答してください。

Kiwix はどのように importOPFSEntries() 関数を実装したのでしょうか。これには、fileHandle.createWriteable() メソッドの使用が含まれます。これにより、各ファイルを OPFS に効果的にストリーミングできるようになります。面倒な作業はすべてブラウザが処理します。(Kiwix では、レガシー コードベースに関連する理由で Promise を使用していますが、この場合、await はよりシンプルな構文を生成し、ピラミッド オブ ドゥーム効果を回避します)。

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

ファイル ストリームを OPFS に直接ダウンロードする

これのバリエーションとして、インターネットから OPFS に直接、またはディレクトリ ハンドル(window.showDirectoryPicker() で選択したディレクトリ)がある任意のディレクトリにファイルをストリーミングできます。これは上記のコードと同じ原則を使用しますが、ReadableStream と、リモート ファイルから読み取ったバイトをキューに入れるコントローラで構成される Response を構築します。生成された Response.body は、OPFS 内の新しいファイルの書き込み元にパイプされます。

この場合、Kiwix は ReadableStream を通過するバイト数をカウントできるため、進行状況インジケーターをユーザーに提供できます。また、ダウンロード中にアプリを終了しないように警告することもできます。コードはここでは複雑すぎるため、ここでは説明しません。このアプリは FOSS アプリであるため、同様のことを行いたい場合はソースを確認できます。Kiwix の UI は次のようになります(以下に示す進行状況の値が異なるのは、パーセンテージが変更されたときにのみバナーが更新され、ダウンロードの進行状況パネルはより頻繁に更新されるためです)。

アプリを終了しないようユーザーに警告し、.zim アーカイブのダウンロードの進行状況を示すバーが下部に表示される Kiwix のユーザー インターフェース。

ダウンロードには長時間かかる可能性があるため、Kwix では、ユーザーは操作中にアプリを自由に使用できますが、バナーは常に表示されるので、ダウンロード操作が完了するまでアプリを閉じないようユーザーに通知されます。

アプリ内のミニ ファイル マネージャーの実装

この時点で、Kiwix PWA のデベロッパーは、OPFS にファイルを追加するだけでは不十分であることに気づきました。また、このストレージ領域から不要なファイルを削除する方法もユーザーに提供する必要がありました。理想的には、OPFS でロックされたファイルをユーザーに表示されるファイル システムにエクスポートすることも必要でした。つまり、アプリ内にミニファイル管理システムを実装する必要がありました。

Chrome 用の優れた OPFS Explorer 拡張機能(Edge でも動作します)をご紹介します。デベロッパー ツールにタブが追加され、OPFS に何が含まれているかを正確に確認したり、不正なファイルや失敗したファイルを削除したりできるようになります。コードが機能しているかどうかの確認、ダウンロードの動作のモニタリング、開発テストのクリーンアップに非常に役立ちました。

ファイルのエクスポートは、Kiwix がエクスポートされたファイルを保存する選択したファイルまたはディレクトリのファイルハンドルを取得する機能に依存するため、window.showSaveFilePicker() メソッドを使用できるコンテキストでのみ機能します。Kiwix ファイルが数 GB 未満であれば、メモリ内に BLOB を作成して URL を指定し、ユーザーに表示されるファイル システムにダウンロードできます。残念ながら、このような大規模なアーカイブでは、サポートされている場合、エクスポートは非常に簡単です。OPFS にファイルを保存する場合とほぼ同じです(保存するファイルのハンドルを取得し、window.showSaveFilePicker() を使用して保存場所を選択するようユーザーに求め、saveHandlecreateWriteable() を使用します)。リポジトリでコードを確認できます。

ファイルの削除はすべてのブラウザでサポートされており、単純な dirHandle.removeEntry('filename') で実行できます。Kiwix の場合、上記のように OPFS エントリを反復処理し、選択したファイルが最初に存在することを確認し、確認を求めることができますが、これはすべての人にとって必要というわけではありません。ご興味のある方は、コードを調べていただけます。

これらのオプションを提供するボタンで Kiwix UI を煩雑にせず、代わりにアーカイブ リストのすぐ下に小さなアイコンを配置することにしました。これらのアイコンのいずれかをタップすると、アーカイブ リストの色が変わり、ユーザーが何をしようとしているかを視覚的に示します。ユーザーがいずれかのアーカイブをクリックまたはタップすると、対応する操作(エクスポートまたは削除)が実行されます(確認後)。

.zim ファイルを削除するかどうかをユーザーに尋ねるダイアログ。

最後に、上記のすべてのファイル管理機能(OPFS へのファイルの追加、OPFS へのファイルの直接ダウンロード、ファイルの削除、ユーザーに表示されるファイル システムへのエクスポート)のスクリーンキャスト デモをご覧ください。

デベロッパーの仕事に終わりはない

OPFS は PWA のデベロッパーにとって大きなイノベーションであり、ネイティブ アプリとウェブアプリのギャップを埋めるのに大いに役立つ、非常に強力なファイル管理機能を提供します。しかし、デベロッパーは不満の多い集団です。決して満足しません。OPFS はほぼ完璧ですが、完全ではありません。主要な機能が Chromium と Firefox の両方のブラウザで動作し、Android とデスクトップに実装されているのは素晴らしいことです。今後、Safari と iOS にもすべての機能が実装される予定です。残りの問題は次のとおりです。

  • 現在 Firefox では、基礎となるディスク容量がどれだけあるとしても、OPFS の割り当て上限は 10 GB です。ほとんどの PWA 作成者にとっては十分かもしれませんが、Kwix の場合はかなり制限されます。Chromium ブラウザの方がはるかに優れています。
  • 現在、window.showSaveFilePicker() が実装されていないため、大きなファイルを OPFS からモバイル ブラウザまたはパソコンの Firefox のユーザーに表示されるファイル システムにエクスポートすることはできません。これらのブラウザでは、サイズの大きいファイルは OPFS に効果的にトラップされます。これは、コンテンツへのオープン アクセスと、特にインターネット接続が断続的または高額な地域で、ユーザー間でアーカイブを共有する機能という Kiwix の理念に反しています。
  • OPFS 仮想ファイル システムが使用するストレージをユーザーが制御することはできません。これは、microSD カードには大量の空き容量があるものの、デバイスのストレージには非常に少ない空き容量があるモバイル デバイスで特に問題になります。

ただし、全体的には、PWA でのファイル アクセスの大きな進歩であり、これらの問題は軽微なものです。Kiwix PWA チームは、File System Access API を最初に提案して設計した Chromium デベロッパーとアドボケイトに、Origin Private File System の重要性についてブラウザ ベンダー間でコンセンサスを得るために尽力してくれたことに感謝しています。Kiwix JS PWA では、過去にアプリの障害となっていた UX の問題の多くを解決し、すべてのユーザーが Kiwix コンテンツにアクセスしやすくなるよう取り組んでいます。ぜひ Kiwix PWA をお試しいただき、ご意見をデベロッパーに伝えてください。

PWA 機能に関する有用なリソースについては、次のサイトをご覧ください。