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.
Endpoints
Section titled “Endpoints”| 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.
Where it mounts
Section titled “Where it mounts”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.
Authorization
Section titled “Authorization”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 policyapp.MapFileBrowser("/files", …).RequireAuthorization("admin");
// reads open, anything that changes something behind a policyapp.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.
Options
Section titled “Options”| 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 |
The defaults, and why
Section titled “The defaults, and why”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.


