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 ファイルと呼ばれる高度に圧縮されたアーカイブに変換し、Kiwix ブラウザでその場で読み取ることができるようになりました。

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

汎用性が高い File API にも欠点があり、Kiwix JS アプリはネイティブ アプリと比べて使いづらく古臭く見えます。この API ではアクセス権をセッション間で保持できないため、アプリを起動するたびに、ユーザーがファイル選択ツールを使用してアーカイブを選択するか、アプリにファイルをドラッグ&ドロップする必要があります。

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

  • パソコンのオペレーティング システムでのみ動作します。
  • サイズが大きく、容量が多い(70 MB ~ 100 MB)。

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

Kiwix は PWA のユーザーの状況を改善する方法はありましたか?

File System Access API の登場

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

API の File System Access 機能はどのように機能しますか?重要な留意点:

  • これは非同期 API です(Web Worker の特殊な関数を除く)。
  • ファイルまたはディレクトリ選択ツールは、ユーザー ジェスチャー(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 に簡単に収まります。(ヒント: アプリ内ライブラリで [other] → [mdwiki_en_all_maxi] を探します)。

OPFS の仕組み

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

OPFS を使用するには、まず navigator.storage.getDirectory() を使用してアクセスをリクエストします(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 を試すようにしました。

使用済みの保存容量(%)と残りの保存容量(GB)を示すパネル。

パネルには、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 のユーザー インターフェース。

ダウンロードは非常に時間のかかる操作であるため、Kiwix では、ダウンロード中にユーザーがアプリを自由に使用できるようにしていますが、バナーを常に表示して、ダウンロード オペレーションが完了するまでアプリを閉じないようにユーザーにリマインダーを表示しています。

アプリ内にミニ ファイル マネージャーを実装する

この時点で、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 作成者にとっては十分な容量ですが、Kiwix にとってはかなり制限的です。幸い、Chromium ブラウザははるかに寛大です。
  • window.showSaveFilePicker() が実装されていないため、現在のところ、モバイル ブラウザまたはパソコン版 Firefox で OPFS からユーザーに表示されるファイル システムに大きなファイルをエクスポートすることはできません。これらのブラウザでは、大規模なファイルは事実上 OPFS にトラップされます。これは、コンテンツへのオープン アクセスと、特にインターネット接続が断続的または高額な地域で、ユーザー間でアーカイブを共有する機能という Kiwix の理念に反しています。
  • OPFS 仮想ファイル システムが使用するストレージをユーザーが制御することはできません。これは、microSD カードには大量の空き容量があるものの、デバイスのストレージには非常に少ない空き容量しかないモバイル デバイスで特に問題になります。

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

PWA の機能に関する優れたリソースについては、以下のサイトをご覧ください。