Skip to content
Shiny.Net.HttpServer v1 - A lightweight feature rich HTTP Server - Tunnels, Websockets, AOT, ASPNET Featureset, & Works EVERYWHERE!Let me see!

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.

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.

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.