はじめに

前々回、前回と OPFS に対する非同期操作や Web Worker を利用した同期操作のサンプルプログラムを作成して、その動きを確認してきました。

サンプルプログラムは、

  • ユーザーファイルから OPFS ファイルへのコピー
  • OPFS ファイルの読み込み
  • URL.createObjectURL() で File や Blob データの URL を作成し、iframe で内容を表示

という単純なものでした。

非同期操作と同期操作とでは、Web Worker の使用や、OPFS のアクセスそのものが異なるため、当然コードは異なってきます。

しかし、ファイル読み込み後のデータの取り扱い(主に画面表示)において、非同期操作で全く意識する必要がなかったにもかかわらず、同期操作では、明確に意識する必要がある項目があります。

それは、MIME タイプ です。

同期操作の Web Worker( worker.js ) のコードをご覧になった方はお気づきだとは思いますが、OPFS ファイル読み込み時には、意図的に MIME タイプの取得と設定を行っている箇所があります。

Web Worker( worker.js )のコード抜粋

self.onmessage = async (e) => {
  const parm = e.data;

  try {
    // OPFS のルートディレクトリを取得
    const root = await navigator.storage.getDirectory();
      :
    // read 要求の場合は、OPFS からファイルを読み込んで返す
    if (parm.type === "read") {
      const fileHandle = await root.getFileHandle(parm.fileName);

👉    const file = await fileHandle.getFile(); // ファイルの MIME タイプを取得するために、File オブジェクトを取得
👉    const mimeType = file.type || 'application/octet-stream'; // MIME タイプが取得できない場合は、デフォルトで application/octet-stream を使用

      const accessHandle = await fileHandle.createSyncAccessHandle(); // 同期アクセスハンドルを作成

      const buffer = new Uint8Array(accessHandle.getSize());
      try {
        accessHandle.read(buffer, { at: 0 }); // ファイルの先頭から読み込む
      } finally {
        accessHandle.close(); // 同期アクセスハンドルを閉じる
      }

👉    const blob = new Blob([buffer], {type: mimeType});

      self.postMessage({ type: "read", blob });
      return;
    }
      :
  }
};

実は、この MIME タイプ設定の部分は意外と重要で、これがないと多くのブラウザでは、 iframe で正しくファイルの内容を表示することができません。

そこで今回は、この MIME タイプに関して少し掘り下げて説明していきたいと思います。


MIME タイプとは?

MIME タイプとは、Multipurpose Internet Mail Extensions type の略で、もともとはメールに添付されたファイルが何の種類なのかを示す識別子のことです。

RFC 2045〜2049 で定義された、「メールで添付ファイルを扱うための拡張仕様」が原点です。

メールは元々テキストしか送れなかったため、

  • 画像
  • 音声
  • PDF
  • HTML
  • バイナリファイル

などを送るために「このデータは何の種類か」を示す必要があり、そこで MIMEタイプが生まれました。

HTTP/1.0(RFC 1945)以降、HTTP は Content-Type ヘッダで MIME タイプを使うことを正式に規定し、その後の Web の標準になりました。

例:

Content-Type: text/html; charset=utf-8
  • text/html → HTML
  • image/png → PNG画像
  • application/json → JSON
  • video/mp4 → MP4動画

など、MIME タイプは Web・API・ブラウザ・ストレージの共通言語になっています。

👉 つまり、Web という仕組みの中でデータを扱う際に、そのデータの識別の基準となるのが、MIME タイプになります。


MIMEタイプの構造

MIMEタイプは、 2つの部分で構成されます。

<タイプ>/<サブタイプ>

✔ タイプ(大分類)

  • text
  • image
  • audio
  • video
  • application
  • font
  • multipart

など

✔ サブタイプ(細分類)

  • html
  • plain
  • png
  • jpeg
  • json
  • xml
  • pdf

など

例:

application/json

→ 「アプリケーション系のデータで、その中身は JSON 」

主な MIME タイプの一覧

  • text/plain
  • text/html
  • text/css
  • text/javascript(古い、今は application/javascript 推奨)
  • application/json
  • application/xml
  • application/pdf
  • image/png
  • image/jpeg
  • image/svg+xml
  • audio/mpeg
  • video/mp4
  • font/woff2

など


MIMEタイプが正しくないと起きる問題

  • ブラウザがファイルを正しく表示できない。
  • ダウンロード時に拡張子が誤認される。
  • <script><link> が読み込まれない。
  • JSON が text / plain として扱われてパースされない。
  • 画像が壊れて見える。
  • 動画が再生されない。
  • セキュリティエラー( MIME type mismatch )

※ 特に最近のブラウザはセキュリティが厳しく、MIME タイプが正しくないファイルは読み込みが拒否されることもあります。


URL.createObjectURL() で MIME タイプは設定されるか?

URL.createObjectURL() は、Blob や File オブジェクトから一時的な URL を生成する JavaScript のメソッドです。

JavaScript の URL インターフェイスの静的メソッド で、引数に指定した Blob または File オブジェクトを表す一時的なURL 文字列を生成してくれます。

