Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

File Browser

A directory served over plain HTTP: GET it for a JSON listing, GET a file for its bytes, PUT to write, DELETE to remove. On a phone the root is FileSystem.AppDataDirectory, which is the case it was built for — pulling logs, a database file or a capture off a device without a cable.

app.MapFileBrowser("/files", o =>
{
o.RootPath = FileSystem.AppDataDirectory;
o.AllowWrite = true;
})
.RequireAuthorization();

The API is deliberately plain HTTP, so anything that already speaks it — curl, a browser, a script — can drive it with no client library.

If you want the directory to appear as a drive rather than as an API — Finder, Windows Explorer, a file manager — WebDAV is the same idea over the protocol those already speak.

If you want it from a terminal rather than from an app, the command line tool serves a directory with the same per-operation permissions — though it mounts it over WebDAV rather than through this API, so that the same address is a file manager in a browser and a drive in Finder or Explorer.

Both samples in the repository map it over the same directory they mount with WebDAV, which is the quickest way to see the difference: samples/Sample.Api leaves reads open and puts writes behind its admin policy, and samples/Sample.Maui serves the app’s own storage from a link on the Server tab.

Request Result
GET /files JSON DirectoryListing of the root
GET /files/{path} A listing for a directory, or the file’s bytes
PUT /files/{path} Writes a file (requires AllowWrite)
PUT /files/{path}/ Creates a directory (requires AllowWrite and AllowCreateDirectories)
DELETE /files/{path} Removes a file, or an empty directory (requires AllowDelete)

A listing entry carries name, path, isDirectory, size, lastModifiedUtc and contentType, with directories first and then files, each alphabetically. Downloads go through the same code as every other download, so byte ranges and conditional GETs work — a resumable pull of a large capture over a flaky link is free.

The browser declares its own JsonSerializerContext, so it needs nothing registered in your app.

The prefix is an ordinary route prefix, so the table above moves with it — MapFileBrowser("/downloads", …) answers on GET /downloads/{path}. Pass "/" to hand the browser the whole site, which is what a plain file server is:

app.MapFileBrowser("/", o => o.RootPath = Directory.GetCurrentDirectory());

A root mount does not swallow the rest of the app. Literal segments are matched before the browser’s catch-all, so a /health endpoint mapped alongside it still answers, and a file called health in the served directory does not shadow it.

MapFileBrowser returns the endpoints it registered, which is the whole reason it is routes rather than middleware — authorization is endpoint metadata, and middleware could not express “reads are open, writes are not”.

// everything behind a policy
app.MapFileBrowser("/files", …).RequireAuthorization("admin");
// reads open, anything that changes something behind a policy
app.MapFileBrowser("/files", …).RequireAuthorizationForChanges("editors");

ReadEndpoints, WriteEndpoints, DeleteEndpoints and All are there if you want to attach something else — a rate limit, an IP filter — to one group.

Property Default Notes
RootPath required Everything is resolved inside it; nothing outside it is reachable
AllowWrite false
AllowDelete false
AllowCreateDirectories true Only relevant when AllowWrite is on
MaxUploadBytes 64 MB Counted as the body streams, not taken from Content-Length
ServeHiddenFiles false A content directory routinely holds a .env or a database journal
Filter null Return false to hide an entry from listings and refuse every operation on it
DefaultContentType application/octet-stream Downloads only

Read-only until told otherwise. AllowWrite and AllowDelete are both off, because the version of this that cannot fill a device’s storage or replace a file something depends on is the one worth defaulting to.

Uploads are bounded and atomic. MaxUploadBytes is counted as the body streams rather than trusting Content-Length, and the bytes go to a staging file that is moved into place — so a refused or interrupted upload leaves the previous file intact.

A directory is only deleted when empty. Recursive delete behind a URL is one mistyped path away from taking everything, and a phone has no undo.

Containment reuses the static file handler’s path normalization, checked after decoding and again after resolving links, with dotfiles hidden.