前回からのつづき
前回、 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
});
■ パラメータ
書き込むデータ:様々な形式のデータが指定可能です。
- string
- Blob
- ArrayBuffer
- TypedArray ( Uint8Array など)
- DataView
- WriteParams オブジェクト( write )
- WriteParams オブジェクト( seek )
- WriteParams オブジェクト( truncate )
- 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 を含め、下記のものが使用可能です。
Uint8ArrayInt8ArrayUint8ArrayUint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64ArrayBigInt64ArrayBigUint64Array
※ 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 にすることで、その問題を防止することができます。
(つづく)