loading
Note:
The other half of the File System APIFileSystem wraps the pickers: every handle is a file the user chose and can revoke. This is the opposite - nothing is shown to the user, nothing is granted, and nothing here is visible outside the origin. It is for data your app owns (a cache, a working copy, a database file), not data the user owns.

Check for support

IsSupported / IsSyncAccessSupported

OPFS itself ships in every current engine. The synchronous access handles the Sync* members use are a separate check: they only exist on a worker thread, and this service relays those calls to a worker it starts for you.

Razor
@inject Bit.Butil.OriginPrivateFileSystem opfs

var supported = await opfs.IsSupported();
var syncSupported = await opfs.IsSyncAccessSupported();
Live sample
support output
Results will appear here when you interact with the samples.

Write and read a file

WriteText / ReadText

Paths are relative to the origin's private root, and directories along the path are created for a write. A write goes through a swap file and commits on close, so a failed write leaves the previous contents rather than half of the new ones.

C#
await opfs.WriteText("notes/today.txt", "hello from C#");

string? text = await opfs.ReadText("notes/today.txt");
// null when there is no file there
Live sample
text output
Results will appear here when you interact with the samples.

Bytes

WriteBytes / ReadBytes

The same two calls for binary content. Nothing is base64'd on the way through - a byte[] crosses the interop boundary as a Uint8Array.

C#
byte[] data = System.Text.Encoding.UTF8.GetBytes("binary payload");
await opfs.WriteBytes("blobs/payload.bin", data);

byte[]? read = await opfs.ReadBytes("blobs/payload.bin");
Live sample
bytes output
Results will appear here when you interact with the samples.

Directories

CreateDirectory / List / Exists

List returns one directory's immediate children - descend by calling it again with a child's Path. CreateDirectory creates every missing directory along the path.

C#
await opfs.CreateDirectory("projects/drafts");

OpfsEntry[] entries = await opfs.List("projects");
foreach (var entry in entries)
{
    // entry.Name, entry.Path, entry.IsDirectory
}

var exists = await opfs.Exists("projects/drafts");
Live sample
directory output
Results will appear here when you interact with the samples.

File metadata

GetFileInfo

Size, MIME type and last-modified time without reading the contents - which is what you want before deciding whether to read a file at all.

C#
OpfsFileInfo? info = await opfs.GetFileInfo("notes/today.txt");

if (info is not null)
{
    var size = info.Size;                 // bytes
    var modified = DateTimeOffset.FromUnixTimeMilliseconds(info.LastModified);
}
Live sample
metadata output
Results will appear here when you interact with the samples.

Move, remove and clear

Move / Remove / Clear

Move renames or relocates a file - natively in Chromium, and as a copy-and-delete elsewhere. Remove needs recursive: true for a directory that is not empty. Clear empties the whole private file system.

C#
await opfs.Move("notes/today.txt", "notes/archive/today.txt");

await opfs.Remove("notes/archive/today.txt");
await opfs.Remove("notes", recursive: true);

await opfs.Clear();
Live sample
move / remove output
Results will appear here when you interact with the samples.

Sync access handles - the fast path

SyncWrite / SyncAppend / SyncRead / SyncSize / SyncTruncate

These read and write at an offset without touching the rest of the file, which is what makes OPFS usable as a backing store for something like SQLite. createSyncAccessHandle only exists on a worker thread, so the calls are relayed to a dedicated worker started on first use and terminated when the service is disposed.

C#
var bytes = System.Text.Encoding.UTF8.GetBytes("record-1;");

// truncate: true (the default) makes the file exactly what was written
await opfs.SyncWrite("db/log.bin", bytes);

// append is the cheap way to keep a log - nothing before it is read or rewritten
await opfs.SyncAppend("db/log.bin", System.Text.Encoding.UTF8.GetBytes("record-2;"));

var size = await opfs.SyncSize("db/log.bin");

// read 9 bytes starting at offset 9, without loading the rest
byte[]? slice = await opfs.SyncRead("db/log.bin", offset: 9, length: 9);

await opfs.SyncTruncate("db/log.bin", 9);
Live sample
sync access output
Results will appear here when you interact with the samples.
Warning:
It counts against the origin's quota Everything written here draws on the same budget as IndexedDB and Cache Storage - see StorageManager for the estimate, and for asking the browser to make the origin's storage persistent. Without that, this data is evictable under disk pressure like everything else.
Note:
One file, one writer A sync access handle is an exclusive lock on its file for the duration of the call, so two overlapping Sync* calls on the same path are serialized rather than run together. Firefox and Safari additionally allow sync access handles only inside a worker - which is where this service puts them, so the same code works in every engine that has OPFS.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes navigator.storage.getDirectory.
IsSyncAccessSupported
ValueTask<bool> IsSyncAccessSupported()
True when OPFS, workers and createSyncAccessHandle are all present, so the Sync* members can work.
List
ValueTask<OpfsEntry[]> List(string path = &quot;&quot;)
Lists a directory's immediate children. Not recursive; an empty path is the root.
CreateDirectory
ValueTask<bool> CreateDirectory(string path)
Creates a directory and every missing directory above it.
Exists
ValueTask<bool> Exists(string path)
True when a file or directory exists at the path.
GetFileInfo
ValueTask<OpfsFileInfo?> GetFileInfo(string path)
Name, size, MIME type and last-modified time without reading the contents.
ReadText
ValueTask<string?> ReadText(string path)
Reads a whole file as text; null when there is no file there.
ReadBytes
ValueTask<byte[]?> ReadBytes(string path)
Reads a whole file as bytes; null when there is no file there.
WriteText
ValueTask<bool> WriteText(string path, string text)
Writes text, replacing what was there and creating what is missing along the path.
WriteBytes
ValueTask<bool> WriteBytes(string path, byte[] data)
Writes bytes, replacing what was there and creating what is missing along the path.
Remove
ValueTask<bool> Remove(string path, bool recursive = false)
Deletes a file or directory. A non-empty directory needs recursive: true.
Move
ValueTask<bool> Move(string path, string destination)
Moves or renames a file - natively in Chromium, as a copy and delete elsewhere.
Clear
ValueTask<bool> Clear()
Deletes everything in the origin's private file system.
SyncRead
ValueTask<byte[]?> SyncRead(string path, long offset = 0, int length = 0)
Reads part of a file through a sync access handle; length 0 reads to the end.
SyncWrite
ValueTask<long> SyncWrite(string path, byte[] data, long offset = 0, bool truncate = true)
Writes bytes at an offset, returning the number written or -1. truncate: false patches in place.
SyncAppend
ValueTask<long> SyncAppend(string path, byte[] data)
Appends bytes at the end of the file, reading and rewriting nothing before them.
SyncTruncate
ValueTask<bool> SyncTruncate(string path, long size)
Cuts the file to size bytes, or pads it with zeros when it is shorter.
SyncSize
ValueTask<long> SyncSize(string path)
Reads a file's size through a sync access handle; -1 when it is missing.
OpfsEntry
class OpfsEntry { string Name; string Path; bool IsDirectory; }
One child of a listed directory. Path is what the other members accept.
OpfsFileInfo
class OpfsFileInfo { string Name; string Path; long Size; string Type; long LastModified; }
A file's metadata. LastModified is milliseconds since the Unix epoch.
An unhandled error has occurred. Reload 🗙