FileSystem
Let the user pick a real file or folder on their machine, then read it, write back to it, and keep the handle - the difference between a download and a working Save.
@inject Bit.Butil.FileSystem fileSystemMDN reference
IsSupported and keep a
download-based fallback - FileReader for reading and
ObjectUrls for handing a file back are the everywhere-else path.
ShowOpenFilePicker, ShowSaveFilePicker and
ShowDirectoryPicker must be called from a user-gesture handler, and each returns
null when the user cancels rather than throwing - dismissing a dialog is not an error.
The file pickers and the directory picker ship together in practice, but they are separate entry points, so both are checkable. During prerender/SSR both return false rather than throwing.
@inject Bit.Butil.FileSystem fileSystem
var files = await fileSystem.IsSupported();
var folders = await fileSystem.IsDirectorySupported();The accept list is what turns the picker's filter dropdown from '*.*' into named groups. A group with no extensions is dropped rather than sent, because the underlying API rejects an empty filter outright. GetFileInfo reads name, size, type and last-modified without loading the contents.
var picked = await fileSystem.ShowOpenFilePicker(
multiple: false,
accept:
[
new FilePickerType
{
Description = "Text files",
MimeType = "text/plain",
Extensions = [".txt", ".md", ".json"],
},
]);
if (picked is { Length: > 0 })
{
var info = await fileSystem.GetFileInfo(picked[0]);
var text = await fileSystem.ReadText(picked[0]);
}This is the part a download cannot do. ShowSaveFilePicker reserves a name but writes nothing; the handle it returns can then be written to as often as you like, so the second Save goes straight to the same file with no dialog. Nothing here needs a server round-trip.
_target = await fileSystem.ShowSaveFilePicker(
suggestedName: "notes.txt",
accept: [new FilePickerType { Description = "Text", MimeType = "text/plain", Extensions = [".txt"] }]);
// as many times as you like, no further prompts:
await fileSystem.WriteText(_target!, _contents);A directory handle grants access to everything under it. ListDirectory returns the immediate children only - descend by calling it again on a child whose IsDirectory is true. GetFile fetches a named entry, optionally creating it, and takes a bare name: this API has no path traversal.
var folder = await fileSystem.ShowDirectoryPicker(write: true);
if (folder is not null)
{
foreach (var entry in await fileSystem.ListDirectory(folder))
{
// entry.IsDirectory to recurse, entry.Name to show
}
// create a file inside it and write to it:
var created = await fileSystem.GetFile(folder, "hello.txt", create: true);
await fileSystem.WriteText(created!, "written from Blazor");
await fileSystem.Remove(folder, "hello.txt");
}A handle you persisted from a previous session comes back as Prompt rather than Granted - the grant does not survive a reload. RequestPermission, from a user gesture, is what makes it usable again. Release drops a handle you are finished with; handles are held on the JS side for as long as your app might use them.
var state = await fileSystem.QueryPermission(handle, write: true);
if (state == FileSystemPermission.Prompt)
{
// from a click handler:
state = await fileSystem.RequestPermission(handle, write: true);
}
// when you're done with it:
await fileSystem.Release(handle);API reference
ValueTask<bool> IsSupported()ValueTask<bool> IsDirectorySupported()ValueTask<FileSystemHandleInfo[]?> ShowOpenFilePicker(bool multiple = false, FilePickerType[]? accept = null, bool excludeAcceptAllOption = false, string startIn = "")ValueTask<FileSystemHandleInfo?> ShowSaveFilePicker(string suggestedName = "", FilePickerType[]? accept = null, bool excludeAcceptAllOption = false, string startIn = "")ValueTask<FileSystemHandleInfo?> ShowDirectoryPicker(bool write = false, string startIn = "")ValueTask<FileSystemHandleInfo[]> ListDirectory(FileSystemHandleInfo directory)ValueTask<string?> ReadText(FileSystemHandleInfo file)ValueTask<byte[]?> ReadBytes(FileSystemHandleInfo file)ValueTask<FileSystemFileInfo?> GetFileInfo(FileSystemHandleInfo file)ValueTask<bool> WriteText(FileSystemHandleInfo file, string text)ValueTask<bool> WriteBytes(FileSystemHandleInfo file, byte[] data)ValueTask<FileSystemHandleInfo?> GetFile(FileSystemHandleInfo directory, string name, bool create = false)ValueTask<bool> Remove(FileSystemHandleInfo directory, string name, bool recursive = false)ValueTask<FileSystemPermission> QueryPermission(FileSystemHandleInfo handle, bool write = false)ValueTask<FileSystemPermission> RequestPermission(FileSystemHandleInfo handle, bool write = false)ValueTask Release(FileSystemHandleInfo handle)ValueTask DisposeAsync()