Skip to content

Latest commit

Β 

History

82 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

LazyBucket

Self-hosted object storage in a single Rust binary.

CI License

LazyBucket is an S3-inspired object storage server you can run yourself. It keeps object bytes on disk and their metadata in SQLite, exposes an HTTP API, and ships with a built-in web UI β€” all in a single binary and a ~145 MB Docker image. It needs no cloud account and no services beyond itself.


Highlights

Storage & API

  • S3-style buckets and objects with an ergonomic REST API
  • Streaming uploads and downloads β€” large files never sit fully in memory
  • HTTP Range requests (206 Partial Content) for seeking and resumable downloads
  • Conditional requests: ETag/If-None-Match and Last-Modified/If-Modified-Since β†’ 304
  • Multipart uploads with per-part ETag validation, for very large files
  • Server-side copy and cross-bucket move
  • Folder-style listing via delimiter, plus cursor pagination
  • Prefix bulk-delete and custom object metadata (x-meta-*)
  • Automatic content-type inference and gzip compression for JSON responses

Web UI

  • Drag-and-drop uploads with progress (chunked for large files)
  • Folder navigation with breadcrumbs, in-folder search, and per-object details
  • Copy / move / rename / delete, copy public link, bucket & store statistics

Operations

  • Single binary; one small (~145 MB) Docker container
  • Liveness, readiness and Prometheus metrics endpoints
  • Structured (JSON) or human-readable logging, configurable log levels
  • Graceful shutdown, startup cleanup and a TTL sweeper for abandoned uploads
  • Constant-time credential checks and path-traversal-safe storage

Architecture

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        HTTP / REST
        β”‚   React SPA    β”‚ ─────────────────────┐
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β–Ό
                                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   curl / SDK / any client  ──────────▢ β”‚   Axum server     β”‚
                                        β”‚  (single binary)  β”‚
                                        β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                                        β”‚ SQLite β†’ metadata β”‚  buckets Β· objects Β· uploads
                                        β”‚ Disk   β†’ bytes    β”‚  ./storage/<bucket>/<key>
                                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Object bytes live on the filesystem under ./storage/<bucket>/<key>; their metadata (size, content type, ETag, timestamps, custom x-meta-* fields) is indexed in SQLite for fast listing and lookup. The two are kept consistent: uploads are idempotent, failed writes roll back, and deleting or renaming a bucket cascades to its objects and files.


Quick start

Docker Compose (recommended)

USER_LOGIN=admin USER_PASSWORD=secret docker compose up --build

The database and objects are persisted in the lazybucket-data volume. Open http://localhost:8000 and sign in with the credentials above.

Docker

docker build -t lazybucket .

docker run -p 8000:8000 \
  -e USER_LOGIN=admin \
  -e USER_PASSWORD=secret \
  -v "$(pwd)/data:/app/data" \
  -e DATABASE_URL="sqlite:///app/data/database.db?mode=rwc" \
  -e STORAGE_PATH="/app/data/storage" \
  lazybucket

Local development

# Backend (serves the API on :8000)
USER_LOGIN=admin USER_PASSWORD=secret cargo run

# Frontend (Vite dev server, proxies /api to the backend)
cd frontend && npm install && npm run dev

Configuration

All configuration is via environment variables. Only the credentials are required.

Variable Required Default Description
USER_LOGIN yes β€” Login for HTTP Basic auth
USER_PASSWORD yes β€” Password for HTTP Basic auth
HOST 0.0.0.0 Bind host
PORT 8000 Bind port
DATABASE_URL sqlite://database.db?mode=rwc SQLite connection URL
STORAGE_PATH ./storage Directory for object files
FRONTEND_DIR ./frontend/dist Built SPA directory to serve
MAX_UPLOAD_BYTES 5368709120 (5 GiB) Maximum single upload size
UPLOAD_TTL_SECS 86400 (24 h) Lifetime of an incomplete multipart upload before it's swept
CORS_ALLOWED_ORIGINS (empty) Comma-separated allowed origins for cross-origin API clients. Empty = same-origin only; * = any origin. The bundled UI is same-origin and needs no CORS.
RUST_LOG info Log level/filter, e.g. lazybucket=debug,tower_http=debug
LOG_FORMAT (text) json for structured logs; anything else = human-readable

See .env.example for a ready-to-copy template.


API

Base path: /api. Requests and responses use JSON; errors return { "error": "<message>" } with an appropriate status code.

