前回からのつづき

前回、 OPFS のファイル操作について、その起点となる FileSystemFileHandle オブジェクトと、読み込みを担当する File オブジェクトについて説明を行ってきました。今回は、その続きとして、FileSystemWritableFileStream オブジェクトを中心に説明していきたいと思います。


FileSystemWritableFileStream オブジェクト

FileSystemWritableFileStream オブジェクトは、ファイルへの書き込みを行うためのオブジェクトです。

FileSystemFileHandle オブジェクトの createWritable() メソッドで取得します。

Chromium 系ブラウザでは比較的早い段階から安定して動作していましたが、Firefox や Safari をはじめとする WebKit 系ブラウザでは、同期アクセス ( createSyncAccessHandle() ) と比べて、非同期アクセス ( createWritable() ) の動作が不安定な時期がありました。

筆者が実機で検証した環境では、iPadOS 17.7.11 の Safari において、write() メソッドが期待どおりに動作しないことを確認しています。一方、iOS 26.5.2 では同じコードが正常に動作しました。

現在では、主要ブラウザの最新版において createWritable() は概ね安定して利用できるようになっています。

ただし、古いOSやブラウザでは挙動が異なる場合があるため、サポート対象の環境で動作確認を行うことをおすすめします。


write() メソッド

ファイルへデータを書き込みます。

await writable.write(data);

または

await writable.write({
  type: "write",
  position: 0,
  data: data
});

■ パラメータ

書き込むデータ:様々な形式のデータが指定可能です。

  1. string
  2. Blob
  3. ArrayBuffer
  4. TypedArray ( Uint8Array など)
  5. DataView
  6. WriteParams オブジェクト( write )
  7. WriteParams オブジェクト( seek )
  8. WriteParams オブジェクト( truncate )
  9. ReadableStream

1. String

文字列を書き込みます。

await writable.write("Hello OPFS");

2. Blob

Blob オブジェクトを書き込みます。

const blob = new Blob(
  ["Hello OPFS"],
  { type: "text/plain" }
);

await writable.write(blob);

👉 画像や動画などの保存時によく使われます。

3. ArrayBuffer

生のバイト列を書き込みます。

const buffer = new ArrayBuffer(10);

await writable.write(buffer);

4. TypedArray

TypedArray も指定できます。

const bytes = new Uint8Array([65, 66, 67, 68]);

await writable.write(bytes);

最も利用頻度が高い Uint8Array を含め、下記のものが使用可能です。

  • Uint8Array
  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array
  • BigInt64Array
  • BigUint64Array

※ TypedArray は、ArrayBuffer の見た目を定義できるオブジェクトです。

5. DataView

DataView も指定できます。

const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
view.setUint16(0, 0x1234);

await writable.write(view);

※ DataView も、ArrayBuffer の見た目を定義できるオブジェクトですが、固定ではなく、都度異なる見た目が指定可能です。

6. WriteParams オブジェクト( write )

指定した書き込み位置に、データを書き込みます。

WriteParams オブジェクト( write )の形式

{
  type: "write",
  position: 書き込み開始バイト位置(先頭0),
  data: 書き込みデータ
}

※ position は文字位置ではなくバイト位置です。UTF-8 の日本語は 1文字が複数バイトになるため、文字列データを部分更新する場合は注意が必要です。

// 先頭から書き込みます。
await writable.write({
  type: "write",
  position: 0,
  data: "Hello OPFS"
});

// 1024バイト目から書き込みます。
await writable.write({
  type: "write",
  position: 1024,
  data: bytes
});

7. WriteParams オブジェクト( seek )

現在位置を指定した位置まで移動します。

WriteParams オブジェクト( seek )の形式

{
  type: "seek",
  position: 指定バイト位置(先頭0)
}
await writable.write({
  type: "seek",
  position: 100
});

👉 これは、後述する await writable.seek(100); と同じ意味です。

8. WriteParams オブジェクト( truncate )

ファイルサイズを変更します。

WriteParams オブジェクト( truncate )の形式

{
  type: "truncate",
  size: ファイルバイトサイズ
}
await writable.write({
  type: "truncate",
  size: 1024
});

👉 これは、後述する await writable.truncate(1024); と同じ意味です。

9. ReadableStream

