Loft is a Rust-based HTTP file server with a Finder-inspired UI, public/private visibility, admin editing, and secret share links for private files and folders.
It is built as a single service with:
axumfor the web serveraskamafor server-rendered HTMLsqlx+ SQLite for metadata- local blob storage for file content
- cookie sessions + CSRF protection for admin flows
Implemented:
- public main-tree browsing
- admin login and logout
- public/private visibility rules
- secret share routes for private files and folders
- raw and download routes with safer content serving
- admin mutations for create, upload, rename, delete, toggle visibility, and regenerate share links
- rolling admin sessions with configurable expiry
- optional expiration times for secret share links
- text-file editing by stable
node_id - responsive SSR pages for desktop and phone layouts
- inline preview support for text, image, video, and audio files
- image and video previews that preserve aspect ratio, with fullscreen viewing
- video HLS playback with Auto, Preview, and Original stream choices
- background video preview generation with retry controls and an admin status page
- streaming upload staging so large uploads do not have to stay in memory
- byte-range raw file serving for browser media playback
/healthzfor deployment and container health checks
Still open as follow-up work:
- final visual polish pass
- Rust toolchain
- SQLite is embedded through
sqlx, so no separate database server is required
cargo runBy default Loft starts on 127.0.0.1:3000.
Open:
- public app: http://127.0.0.1:3000
- login page: http://127.0.0.1:3000/login
Loft is configured with environment variables.
| Variable | Default | Purpose |
|---|---|---|
LOFT_BIND_HOST |
127.0.0.1 |
Bind address |
LOFT_PORT |
3000 |
HTTP port |
LOFT_DATA_DIR |
data |
Data root for SQLite + blobs |
LOFT_ADMIN_USERNAME |
admin |
Admin login username |
LOFT_ADMIN_PASSWORD_HASH |
placeholder hash | Argon2id password hash |
LOFT_SESSION_SECRET |
dev-session-secret |
Session secret configuration |
LOFT_SESSION_TTL_DAYS |
30 |
Rolling login expiry in days |
LOFT_TEXT_EDITOR_SIZE_THRESHOLD |
1048576 |
Max editable text-file size in bytes |
LOFT_UPLOAD_SIZE_LIMIT |
52428800 |
Upload request size limit in bytes |
Example:
export LOFT_BIND_HOST=127.0.0.1
export LOFT_PORT=3000
export LOFT_DATA_DIR=./data
export LOFT_ADMIN_USERNAME=admin
export LOFT_ADMIN_PASSWORD_HASH='$argon2id$...'
export LOFT_SESSION_SECRET='replace-this-in-real-deployments'
export LOFT_SESSION_TTL_DAYS=30
cargo runTo generate a password hash for LOFT_ADMIN_PASSWORD_HASH:
cargo run -- admin hash-password 'replace-me'If you are deploying from Docker Hub and do not want to install Rust locally, you can generate the hash with the published image itself:
docker run --rm kaelzs/loft:latest admin hash-password 'replace-me'For LOFT_SESSION_SECRET, use a long random string. Two easy options:
openssl rand -hex 32python3 -c "import secrets; print(secrets.token_urlsafe(32))"There is a focused deployment guide at docs/deployment.md.
It covers:
- local runtime setup
- environment variables
- data directory layout
- Docker Hub run examples
- Docker Compose configuration
Pull the published image:
docker pull kaelzs/loft:latestGenerate an admin password hash:
docker run --rm kaelzs/loft:latest admin hash-password 'your-admin-password'Generate a session secret:
openssl rand -hex 32Prepare a writable data directory on Linux hosts:
mkdir -p ./data
sudo chown -R 10001:10001 ./dataRun Loft:
docker run --rm \
-p 3000:3000 \
-v "$(pwd)/data:/data" \
-e LOFT_ADMIN_USERNAME=admin \
-e LOFT_ADMIN_PASSWORD_HASH='replace-with-argon2id-hash' \
-e LOFT_SESSION_SECRET='replace-with-a-long-random-string' \
-e LOFT_SESSION_TTL_DAYS=30 \
kaelzs/loft:latestOpen:
Run it in the background:
docker run -d \
--name loft \
--restart unless-stopped \
-p 3000:3000 \
-v "$(pwd)/data:/data" \
-e LOFT_ADMIN_USERNAME=admin \
-e LOFT_ADMIN_PASSWORD_HASH='replace-with-argon2id-hash' \
-e LOFT_SESSION_SECRET='replace-with-a-long-random-string' \
-e LOFT_SESSION_TTL_DAYS=30 \
kaelzs/loft:latestUseful follow-up commands:
docker logs -f loft
docker stop loft
docker rm loftExample compose.yaml:
services:
loft:
image: kaelzs/loft:latest
container_name: loft
restart: unless-stopped
ports:
- "3000:3000"
environment:
LOFT_BIND_HOST: 0.0.0.0
LOFT_PORT: 3000
LOFT_DATA_DIR: /data
LOFT_ADMIN_USERNAME: admin
LOFT_ADMIN_PASSWORD_HASH: ${LOFT_ADMIN_PASSWORD_HASH}
LOFT_SESSION_SECRET: ${LOFT_SESSION_SECRET}
LOFT_SESSION_TTL_DAYS: 30
LOFT_TEXT_EDITOR_SIZE_THRESHOLD: 1048576
LOFT_UPLOAD_SIZE_LIMIT: 52428800
volumes:
- ./data:/dataExample .env:
LOFT_ADMIN_PASSWORD_HASH='$argon2id$...'
LOFT_SESSION_SECRET=replace-with-a-long-random-stringStart Loft:
docker compose up -dView logs:
docker compose logs -fStop Loft:
docker compose downUpdate to a newer image:
docker compose pull
docker compose up -dFor Docker Compose, keep the Argon2 password hash in .env and keep it quoted because the hash contains $.
If you do not want to use .env, you must escape every $ in the Argon2 hash as $$ inside compose.yaml.
On Linux hosts, make the mounted data directory writable by Loft's container user:
mkdir -p ./data
sudo chown -R 10001:10001 ./dataLoft currently supports these Docker environment variables:
| Variable | Docker image default | Purpose |
|---|---|---|
LOFT_BIND_HOST |
0.0.0.0 |
Bind address inside the container. |
LOFT_PORT |
3000 |
HTTP port inside the container. |
LOFT_DATA_DIR |
/data |
Internal storage path for SQLite and blobs. |
LOFT_ADMIN_USERNAME |
admin |
Admin login username. |
LOFT_ADMIN_PASSWORD_HASH |
placeholder hash | Argon2id hash for the admin password. Set this explicitly. |
LOFT_SESSION_SECRET |
replace-this-before-production |
Session signing secret. Set this explicitly. |
LOFT_SESSION_TTL_DAYS |
30 |
Rolling login expiry in days. Each valid request refreshes it. |
LOFT_TEXT_EDITOR_SIZE_THRESHOLD |
1048576 |
Max inline-editable text size in bytes. |
LOFT_UPLOAD_SIZE_LIMIT |
52428800 |
Max upload request size in bytes. |
Inside LOFT_DATA_DIR, Loft keeps:
loft.sqlite3for metadatablobs/for file content, stored by internal blob ID
Logical paths in the UI are not tied directly to blob storage paths.
- Admin passwords are checked against an Argon2id hash.
- Anonymous users can only browse publicly reachable nodes.
- Private content in the main tree returns
404to anonymous users. - Secret share links only work while the target node is private.
- Browser write actions require CSRF protection.
rawroutes sendX-Content-Type-Options: nosniff.
Run the full test suite:
cargo testFormatting check:
cargo fmt --checksrc/
app.rs
auth/
catalog/
storage/
web/
migrations/
static/
templates/
tests/
The visual direction is based on the files in Design/, with a warm Apple/Finder-like presentation adapted for both desktop and phone layouts.
- Product and UI design direction in this project was created with Claude Design.
- Application code and implementation work in this project was created with Codex.