A lightweight, open-source file storage server written in Go, inspired by Amazon S3 concepts.
348

A lightweight, open-source file storage server written in Go, inspired by Amazon S3 concepts.
Store, retrieve, delete, and list files via a simple HTTP API — no database required, filesystem only.
data: URI)application/octet-stream)ALLOW_DELETE)../ attacks# Create storage directory
mkdir -p storage
# Create .env file
echo "STORAGE_API_KEY=your-secret-key-here" > .env
# Run container
docker run -d \
--name go_bucket \
-p 8080:8080 \
-v $(pwd)/storage:/data \
--env-file .env \
teguh02/go_bucket:latest
# Check health
curl http://localhost:8080/health
# Clone and enter directory
cd go_bucket
# Copy environment file
cp .env.example .env
# Edit .env and set your API key
# STORAGE_API_KEY=your-secret-key-here
# Start
docker compose up -d
# Check health
curl http://localhost:8080/health
# docker-compose.yml
services:
go_bucket:
image: teguh02/go_bucket:latest
container_name: go_bucket
ports:
- "8080:8080"
volumes:
- ./storage:/data
environment:
- STORAGE_API_KEY=your-secret-key
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
# docker-compose.prod.yml
services:
go_bucket:
image: teguh02/go_bucket:latest
container_name: go_bucket
ports:
- "8080:8080"
volumes:
- ./storage:/data
environment:
- STORAGE_API_KEY=${STORAGE_API_KEY}
- PORT=8080
- MAX_UPLOAD_MB=100
- ALLOW_OVERWRITE=false
- ALLOW_DELETE=true
- CORS_ALLOWED_ORIGINS=https://yourdomain.com
- PUBLIC_BASE_URL=https://cdn.yourdomain.com
- CACHE_MAX_AGE=86400
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
# docker-compose.dev.yml
services:
go_bucket:
image: teguh02/go_bucket:latest
container_name: go_bucket_dev
ports:
- "8080:8080"
volumes:
- ./storage:/data
environment:
- STORAGE_API_KEY=dev-key
- PORT=8080
- MAX_UPLOAD_MB=500
- ALLOW_OVERWRITE=true
- ALLOW_DELETE=true
- CORS_ALLOWED_ORIGINS=*
- PUBLIC_BASE_URL=http://localhost:8080
restart: unless-stopped
# docker-compose.proxy.yml
services:
nginx:
image: nginx:latest
container_name: nginx_proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./storage:/data
depends_on:
- go_bucket
restart: unless-stopped
go_bucket:
image: teguh02/go_bucket:latest
container_name: go_bucket
volumes:
- ./storage:/data
environment:
- STORAGE_API_KEY=${STORAGE_API_KEY}
- PORT=8080
- PUBLIC_BASE_URL=https://cdn.yourdomain.com
restart: unless-stopped
# nginx.conf
upstream go_bucket {
server go_bucket:8080;
}
server {
listen 80;
server_name cdn.yourdomain.com;
location / {
proxy_pass http://go_bucket;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /files/ {
proxy_pass http://go_bucket;
proxy_set_header Host $host;
add_header Cache-Control "public, max-age=31536000";
}
}
Run with:
docker compose -f docker-compose.proxy.yml up -d
GET /health
Response:
{"ok": true, "time": "2024-01-15T10:30:00Z"}
POST /api/upload
Headers:
X-API-Key: your-api-key OR Authorization: Bearer your-api-keyForm Data:
file (required): The file to upload — can be a file attachment, a URL string, or a base64-encoded stringpath (optional): Destination path, e.g., avatars/user1.jpgThe endpoint automatically detects the upload type:
curl -X POST "http://localhost:8080/api/upload" \
-H "X-API-Key: your-api-key" \
-F "file=@./photo.jpg" \
-F "path=avatars/user1.jpg"
curl -X POST "http://localhost:8080/api/upload" \
-H "X-API-Key: your-api-key" \
-F "file=https://placehold.co/600x400.png" \
-F "path=images/placeholder.png"
# Plain base64
curl -X POST "http://localhost:8080/api/upload" \
-H "X-API-Key: your-api-key" \
-F "file=SGVsbG8gV29ybGQ=" \
-F "path=files/hello.txt"
# Data URI
curl -X POST "http://localhost:8080/api/upload" \
-H "X-API-Key: your-api-key" \
-F "file=data:text/plain;base64,SGVsbG8gV29ybGQ=" \
-F "path=files/hello.txt"
The destination path is supplied via the ?path= query parameter or the X-File-Path header:
curl -X POST "http://localhost:8080/api/upload?path=images/photo.jpg" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: image/jpeg" \
--data-binary @./photo.jpg
JavaScript (axios):
const blob = new Blob([fileData], { type: 'image/jpeg' });
await axios.post('http://localhost:8080/api/upload?path=images/photo.jpg', blob, {
headers: { 'X-API-Key': 'your-api-key', 'Content-Type': 'image/jpeg' }
});
# URL via JSON
curl -X POST "http://localhost:8080/api/upload" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"file":"https://example.com/doc.pdf","path":"docs/doc.pdf"}'
# Base64 via JSON
curl -X POST "http://localhost:8080/api/upload" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"file":"SGVsbG8gV29ybGQ=","path":"files/hello.txt"}'
Response:
{
"ok": true,
"path": "avatars/user1.jpg",
"url": "http://localhost:8080/files/avatars/user1.jpg",
"size": 12345,
"content_type": "image/jpeg",
"hash": {
"md5": "d8e8fca2dc0f896fd7cb4cb0031ba249",
"sha1": "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83"
}
}
GET /files/{path}
Example:
curl http://localhost:8080/files/avatars/user1.jpg
Direct access in browser:
http://localhost:8080/files/avatars/user1.jpg
DELETE /api/files/{path}
Deletion can be disabled globally via the
ALLOW_DELETE=falseenvironment variable.
Example:
curl -X DELETE "http://localhost:8080/api/files/avatars/user1.jpg" \
-H "X-API-Key: your-api-key"
Response:
{"ok": true, "deleted": "avatars/user1.jpg"}
GET /api/list?prefix={optional-prefix}&page={page}&per_page={per_page}
Query Parameters:
| Parameter | Default | Description |
|---|---|---|
prefix | - | Filter files by folder/prefix |
page | 1 | Page number |
per_page | 10 | Results per page |
Example:
# List all files (page 1, 10 per page)
curl "http://localhost:8080/api/list" \
-H "X-API-Key: your-api-key"
# Page 2 with 20 per page
curl "http://localhost:8080/api/list?page=2&per_page=20" \
-H "X-API-Key: your-api-key"
# List files in avatars folder
curl "http://localhost:8080/api/list?prefix=avatars" \
-H "X-API-Key: your-api-key"
Response:
{
"ok": true,
"files": [
{
"path": "avatars/user1.jpg",
"size": 12345,
"modified": "2024-01-15T10:30:00Z",
"hash": {
"md5": "d8e8fca2dc0f896fd7cb4cb0031ba249",
"sha1": "4e1243bd22c66e76c2ba9eddc1f91394e57f9f83"
}
}
],
"count": 1,
"page": 1,
"per_page": 10,
"total": 1,
"total_pages": 1
}
| Variable | Required | Default | Description |
|---|---|---|---|
STORAGE_API_KEY | Yes | - | API key for upload/delete/list operations |
PORT | No | 8080 | Server port |
STORAGE_DIR | No | /data | Storage directory (container path) |
PUBLIC_BASE_URL | No | auto | Base URL for generated file URLs |
MAX_UPLOAD_MB | No | 50 | Maximum upload size in MB |
ALLOW_OVERWRITE | No | false | Allow overwriting existing files |
ALLOW_DELETE | No | true | Allow deleting files via API |
CORS_ALLOWED_ORIGINS | No | * | CORS origins (comma-separated or *) |
CACHE_MAX_AGE | No | 31536000 | Cache-Control max-age in seconds |
go_bucket/
├── .github/
│ └── workflows/
│ └── test.yml # GitHub Actions CI
├── cmd/
│ └── server/
│ └── main.go # Application entry point
├── internal/
│ ├── config/
│ │ └── config.go # Configuration loading
│ └── http/
│ ├── handlers.go # HTTP handlers
│ ├── handlers_test.go # Unit & integration tests
│ └── middleware.go # Auth, CORS, logging middleware
├── storage/ # File storage (mounted volume)
├── .env.example # Example environment file
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── go.mod
└── README.md
.., absolute paths, null bytesALLOW_DELETE=false)Important: Never expose the API key in client-side code. Use server-side API routes or server actions.
// app/actions/upload.ts
'use server'
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File
const path = formData.get('path') as string
const uploadForm = new FormData()
uploadForm.append('file', file)
uploadForm.append('path', path)
const response = await fetch(`${process.env.CDN_URL}/api/upload`, {
method: 'POST',
headers: {
'X-API-Key': process.env.CDN_API_KEY!,
},
body: uploadForm,
})
return response.json()
}
// app/api/cdn/delete/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function DELETE(request: NextRequest) {
const { path } = await request.json()
const response = await fetch(`${process.env.CDN_URL}/api/files/${path}`, {
method: 'DELETE',
headers: {
'X-API-Key': process.env.CDN_API_KEY!,
},
})
return NextResponse.json(await response.json())
}
# Set environment variables
export STORAGE_API_KEY=dev-key
export STORAGE_DIR=./storage
export PORT=8080
# Run
go run ./cmd/server
go build -o cdn-server ./cmd/server
./cdn-server
go test ./... -v -race
# Build
docker build -t teguh02/go_bucket:latest .
# Push to Docker Hub
docker push teguh02/go_bucket:latest
MIT
Content type
Image
Digest
sha256:8f9bcc9ae…
Size
5.9 MB
Last updated
5 months ago
docker pull teguh02/go_bucket