OPFS ファイルの同期操作では、ファイルから読み込んだデータは、File オブジェクトではなく、arrayBuffer にバイナリデータとして戻されます。そのため、そのままでは、URL.createObjectURL() に引き渡すことができず、Blob に変換する必要があります。

この Blob 変換時に、MIME タイプの指定が可能です。

可能ですという表現を使いましたが、Blob 変換時、MIME タイプの指定はオプションであり、指定しなくても、作成自身は可能です。

では、指定しなかった場合と指定した場合では、どのような違いがでるのでしょうか?

例えば、

const buffer = "<H1>Hello, world!!</H1>";
    :
const url1 = URL.createObjectURL(new Blob([buffer]));
const url2 = URL.createObjectURL(new Blob([buffer], { type: "text/html" }));

のように、同じデータをMIME タイプを指定しないで作成した Blob の url1 と MIME タイプを指定して作成した Blob の url2 とでは、 iframe に設定した場合にどのような違いがでるのでしょうか?

下記の「表示」ボタンを押下して、確認してみてください。

◇ MIMEタイプ未設定(url1)

◇ MIMEタイプ設定済(url2)

MIMEタイプ設定済(url2)の場合、常にHTML文書として正しく解釈されて表示されています。

一方、MIMEタイプ未設定(url1)の場合、

  • Chrome系のブラウザ

→ テキストとして表示

  • Safari系のブラウザ

→ 表示されず、ダウンロードが実行

  • FireFox系のブラウザ

→ HTML文書として表示

といったように、ブラウザの実装により、データの取り扱い方が変わってきます。

👉 つまり、多くのブラウザにおいて、データを正しいタイプで認識してもらうためには、事前に MIME タイプを設定した Blob や File オブジェクトを URL.createObjectURL() に受け渡す必要があります。

このように、URL.createObjectURL() は URL を作成するだけのメソッドであり、データに MIME タイプを設定するような機能は基本的にありません。

Web Worker のコード内で、OPFS ファイルの同期読み込み後、arrayBuffer から Blob へ変換する時に、意図的に MIME タイプを設定しているのは、これが理由です。

尚、コードでは、設定に利用している MIME タイプは、File オブジェクトのものを利用しています。

次は、これがなぜ機能するのかについてもう少し詳しく確認していきたいと思います。


ユーザーファイルを iframe で表示してみる

<input type='file'> タグで取得したユーザーファイルの File オブジェクトは、Blob オブジェクトを継承したオブジェクトです。

URL.createObjectURL() のパラメータとしても、そのまま受け渡すことが可能です。

File オブジェクトには、ファイルの中身に加え、名前、サイズ、MIME タイプが格納されています。

例えば、

<input id='input' type='file'>
    :
document.getElementById("input").addEventListener("change", async (e) => {
  const file = e.target.files[0];
  const url = URL.createObjectURL(file);
    :
});

で作成した URL を iframe に設定してみると、File オブジェクトに MIME タイプが設定されており、画面にデータが正しく表示されます。

下記の「ファイルの選択」ボタン(※ FireFox 系のブラウザの場合、「参照...」ボタン)を押下し、表示されたファイル選択ダイアログボックスで、画像や pdf ファイルを選択してみてください。

iframe にファイルの内容が表示され、下段にファイル名、ファイルサイズ、MIMEタイプが表示されることを確認できると思います。

では、ユーザーファイルのファイルシステム上に MIME タイプが保存されているのでしょうか?

結論からいうと、ファイルシステム上には MIME タイプは、保存されていません。しかし、これは遠からず近からずといったところです。 というのも、ファイルシステム上のファイル名には、ファイル識別子を含むことができるからです。

つまり、ファイル識別子を使えば、対応する MIME タイプを推測することが可能だからです。

File System Access API も、このような推測を行っているのでしょうか?

それは、先ほど読み込んだファイルのファイル識別子を .bin に変更し、再読み込みするとはっきりします。

中身は同じであるにも関わらず、iframe には表示されず、ダウンロードが実行されたことと思います。また、MIME タイプも、application/octet-stream (※ Safari 系のブラウザの場合、application/macbinary )というバイナリファイルを示す値に変更されていると思います。

👉 つまり、ファイルシステム上には、 MIME タイプは保存されておらず、File オブジェクトを生成する際に、ファイル識別子から、対応する MIME タイプが推測され、設定されているということです。

※ この MIME タイプ推測は Chrome 系特有の挙動ではなく、Firefox 系や Safari 系を含む主要ブラウザ共通の File System Access API のふるまいです。File オブジェクトは、拡張子から推測された MIME タイプを自動的に type に設定します。

では、同じ、File System Access API を使用している OPFS のファイルシステムはどうなっているのでしょうか?

続けてみていきたいと思います。


OPFS ファイルを iframe で表示してみる

OPFS ファイルは、File System Access API の getFile() で非同期読み込みが可能です。ユーザーファイル同様に、File オブジェクトが戻されます。