Authentication β€” HTTP Basic: Authorization: Basic base64(login:password). Public (no auth): GET /api/health, /api/version, /api/ready, /api/metrics, and object downloads (GET/HEAD /api/:bucket/*key). Everything else requires authentication.

Service

Method Path Description
GET /api/health Liveness β€” always ok
GET /api/ready Readiness β€” pings the database (503 if unreachable)
GET /api/version Build name and version
GET /api/stats Store totals: buckets, objects, bytes (auth)
GET /api/metrics Prometheus metrics (public)

Buckets

Method Path Description
GET /api/buckets List buckets with per-bucket object count and size
PUT /api/buckets/:name Create a bucket
PATCH /api/buckets/:name Rename a bucket ({"name":"new"})
DELETE /api/buckets/:name Delete a bucket and all its objects

Objects

Method Path Description
GET /api/:bucket List objects β€” ?prefix=, ?delimiter=/ (folders), ?limit=, ?after= (pagination)
PUT /api/:bucket/*key Upload an object (see notes)
GET /api/:bucket/*key Download β€” supports Range, If-None-Match, If-Modified-Since
HEAD /api/:bucket/*key Object metadata headers, no body
PATCH /api/:bucket/*key Rename ({"key":"new"}) or move ({"key":"new","bucket":"other"})
DELETE /api/:bucket/*key Delete an object
DELETE /api/:bucket?prefix=folder/ Bulk-delete every object under a prefix

On upload (PUT) you can also:

  • attach custom metadata with x-meta-* request headers (echoed back on GET/HEAD);
  • copy an existing object instead of sending a body with x-copy-source: <bucket>/<key>.

Multipart uploads

For very large files, upload in parts and assemble server-side:

Method Path Description
POST /api/:bucket/*key?uploads Initiate β€” returns { "uploadId": "…" }
PUT /api/:bucket/*key?uploadId=..&partNumber=N Upload part N β€” returns the part's ETag
POST /api/:bucket/*key?uploadId=.. Complete β€” optionally send { "parts": [{ "partNumber", "etag" }] } to validate
DELETE /api/:bucket/*key?uploadId=.. Abort and discard the session

Examples

BASE=http://localhost:8000/api
AUTH='-u admin:secret'

# Create a bucket and upload an object with metadata
curl $AUTH -X PUT  "$BASE/buckets/photos"
curl $AUTH -X PUT --data-binary @cat.jpg \
     -H 'x-meta-author: alice' "$BASE/photos/animals/cat.jpg"

# Browse like folders
curl $AUTH "$BASE/photos?delimiter=/"           # β†’ { objects, prefixes, next }

# Download a byte range
curl "$BASE/photos/animals/cat.jpg" -H 'Range: bytes=0-1023' -o head.bin

# Server-side copy, then move to another bucket
curl $AUTH -X PUT -H 'x-copy-source: photos/animals/cat.jpg' "$BASE/photos/backup/cat.jpg"
curl $AUTH -X PATCH -H 'Content-Type: application/json' \
     -d '{"key":"cat.jpg","bucket":"archive"}' "$BASE/photos/backup/cat.jpg"

Security notes

  • Credentials are compared in constant time; every rejection returns 401 with a WWW-Authenticate challenge.
  • Bucket names and object keys are validated against path traversal before touching disk.
  • CORS is closed by default; open it explicitly per origin via CORS_ALLOWED_ORIGINS.
  • LazyBucket speaks plain HTTP β€” terminate TLS at a reverse proxy (nginx, Caddy, Traefik) in production, and use a strong USER_PASSWORD.

Development

cargo test                                   # unit + HTTP integration tests
cargo fmt --all --check                      # formatting
cargo clippy --all-targets -- -D warnings    # lints
cd frontend && npm run build                 # build the SPA

docker compose up --build                    # full end-to-end run

CI (GitHub Actions) runs formatting, Clippy, the test suite, and the frontend build on every push and pull request.

Project layout

src/
  main.rs            binary entrypoint (config, startup, graceful shutdown)
  lib.rs             router assembly, CORS/compression, upload sweeper
  api/               handlers, auth middleware, typed error responses
  db/                SQLite access: buckets, objects, uploads, stats
  storage/           on-disk object storage + name validation
migrations/          schema migrations (embedded at build time)
frontend/            React + Vite single-page app

License

MIT Β© 2026 Tsunami43

About

πŸͺ£ Self-hosted S3-inspired object storage in a single Rust binary β€” streaming, multipart, web UI.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages