A tiny, single-binary file server and utility originally designed for Anbernic handhelds, but expanded to Windows and macOS. Point a browser at the device and you get a full file manager; mount it as a network drive over WebDAV; and read the device's IP, password, and a scannable QR code straight off the device's screen, or terminal.
Built and tested on the RG35XX Pro and RG34XXSP (Allwinner H700 /
aarch64, stock Anbernic OS) and the Steam Deck (Game Mode, via the SDL
build). The framebuffer and headless builds are a single statically-linked
binary with no runtime dependencies; the SDL on-screen build dynamically links
the system libSDL2 (present on SteamOS and the Anbernic stock OS).
- File-manager web UI — a single-page app served at
http://<device-ip>:8080/: list/grid views, image thumbnails, a hidden-files toggle (dotfiles only — the Windows hidden attribute and macOSUF_HIDDENflag are not consulted), in-browser preview (images, video, audio, text) with arrow-key gallery navigation, an in-browser text editor to edit files in place (e.g.config.json), breadcrumbs, sortable columns, shift-click range select, right-click context menu (new folder, upload, download, cut/copy/paste, delete, rename), drag-and-drop upload with a progress bar, and zip download of folders or multi-selections. - WebDAV server — mount the device as a network drive from any WebDAV
client (Finder, Windows Explorer,
rclone, etc.) athttp://<device-ip>:8080/dav(also browsable directly in a web browser). - On-screen connection info — the device's IP, login password, and a QR
code (linking to the web UI) are drawn directly to the framebuffer
(
/dev/fb0), so a headless handheld is usable the moment it boots the app. - Live gamepad button viewer — the Status tab streams every button/axis event (name + raw evdev code + up/down) live over SSE; handy for discovering button mappings.
- Unified settings — every setting (root, port, bind address, password,
permission level, default folder, screensaver) is reachable three ways with a
clear precedence: CLI flags →
AMBERDAV_*env vars → config file → defaults. The web UI displays settings read-only; it never edits the file.
| Login | File manager | Context menu |
|---|---|---|
![]() |
![]() |
![]() |
Cross-compiled from any host (macOS/Linux/Windows) to a static aarch64 musl
binary using cargo-zigbuild
(Zig is the cross-linker; the whole dependency tree is pure Rust).
# One-time setup
rustup target add aarch64-unknown-linux-musl
cargo install cargo-zigbuild
# Zig must be installed and on PATH — e.g. `brew install zig`,
# `pip install ziglang`, or from https://ziglang.org/download/
# Device build — `--features fb` pulls in the on-device screen + gamepad UI.
cargo zigbuild --release --target aarch64-unknown-linux-musl --features fbThe fb and sdl features each gate the device-only code (gamepad
input, DVD-bounce screensaver, the on-screen connection canvas) and its
dependencies; they differ only in the display sink — fb is the static
framebuffer/Wayland sink, sdl links the system libSDL2. Both are opt-in:
with neither you get a smaller, headless WebDAV/file-server binary suited to
desktops and servers. Build a headless binary for any target by simply omitting
the features, e.g. cargo build --release or
cargo zigbuild --release --target x86_64-unknown-linux-musl.
Output: target/aarch64-unknown-linux-musl/release/amber-dav (~2 MB, static):
$ file target/aarch64-unknown-linux-musl/release/amber-dav
ELF 64-bit LSB executable, ARM aarch64, statically linked, stripped
The same aarch64 binary runs on both the RG35XX Pro and the RG34XXSP.
Each GitHub Release ships a prebuilt binary per platform:
Assets follow amber-dav-<arch>-<os>[-fb|-sdl]: plain <arch>-<os> is the
standard headless build, the -fb suffix marks the static build with the
on-device framebuffer/Wayland UI compiled in, and -sdl is the dynamic
on-screen build (links the system libSDL2).
| Asset | Platform | Build |
|---|---|---|
amber-dav-aarch64-linux |
ARM Linux servers/Raspberry Pi/NAS/Graviton (static musl) | headless |
amber-dav-aarch64-linux-fb |
the Anbernic device (static musl) | fb |
amber-dav-aarch64-linux-sdl |
Anbernic (SDL/mali) |
sdl (dynamic, needs libSDL2) |
amber-dav-x86_64-linux |
x86 Linux servers/NAS/Docker (static musl) | headless |
amber-dav-x86_64-linux-fb |
x86 Linux handhelds without libSDL2 | fb |
amber-dav-x86_64-linux-sdl |
Steam Deck | sdl (dynamic, needs libSDL2) |
amber-dav-aarch64-macos |
macOS, Apple Silicon | headless |
amber-dav-x86_64-macos |
macOS, Intel | headless |
amber-dav-aarch64-windows |
Windows on ARM (Snapdragon/Copilot+ PCs) | headless |
amber-dav-x86_64-windows |
Windows | headless |
The -fb and -sdl assets are built with --features fb and --features sdl
respectively: both include the on-device screen and gamepad viewer (differing
only in the display sink). Every other asset is a
headless WebDAV/file-server CLI that writes to stdout — a quick way to host
a folder from a desktop or server:
amber-dav /path/to/folder 8080 # positional root + port
amber-dav --root /path/to/folder --port 8080 --bind 127.0.0.1
amber-dav --help # full flag/env listThe stock OS launches apps from Roms/APPS/ on the SD card: a *.sh script
there appears in the Apps menu, and the filename becomes the menu label. This
repo ships a ready-made launcher under example_APPS/.
Copy this layout to the SD card (the games/second card on a two-slot device):
Roms/APPS/WebDAV.sh <- launcher (this is the Apps-menu entry)
Roms/APPS/webdav/amber-dav <- the aarch64 binary you built
Roms/APPS/webdav/config.json <- written automatically on first launch
Roms/APPS/webdav/log.txt <- created on launch (IP, password, QR text)
Then launch WebDAV from the device's Apps menu. WebDAV.sh serves the whole
SD card root (two levels up from Roms/APPS) on port 8080 and writes startup
output to log.txt. Edit the script to change the port, served root, or screen
rotation — it's commented.
The binary takes optional
[ROOT] [PORT]positional arguments (defaults: current dir,8080). CLI flags andAMBERDAV_*env vars take precedence overconfig.json, so a launcher can override the file without editing it.
Use the amber-dav-x86_64-linux-sdl asset — add it as a Non-Steam Game and
launch it.
The SDL build links the system libSDL2 (present on SteamOS) and auto-selects
the video driver (x11 on the Deck). Force one with SDL_VIDEODRIVER if needed.
- In Desktop Mode, copy the binary somewhere persistent (e.g.
~/Applications). - Add it to Steam as a Non-Steam Game, then switch to Game Mode and launch it.
- The connection screen appears; scan the QR or read the IP/password.
- Quit with the configured exit button (the ☰ Menu button by default; see
exit_keys) or by closing the window from the Steam overlay.
Optionally point --connection-file at a path a launcher script or a Decky
plugin can read (e.g. --connection-file ~/.local/share/amber-dav/connection.json).
A Decky Loader plugin would live in its own repo and bundle this same binary; amber-dav needs no Decky-specific code.
The amber-dav-aarch64-linux-sdl asset renders the same screen via SDL's
mali vendor driver (what the stock emulators use), as an alternative to the
static framebuffer build. Just launch it from an APPS-menu *.sh script like
any other binary — the SDL driver is auto-selected (mali on the Anbernic, once
x11 is found unavailable), so no SDL_VIDEODRIVER is needed. Set
SDL_VIDEODRIVER=mali only to force it and skip the auto-detection.
On startup the device screen and log.txt show the connection details:
amber-dav
serving: /mnt/sdcard
status: http://192.168.1.42:8080/
webdav: http://192.168.1.42:8080/dav
password: 9vqcm4xt (user: anything)
<QR code to the status page>
- Web UI: open
http://<device-ip>:8080/(or scan the QR), log in with the password shown on the device. - WebDAV: point a client at
http://<device-ip>:8080/dav. The username is ignored; the password is the one on screen.
By default the password is a fresh random 8-character code each launch. Set a
fixed password in config.json (below) so you don't have to re-read it every
time.
-
Windows Explorer / "Map network drive" does not work out of the box. The built-in WebClient service refuses Basic auth over plain HTTP (amber-dav has no TLS) and caps downloads at ~50 MB. To use it anyway, set two values under
HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters:BasicAuthLevel(DWORD) =2— allow Basic auth over HTTPFileSizeLimitInBytes(DWORD) — default50000000(~50 MB); raise it up to4294967295(~4 GB)
then restart the service (
net stop webclient && net start webclient, as administrator). The simpler path on Windows is a real WebDAV client — rclone, WinSCP, or Cyberduck — all of which connect without registry changes. -
macOS Finder (Go ▸ Connect to Server,
http://<device-ip>:8080/dav) works, but Finder issues a flood of PROPFIND requests and can be slow on large folders.rcloneor Cyberduck are faster. -
Locks are advisory. The server answers
LOCK/UNLOCK(some clients, e.g. the Windows mini-redirector and Office, insist on them) but does not enforce them — fine for a single-user LAN tool, just don't expect two writers to be protected from each other.
Every setting can be supplied three ways. When a setting is given in more than one place, the highest layer wins:
- CLI flags —
--root,--port, … (runamber-dav --helpfor the list) AMBERDAV_*env vars — for containers/deploymentconfig.json— persisted preferences- compiled-in defaults
| Setting | CLI flag | Env var | Config key |
|---|---|---|---|
| Served root | --root <path> (or positional 1) |
AMBERDAV_ROOT |
root |
| Port | --port <n> (or positional 2) |
AMBERDAV_PORT (or PORT) |
port |
| Bind address | --bind <addr> |
AMBERDAV_BIND |
bind |
| Password | --password <pw> |
AMBERDAV_PASSWORD |
password |
| Show password | --display-password / --no-display-password |
AMBERDAV_DISPLAY_PASSWORD |
display_password |
| Default folder | --default-folder <path> |
AMBERDAV_DEFAULT_FOLDER |
default_folder |
| Sidebar favorites | — | — | favorites |
| Permission | --permission <level> |
AMBERDAV_PERMISSION |
permission |
| Screensaver on | --bounce-screen / --no-bounce-screen |
AMBERDAV_BOUNCE_SCREEN |
bounce_screen.enabled |
| Screensaver folders | --bounce-folders <a,b,…> |
AMBERDAV_BOUNCE_FOLDERS |
bounce_screen.folders |
| Connection file | --connection-file <path> |
AMBERDAV_CONNECTION_FILE |
connection_file |
| Exit key codes | --exit-keys <a,b,…> |
AMBERDAV_EXIT_KEYS |
exit_keys |
| Blank-screen key codes | --blank-keys <a,b,…> |
AMBERDAV_BLANK_KEYS |
blank_keys |
| Bounce-toggle key codes | --bounce-keys <a,b,…> |
AMBERDAV_BOUNCE_KEYS |
bounce_keys |
| Display sink | — | AMBERDAV_DISPLAY |
— |
| Screen rotation (fb builds) | — | AMBERDAV_FB_ROTATE |
— |
| Log level | --verbose (debug) |
AMBERDAV_LOG (or RUST_LOG): off/error/warn/info/debug/trace |
— |
| Build | Location |
|---|---|
device (fb/sdl) |
next to the binary — config.json |
| macOS | ~/Library/Application Support/amber-dav/config.json |
| Windows | %APPDATA%\amber-dav\config\config.json |
| Linux (headless) | $XDG_CONFIG_HOME/amber-dav/config.json → ~/.config/amber-dav/config.json |
$AMBERDAV_CONFIG overrides the location on any build.
On device (fb/sdl) builds a default config.json is written next to the
binary on first launch (the device is configured through the web UI). The
generated file documents every option and its allowed values in // comments,
so it can be edited in place without consulting this README. Headless builds
never write a config implicitly — run with --save to write the fully-resolved
settings (CLI + env merged onto any existing file) to the config path and exit:
amber-dav --root /mnt/media --password secret --permission read_only --save
# → writes the config file with those values, then exitsMigrating from an older desktop build: the config no longer lives next to the binary on desktop/server installs — it moved to the platform location above. Move your existing
config.jsonthere (or point$AMBERDAV_CONFIGat it, or re-create it with--save); otherwise it is ignored and defaults apply.
Edit the file directly or over WebDAV, then relaunch the app to apply changes.
The file is parsed as JSONC: // and /* */ comments and trailing commas
are all accepted, so the example above works as written. If the file still
fails to parse, the app boots with defaults and shows the parse error on the
device screen and the web Status tab (and stderr), so a broken config is
never silently ignored.
Permission levels are enforced on both the JSON API and the WebDAV mount:
| Level | Read / browse | Create / upload / rename / move | Delete |
|---|---|---|---|
read_only |
✅ | ❌ | ❌ |
read_write |
✅ | ✅ | ❌ |
read_write_delete |
✅ | ✅ | ✅ |
permission, default_folder, and favorites take effect per request. password,
display_password, root, port, bind, bounce_screen, and the *_keys
lists are bound at boot — relaunch to apply.
| Button | evdev code | Action |
|---|---|---|
Menu (KEY_GOTO, Anbernic) / ☰ (BTN_START, Steam Deck) |
354, 315 | Quit the app and return to the OS menu (exit_keys) |
A (BTN_SOUTH) |
304 | Blank the screen (all black); press again to restore (blank_keys) |
X (BTN_NORTH) |
307 | Toggle the bounce screensaver, if bounce_screen.enabled (bounce_keys) |
The bounce screensaver drifts a random image around a black screen,
DVD-logo style, swapping images as it ricochets off the edges — preventing
burn-in on OLED/AMOLED panels during long idle periods. It draws from the
images in bounce_screen.folders (PNG, JPEG, GIF, BMP, WebP). With no images
configured it simply blanks to black, which still protects the panel.
The screensaver runs on the framebuffer and SDL sinks only. The Wayland
sink — what the fb build uses under Gamescope, i.e. Steam Deck Game
Mode — does not support it: pressing X shows the info screen instead. On an
OLED Deck, use the -sdl build if you want the burn-in saver in Game Mode,
or blank the screen with A.
Each control is a list of evdev codes, so a button can differ per device
(the defaults above already cover the Anbernic and the Steam Deck). Retarget
them from the config file (exit_keys/blank_keys/bounce_keys), the
AMBERDAV_EXIT_KEYS/AMBERDAV_BLANK_KEYS/AMBERDAV_BOUNCE_KEYS env vars
(comma-separated), or --exit-keys/--blank-keys/--bounce-keys. Find a
button's code in the web UI Status tab's live input view.
The Settings tab has a Software Update card. Click Check for update and AmberDAV queries the GitHub Releases API to compare the running version against the latest release. If a newer build is available, click Apply update to download and install it in place:
- The matching platform binary is downloaded from GitHub Releases.
- The current binary is renamed to
amber-dav.old(in the same directory), replacing any previous.oldfile. - The new binary is moved into place.
- Relaunch the app to run the new version — the process is not restarted automatically.
If anything goes wrong during the rename step, the original binary is restored.
The update check and apply target the matching prebuilt release asset (see the table above). A device binary only ever pulls its own asset — an
-fbbuild stays-fb, an-sdlbuild stays-sdl, and a headless binary only the headless one — so a device never self-updates across build shapes. Custom builds with no matching asset see a "no asset for this platform" response.
You can replace the binary without pulling the SD card. Because you can't write
over a running executable (ETXTBSY), upload to a temp name and MOVE it into
place (a rename over a running binary is allowed):
BIN=target/aarch64-unknown-linux-musl/release/amber-dav
HOST=http://192.168.1.42:8080
PASS=littleSecr3t
# Upload alongside the running binary, then atomically swap it in.
curl -u x:$PASS -T "$BIN" "$HOST/dav/Roms/APPS/webdav/amber-dav.new"
curl -u x:$PASS -X MOVE \
-H "Destination: $HOST/dav/Roms/APPS/webdav/amber-dav" \
"$HOST/dav/Roms/APPS/webdav/amber-dav.new"Quit (Menu button) and relaunch from the Apps menu to run the new build. This
requires permission to be read_write or read_write_delete.
- Screen looks rotated / sideways. The on-screen info is authored
landscape, and portrait-mounted panels (e.g. the RG34XXSP) are auto-rotated
90° — the Status tab shows
rot=90(auto)when that kicked in. If a panel still comes out wrong, setAMBERDAV_FB_ROTATEto0,90,180, or270to override (there's a commented line inWebDAV.sh). - Blank screen / frozen on the loading splash. Some Allwinner framebuffer
drivers only present a frame on an
FBIOPAN_DISPLAYioctl and use multiple buffer pages; amber-dav handles both. Checklog.txtand the Status tab — the screen line reports geometry, e.g.ok (640x480 32bpp rot=0 pages=2 virt=960 pan=true) mode=Info. - Buttons don't register. The input viewer reads
/dev/input/event*. The stock OS runs apps as root, so this normally just works; otherwise the process needs read access to those device nodes. - Can't connect. Confirm the phone/PC is on the same LAN/Wi-Fi as the
handheld, and that the IP in
log.txtmatches what you're hitting.
This is a LAN tool. It serves plain HTTP (no TLS) and authenticates with a short shared password — fine for a trusted home network, not for the open internet. Don't port-forward it. Login uses a session cookie; the WebDAV mount uses HTTP Basic auth. Path-traversal is blocked, and the configured permission level is enforced on every mutating request.
| File | Purpose |
|---|---|
src/main.rs |
startup, settings resolution, routing, shared state, banner + QR |
src/cli.rs |
CLI flags (clap) + CLI/env/file/default precedence resolution |
src/config.rs |
config.json schema + location, load/save, permission levels |
src/auth.rs |
session-cookie login for the web UI |
src/webdav.rs |
dav-server handler bridged into axum + Basic auth + permission gate |
src/files.rs |
JSON file API (list/upload/download/zip/rename/move/copy/delete) + HTTP Range |
src/input.rs |
evdev reader → broadcast channel; drives screen controls (device builds only) |
src/screen.rs |
draws IP/password/QR to /dev/fb0; blank + bounce screensaver (device builds only) |
src/ui.rs |
landing/login pages, status/info endpoint, settings (read-only), SSE stream |
src/update.rs |
in-app update: GitHub Releases check + binary download/rename dance |
src/password.rs |
per-boot password generator |
src/web/ |
login.html, app.html (the single-page file manager) |
example_APPS/ |
ready-to-copy WebDAV.sh launcher + SD-card layout |
MIT — see LICENSE.



{ // Fixed login password. Omit or leave empty for a fresh random code each boot. "password": "littleSecr3t", // Show the password on the device screen. Forced on for a random password // (otherwise it could never be discovered). Set false to hide a fixed one. "display_password": true, // Absolute path to serve. Omit/empty to use the CLI argument / default ("."). "root": null, // Port to listen on. Omit/null to use the CLI argument / default (8080). "port": null, // Address to bind. Omit/null for 0.0.0.0 (all interfaces); "127.0.0.1" for // tunneled/proxied deployments. "bind": null, // Folder (relative to root) to open right after login. "" = root. "default_folder": "Roms", // Named folder shortcuts shown in the web UI sidebar, in order. Each `path` // is relative to the served root (same as default_folder; "" = root). Omit // or leave empty for no Favorites section. Handy on device builds for // jumping between a few frequently-used folders. "favorites": [ { "name": "Game Boy", "path": "Roms/GB" }, { "name": "Screenshots", "path": "Roms/Imgs" } ], // Allowed operations: "read_only" | "read_write" | "read_write_delete". "permission": "read_write_delete", // Burn-in screensaver (see Controls). Toggled on-device with the X button. "bounce_screen": { "enabled": true, // Files or folders to draw images from. Folders are scanned recursively. // Relative entries resolve against the served root; absolute paths work too. "folders": ["Roms/GBA/Imgs", "Roms/SNES/Imgs"] } }