はじめに

前々回、前回と 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 タイプを設定していない場合、単なる文字列として表示されるのと異なり、MIME タイプを設定した場合は、HTML として解析されて表示されています。

👉 つまり、URL.createObjectURL() は URL を作成するだけのメソッドであり、MIME タイプは、パラメータで渡す Blob データそのものに、事前に設定されている必要があります。

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 タイプが設定されており、画面にデータが正しく表示されます。

下記の「ファイルの選択」ボタンを押下し、ファイル選択ダイアログボックスで画像や pdf ファイルを選択してみてください。

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

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

結論からいうと、これは半分正しく、半分間違っているというのが、現実です。

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

中身は同じであるにも関わらず、iframe には表示されず、ダウンロードされたことと思います。また、MIME タイプも、application/octet-stream というバイナリファイルを示す値に変更されていると思います。

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

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

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

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


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

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

では、この File オブジェクトにも、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, { 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 に設定した場合、正しく表示されるのでしょうか?

先ほどと同様に、下記の「ファイルの選択」ボタンを押下し、ファイル選択ダイアログボックスで画像や pdf ファイルを選択してみてください。

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

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

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

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

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

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


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

先ほどと同様に、ユーザーファイルからコピーしたファイルを ~.bin という名前で保存し、

<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 ファイルの File オブジェクトを URL.createObjectURL() に渡して作成した URL を iframe に設定してみます。

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

下記の「ファイルの選択」ボタンを押下し、ファイル選択ダイアログボックスで画像や pdf ファイルを選択してみてください。

MIME タイプが application/octet-stream というバイナリファイルとなり、画面には表示されず、ダウンロードされることが確認できると思います。

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

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

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


まとめ

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

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