ReadableStream オブジェクトにより、渡されるデータを書き込みます。

// `await fetch` は、ReadableStream オブジェクトを返します
const response = await fetch("/large-file.bin");

await writable.write(response.body);

どちらかというと、ReadableStream オブジェクトの pipeTo()関数に、FileSystemWritableFileStream オブジェクトを受け渡す方が一般的な書き方です。

const writable = await fileHandle.createWritable();

const response = await fetch("/large-file.bin");
await response.body.pipeTo(writable);

👉 これらの処理では、一度に全てのデータをメモリに読み込む必要がない為、大容量のファイル( 100MB 以上)の読み書き時のメモリ節約に有効です。

実務でよく使うもの

実際の OPFS 開発では、この4つがよく使われます。

  • テキスト
await writable.write("Hello OPFS");
  • UTF-8
const bytes = new TextEncoder().encode(text);

await writable.write(bytes);
  • Blob
await writable.write(blob);
  • ランダムアクセス
await writable.write({
  type: "write",
  position: 0,
  data: bytes
});

■ 戻り値

Promise オブジェクト

await 完了後、 undefined


seek() メソッド

書き込み位置を移動します。

await writable.seek(5);

■ パラメータ

バイト位置(先頭0)

■ 戻り値

Promise オブジェクト

await 完了後、undefined

例えば、

Hello OPFS

という内容が保存されている場合、

await writable.seek(6);

await writable.write("Browser");

を実行すると、

Hello Browser

になります。


truncate() メソッド

ファイルサイズを変更します。

await writable.truncate(5);

■ パラメータ

ファイルサイズ(バイト)

■ 戻り値

Promise オブジェクト

await 完了後、undefined

例えば、

Hello OPFS

に対して実行すると、

Hello

になります。


close() メソッド

書き込みを終了します。

await writable.close();

■ パラメータ

なし

■ 戻り値

Promise オブジェクト

await 完了後、undefined

書き込み後は必ず実行する必要があります。

👉 close() を呼び出して初めて変更内容が確定します。


書き込み用のストリームによる安全な書き込み

書き込み用のストリームによる書き込みは、かなり安全な方法で上書きされます。

例えば、

既に sample.txt ファイルがあり、中身が

Hello World

だったとします。この状態で、下記のプログラムを実行すると

const writable = await handle.createWritable();
await writable.write("ABC");
await writable.close();

結果は、

ABC

になります。

つまり、

ABClo World

のように、中途半場に先頭だけが書き換わるということはありません。 先頭から書き込んだ場合は、結果として新しい内容全体で置き換えられます。

では、どのようにして、これを実現しているのでしょうか?

実は、

const writable = await handle.createWritable();

を実行した時点では、元のファイルを直接編集するのではなく、 sample.txt.tmp のような一時領域へ書き込まれています。そのため、write() を何回実行しても元ファイルはそのままです。

最後に、

await writable.close();

を実行してはじめて、

sample.txt.tmp のような一時領域

        ⇓

sample.txt

という置き換えが行われるのです。

close() が呼び出されない限り変更内容が確定しないため、途中で、

  • ブラウザがクラッシュ
  • 電源断
  • JavaScriptエラー

などが起きても、元のファイルが壊れることなく、安全に保持されます。

このあたりは、デスクトップアプリの「安全な保存」と同じ考え方です。

書き込み用のストリームによる書き込みは、「ファイルを開いて、そのまま書き込む」というイメージがありますが、実際には、安全性を重視した「トランザクションのような保存」になっています。

これは、 SQLite などのデータベースで採用されている「途中で壊れないように更新する」という考え方にも通じています。

また、createWritable() には、keepExistingData というオプションがあります。

const writable = await handle.createWritable({
    keepExistingData: true
});

このオプションは、デフォルトでは、false になっているのですが、これを true として指定すると、既存ファイルの内容を一時領域へコピーした状態で書き込みを開始します。

この機能は「ファイルの一部だけを書き換えたい」といった用途に役立ちます。

例えば、ファイルの途中にある数バイトだけ更新したいとします。

その場合、既定では空の書き込み領域から開始するため、途中だけ書き込むと、それ以外の部分は失われてしまいます。

このオプションを true にすることで、その問題を防止することができます。

(つづく)