A lightweight object storage and static site hosting MVP with a Go API, React console, MySQL metadata, and local filesystem object storage.
English | Chinese
light-oss is designed for internal tools, prototypes, lightweight private deployments, and systems that need a simple object storage base that is easy to run and modify.
It is not a full S3-compatible service. The current implementation focuses on a clear MVP: bucket management, object upload/download, directory browsing, signed downloads, static site hosting, and a web console.
backend/: Go + Gin API for buckets, objects, folders, signed URLs, and site bindings.frontend/: React + TypeScript + Vite management console.gateway/: Nginx gateway for console, API, and hosted-site routing.docker-compose.yml: local stack for MySQL, backend, frontend, and gateway.
- Bucket create/list/delete, including cascading object and site cleanup.
- Object upload, download, metadata lookup, listing, deletion, and
public/privatevisibility. - Bearer Token authentication for private APIs and private object access.
- Signed download paths for private objects and signed single-object uploads that reuse the ordinary upload pipeline.
- Folder tree, folder creation/deletion, directory entries, search, pagination, and ZIP folder download.
- Batch folder upload with
multipart/form-dataand a manifest. - Static site hosting from
bucket + root_prefix, with custom domains, index document, error document, and SPA fallback. - Health checks, upload size limits, basic rate limiting,
request_id, and structured error responses.
- Backend: Go 1.22+, Gin, GORM, golang-migrate
- Frontend: React, TypeScript, Vite, TanStack Query, Axios
- UI: Tailwind CSS, Radix / shadcn-style components
- Database: MySQL 8.x
- Gateway: Nginx
.
├─ backend/
│ ├─ cmd/server
│ ├─ docs/openapi.apifox.json
│ ├─ internal/
│ └─ migrations/
├─ frontend/
├─ gateway/
├─ docker-compose.yml
├─ Makefile
└─ .env
- Go 1.22+
- Node.js 20+
- npm
- MySQL 8.x
- Docker Desktop or Docker Engine, if you use Compose
The simplest local setup is to run MySQL with Docker, then run the backend and frontend directly on your machine.
-
Prepare the root
.envor.env.personal.Local backend and frontend commands prefer
.env.personalwhen it exists. If.env.personalis found,.envis not merged for missing values, so keep.env.personalcomplete.Recommended local values:
APP_ENV=development APP_ADDR=:8080 APP_STORAGE_MODE=local APP_RATE_LIMIT_BACKEND=local APP_STORAGE_ROOT=./light-oss-data/storage APP_BEARER_TOKENS=light-oss APP_SIGNING_SECRET=change-me-in-local-dev DB_DSN=root:112233ss@tcp(localhost:3306)/light-oss?charset=utf8mb4&parseTime=True&loc=UTC&multiStatements=true VITE_DEFAULT_API_BASE_URL=http://localhost:8080 VITE_DEFAULT_BEARER_TOKEN=light-oss
-
Start MySQL.
docker compose up -d mysql
-
Start the backend.
cd backend go test ./... go run ./cmd/server
The API listens on
http://localhost:8080by default. -
Start the frontend in another terminal.
cd frontend npm install npm test npm run dev
The console runs at
http://localhost:3000by default. -
Open
/settingsin the console and confirm the API Base URL and Bearer Token.
Use this mode when you want to verify the full gateway and hosted-site domain flow.
-
Adjust the root
.envfor gateway mode.APP_STORAGE_MODE=local APP_RATE_LIMIT_BACKEND=local APP_STORAGE_ROOT=/data/storage VITE_DEFAULT_API_BASE_URL=http://api.localhost VITE_DEFAULT_BEARER_TOKEN=light-oss
-
Add local hosts entries if needed.
127.0.0.1 console.localhost 127.0.0.1 api.localhost 127.0.0.1 demo.localhost -
Start the stack.
docker compose up --build
Or:
make up
-
Visit the services.
- Console:
http://console.localhost - API:
http://api.localhost - MySQL:
localhost:3306 - Example hosted site:
http://demo.localhost
- Console:
Gateway routing:
console.localhost-> frontendapi.localhost-> backend API- other hostnames -> backend static site resolver
Custom site domains must point to the gateway through DNS or hosts. The gateway currently handles HTTP only; HTTPS, certificate management, and production DNS automation are outside this MVP.
- Root
.envis used by Docker Compose. Compose pins the in-container listener and storage root to:8080and/data/storageso host paths cannot leak into container configuration. - Root
.env.personalis preferred by local backend/frontend commands when it exists. - Frontend settings saved in browser
localStorageoverrideVITE_DEFAULT_API_BASE_URLandVITE_DEFAULT_BEARER_TOKEN. - Signing endpoints return relative API paths. Signed uploads also return the exact request headers that the uploader must send unchanged, each exactly once. Clients must preserve additional returned headers.
- Signing JSON and multipart uploads accept raw filenames. Only the single-object PUT
X-Original-Filenameheader is percent-decoded, once at the HTTP boundary; literal percent sequences are preserved in stored metadata. - Gateway access logs retain request paths, status codes, and timings but omit query strings and Referer headers to keep signed URLs out of access logs.
APP_STORAGE_MODEdefaults tolocal, in which case the service createsAPP_STORAGE_ROOTas needed. Use a host path for direct local runs; the default Compose setup uses/data/storage.APP_STORAGE_MODE=shared-filesystemselects an operator-provided shared volume.APP_STORAGE_ROOTmust already be mounted because the service will not create a missing shared root. The volume must provide read-write-many access, coherent cross-instance visibility, atomic rename within one filesystem, and read/write/delete permissions. A root-level.storage-idsentinel is bound to MySQL so an instance mounted to a different volume is rejected.- Object quota is coordinated by the MySQL
used_bytes/reserved_bytesledger.APP_CHUNK_SIZE_BYTEScontrols streaming reservation increments, andAPP_STORAGE_STAGING_TTL_SECONDScontrols recovery of abandoned staging blobs. DB_CONNECT_TIMEOUT_SECONDS,DB_READ_TIMEOUT_SECONDS, andDB_WRITE_TIMEOUT_SECONDSenforce finite MySQL network waits (defaults: 5/300/30 seconds), including an upper bound on an uncertain transaction commit response.APP_BEARER_TOKENSis the Bearer Token allowlist. Multiple tokens are comma-separated.APP_RATE_LIMIT_BACKENDdefaults tolocal. Set it tomysqlto coordinate every IP and authenticated route bucket through the sharedrate_limit_bucketstable; all replicas must use identical rate and burst settings. A database decision error fails closed with HTTP 503 instead of bypassing the limit.- Rate limiting has two levels:
APP_RATE_LIMIT_IP_RPS/APP_RATE_LIMIT_IP_BURSTapply a coarse client-IP budget before authentication, whileAPP_RATE_LIMIT_MANAGEMENT_RPS/APP_RATE_LIMIT_MANAGEMENT_BURSTare the authenticated management API budget. - Public object/site downloads, uploads, signed-link creation, and health checks use independent budgets. Override them with
APP_RATE_LIMIT_PUBLIC_*,APP_RATE_LIMIT_UPLOAD_*,APP_RATE_LIMIT_SIGN_*, andAPP_RATE_LIMIT_HEALTH_*; each route class has its own explicit default. APP_RATE_LIMIT_CACHE_TTL_SECONDScontrols local-cache eviction and shared MySQL bucket expiry;APP_RATE_LIMIT_CACHE_MAX_ENTRIESis a hard cap for both the local cache and shared MySQL buckets. At shared capacity, a new key receives 429 while existing keys retain their buckets; expired rows release capacity.APP_TRUSTED_PROXIESis a comma-separated allowlist of exact gateway IP addresses or CIDRs; leave it empty to ignore forwarded client-IP headers, and never use an all-address range.shared-filesystemis covered by a two-instance test against one MySQL database and shared root, including cross-instance upload/read/overwrite/cleanup and lease/staging takeover. The MySQL limiter is also covered by a two-Router concurrency test that proves one shared burst. The backend must remain atreplicas: 1whenAPP_STORAGE_MODE=localorAPP_RATE_LIMIT_BACKEND=local; horizontal scaling requires both shared modes and validation against the actual shared volume described in the runbook.- Do not commit real production passwords, signing secrets, tokens, or domain configuration.
- OpenAPI document:
backend/docs/openapi.apifox.json - Upload performance smoke baseline:
backend/docs/upload-performance-baseline.zh-CN.md - Blob ledger, reconciliation, and migration runbook:
backend/docs/storage-lifecycle-operations.zh-CN.md - Legacy schema retirement note:
backend/docs/legacy-upload-session-tables.zh-CN.md - Backend code organization:
backend/docs/code-organization.zh-CN.md - Main authenticated API prefix:
/api/v1 - Liveness:
GET /livez; readiness:GET /readyz - Authenticated health check:
GET /api/v1/healthz - Authenticated runtime metrics:
GET /api/v1/system/metrics - Object API path keys are full object paths, so nested
/segments act as directory-like prefixes. - Static sites only serve
publicobjects. - Successful JSON responses usually use
{"request_id":"...","data":...}. - Failed JSON responses usually use
{"request_id":"...","error":{"code":"...","message":"..."}}.
Upgrade note: signed object paths now use one opaque token query parameter. Previously issued download links using expires and signature become invalid after this release, so coordinate multi-instance rollout and reissue links. The old public GET /healthz alias and APP_RATE_LIMIT_RPS / APP_RATE_LIMIT_BURST variables have also been removed. Use /livez, /readyz, and APP_RATE_LIMIT_MANAGEMENT_RPS / APP_RATE_LIMIT_MANAGEMENT_BURST. Migration 000012_remove_legacy_schema drops unused upload-session tables and objects.is_deleted; back up any historical session data before upgrading and do not run old and new backend versions together.
For manual API testing, set:
BASE_URL=http://localhost:8080
TOKEN=light-ossThen call authenticated endpoints with:
curl "$BASE_URL/api/v1/buckets" \
-H "Authorization: Bearer $TOKEN"make test
make lint
cd backend && go test ./...
cd backend && go test -race ./...
cd backend && go vet ./...
cd backend && gofmt -l .
cd frontend && npm test
cd frontend && npm run lint
cd frontend && npm run buildRun managed-blob reconciliation and exit (orphans are registered and reported, never deleted automatically):
cd backend && go run ./cmd/server -reconcile-storage-onlyUseful logs in Compose mode:
docker compose logs -f mysql
docker compose logs -f backend
docker compose logs -f frontend
docker compose logs -f gateway- Object content is stored on a local or operator-provided shared filesystem, not an object-storage service.
- Static site hosting only serves public objects.
- Gateway mode currently supports HTTP only.
- The project is a lightweight OSS MVP, not a full S3-compatible implementation.