では、この File オブジェクトにも、MIME タイプが設定されているのでしょうか

例えば、

<input id='input' type='file'>
    :
document.getElementById("input").addEventListener("change", async (e) => {
  const file = e.target.files[0];
    :
  const root = await navigator.storage.getDirectory();
  const post = await root.getDirectoryHandle("opfs-no-fukabori-mime-type", { create: true });
  const handle = await post.getFileHandle(file.name, { create: true });
  const writable = await fileHandle.createWritable(); // 書き込み用のストリームを作成
  await writable.write(file); // ユーザーファイルの内容を OPFS のファイルに書き込む
  await writable.close(); // 書き込み用のストリームを閉じる

  const opfsFile = await handle.getFile(); // OPFS 上のファイルの取得
    :
});

のように、指定したユーザーファイルを OPFS に一旦コピーし、 非同期読み込みして File オブジェクトを取得します。その後、 URL.createObjectURL() に渡して作成した URL を iframe に設定した場合、正しく表示されるのでしょうか?

先ほどと同様に、下記の「ファイルの選択」ボタン(※ FireFox 系のブラウザの場合、「参照...」ボタン)を押下し、表示されたファイル選択ダイアログボックスで画像や pdf ファイルを選択してみてください。

ユーザーファイルの時と同様に、画面にデータが正しく表示されているのが、確認できると思います。

このように、OPFS の File オブジェクトにも、MIME タイプが設定されているようです。

では、この OPFS の File オブジェクトの MIME タイプの元ネタはなんでしょうか?

Web 技術として比較的新しいファイルシステムであるため、ファイルシステム内に保管されているのでしょうか?

それとも、同じ File System Access API を使用しているユーザーファイルと同様に、ファイル識別子に対応する MIME タイプが設定されているのでしょうか?

ここは、結論を急がず、もう少し慎重に確認してみる必要がありそうです。


ファイル識別子を変更した OPFS ファイルを iframe で表示してみる

それでは、 OPFS で MIME タイプが保存されているかを確認するために、ユーザーファイルを OPFS にコピーして保存する際に、ファイル識別子を変更して、動きを確認していきたいと思います。

例えば、

<input id='input' type='file'>
    :
document.getElementById("input").addEventListener("change", async (e) => {
  const file = e.target.files[0];
    :
  const root = await navigator.storage.getDirectory();
  const post = await root.getDirectoryHandle("opfs-no-fukabori-mime-type", { create: true });
  const handle = await post.getFileHandle(file.name + ".bin", { create: true });
  const writable = await fileHandle.createWritable(); // 書き込み用のストリームを作成
  await writable.write(file); // ユーザーファイルの内容を OPFS のファイルに書き込む
  await writable.close(); // 書き込み用のストリームを閉じる

  const opfsFile = await handle.getFile(); // OPFS 上のファイルの取得
    :
});

のように、指定したユーザーファイルを OPFS に ~.bin という名前で一旦コピーし、 非同期読み込みして File オブジェクトを取得します。その後、 URL.createObjectURL() に渡して作成した URL を iframe に設定してみます。

ファイル識別子は ~.bin になりますが、write() 時には、読み込んだ File オブジェクトをそのまま渡しているので、 File System Access API には、正しい MIME タイプが伝わっているはずです。

下記の「ファイルの選択」ボタン(※ FireFox 系のブラウザの場合、「参照...」ボタン)を押下し、表示されたファイル選択ダイアログボックスで画像や pdf ファイルを選択してみてください。

MIME タイプが application/octet-stream (※ Safari 系のブラウザの場合、application/macbinary )というバイナリファイルとなり、画面には表示されず、ダウンロードが実行されることが確認できると思います。

どうやら、ユーザーファイル同様、OPFS のファイルシステムにも、MIME タイプは、保存されていないようです。

👉 つまり、OPFS ファイルの場合も、ユーザーファイル同様、File オブジェクトの生成時に、ファイル識別子から、対応する MIME タイプが導き出され、設定されているということです。

※ この挙動も主要ブラウザ共通です。OPFS は MIME タイプを保存しませんが、getFile() が返す File オブジェクトには、ブラウザが拡張子から推測した MIME タイプが自動で設定されます。


まとめ

  • URL.createObjectURL() では、URL の作成しか行わず、MIME タイプの設定は行われない。
  • URL.createObjectURL() で正しくデータを判別してもらいたい場合は、パラーメータとして渡される Blob や File オブジェクトに、MIME タイプが設定されている必要がある。
  • OPFS を同期読み込みした場合は、arrayBuffer にバイナリーデータが読み込まれるだけなので、画面表示などに使いたい場合には、Blob などに変換した際、MIME タイプの設定が必要である。
  • ユーザーファイル、OPFS ファイルともに、ファイルシステムに MIME タイプは保存されていない。読み込みで、File オブジェクトを生成した場合、ファイル識別子に対応した MIME タイプが自動で設定される。

※ この MIME タイプ推測は、Chrome 系、Firefox 系、Safari 系など主要ブラウザに共通する File System Access API の標準的な挙動です。