Skip to content

Commit 2272a6a

Browse files
pierre-bclaude
andcommitted
Add public file access feature with prefix-based visibility
- Add S3_PUBLIC_PREFIX env var (default: public/) for unauthenticated GET/HEAD - Add S3_PUBLIC_CACHE_MAX_AGE env var for Cache-Control headers - Add ?download=1 query param to force file download - Auto-create public directory on server startup - Add comprehensive integration tests for public/private visibility - Fix all golangci-lint errcheck warnings - Move integration tests to go.yml workflow, simplify docker.yml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent fe19de9 commit 2272a6a

File tree

10 files changed

+1343
-206
lines changed

10 files changed

+1343
-206
lines changed

.github/workflows/docker.yml

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,19 @@
1-
name: Docker Build, Test and Push
1+
name: Docker Build and Push
22

33
on:
44
push:
5-
branches: [main]
65
tags:
76
- "v*.*"
87
- "latest"
9-
pull_request:
10-
branches: [main]
118

129
env:
1310
REGISTRY: docker.io
1411
IMAGE_NAME: notifuse/selfhost_s3
1512

1613
jobs:
17-
integration-tests:
18-
name: Integration Tests
19-
runs-on: ubuntu-latest
20-
21-
steps:
22-
- name: Checkout code
23-
uses: actions/checkout@v4
24-
25-
- name: Set up Go
26-
uses: actions/setup-go@v5
27-
with:
28-
go-version: "1.23"
29-
cache: true
30-
31-
- name: Set up Docker Buildx
32-
uses: docker/setup-buildx-action@v3
33-
34-
- name: Build Docker image for testing
35-
uses: docker/build-push-action@v5
36-
with:
37-
context: .
38-
file: ./Dockerfile
39-
push: false
40-
load: true
41-
tags: selfhost_s3:test
42-
cache-from: type=gha
43-
cache-to: type=gha,mode=max
44-
45-
- name: Start selfhost_s3 container
46-
run: |
47-
docker run -d \
48-
--name selfhost_s3-test \
49-
-p 9000:9000 \
50-
-e S3_BUCKET=test-bucket \
51-
-e S3_ACCESS_KEY=testkey \
52-
-e S3_SECRET_KEY=testsecret \
53-
-e S3_PORT=9000 \
54-
-e S3_REGION=us-east-1 \
55-
selfhost_s3:test
56-
57-
- name: Wait for selfhost_s3 to be ready
58-
run: |
59-
echo "Waiting for selfhost_s3 to be ready..."
60-
for i in {1..30}; do
61-
if curl -f http://localhost:9000/health > /dev/null 2>&1; then
62-
echo "selfhost_s3 is ready!"
63-
break
64-
fi
65-
echo "Waiting for selfhost_s3... ($i/30)"
66-
sleep 2
67-
done
68-
# Final check
69-
curl -f http://localhost:9000/health || exit 1
70-
71-
- name: Run integration tests
72-
env:
73-
INTEGRATION_TEST_ENDPOINT: http://localhost:9000
74-
INTEGRATION_TEST_BUCKET: test-bucket
75-
INTEGRATION_TEST_ACCESS_KEY: testkey
76-
INTEGRATION_TEST_SECRET_KEY: testsecret
77-
INTEGRATION_TEST_REGION: us-east-1
78-
run: |
79-
go test -race -timeout=5m ./integration/... -v
80-
81-
- name: Show container logs on failure
82-
if: failure()
83-
run: docker logs selfhost_s3-test
84-
85-
- name: Cleanup
86-
if: always()
87-
run: docker rm -f selfhost_s3-test || true
88-
8914
build-and-push:
9015
name: Build and Push
9116
runs-on: ubuntu-latest
92-
needs: integration-tests
9317
# Only push to Docker Hub on tag pushes (v*.* or latest)
9418
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
9519
permissions:

.github/workflows/go.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,69 @@ jobs:
3636
files: coverage.txt
3737
fail_ci_if_error: false
3838
verbose: true
39+
40+
integration-tests:
41+
name: Integration Tests
42+
runs-on: ubuntu-latest
43+
44+
steps:
45+
- name: Checkout code
46+
uses: actions/checkout@v4
47+
48+
- name: Set up Go
49+
uses: actions/setup-go@v5
50+
with:
51+
go-version: "1.23"
52+
cache: true
53+
54+
- name: Set up Docker Buildx
55+
uses: docker/setup-buildx-action@v3
56+
57+
- name: Build Docker image for testing
58+
uses: docker/build-push-action@v5
59+
with:
60+
context: .
61+
file: ./Dockerfile
62+
push: false
63+
load: true
64+
tags: selfhost_s3:test
65+
cache-from: type=gha
66+
cache-to: type=gha,mode=max
67+
68+
- name: Start selfhost_s3 container
69+
run: |
70+
docker run -d \
71+
--name selfhost_s3-test \
72+
-p 9000:9000 \
73+
-e S3_BUCKET=test-bucket \
74+
-e S3_ACCESS_KEY=testkey \
75+
-e S3_SECRET_KEY=testsecret \
76+
-e S3_PORT=9000 \
77+
-e S3_REGION=us-east-1 \
78+
selfhost_s3:test
79+
80+
- name: Wait for selfhost_s3 to be ready
81+
run: |
82+
echo "Waiting for selfhost_s3 to be ready..."
83+
for i in {1..30}; do
84+
if curl -f http://localhost:9000/health > /dev/null 2>&1; then
85+
echo "selfhost_s3 is ready!"
86+
break
87+
fi
88+
echo "Waiting for selfhost_s3... ($i/30)"
89+
sleep 2
90+
done
91+
# Final check
92+
curl -f http://localhost:9000/health || exit 1
93+
94+
- name: Run integration tests
95+
run: |
96+
go test -race -timeout=5m ./integration/... -v
97+
98+
- name: Show container logs on failure
99+
if: failure()
100+
run: docker logs selfhost_s3-test
101+
102+
- name: Cleanup
103+
if: always()
104+
run: docker rm -f selfhost_s3-test || true

README.md

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ A minimal S3-compatible object storage server written in Go that persists files
1212

1313
- S3-compatible API (AWS Signature V4 authentication)
1414
- Local filesystem storage
15+
- Public file access (optional prefix-based)
16+
- Download mode with `?download=1` query parameter
17+
- Configurable cache headers for public files
1518
- Single binary, no dependencies
1619
- Multi-platform Docker images (amd64, arm64)
1720

@@ -91,16 +94,18 @@ export S3_SECRET_KEY=mysecretkey
9194

9295
selfhost_s3 is configured via environment variables:
9396

94-
| Variable | Required | Default | Description |
95-
| ------------------ | -------- | ----------- | -------------------------------------- |
96-
| `S3_BUCKET` | Yes | - | S3 bucket name |
97-
| `S3_ACCESS_KEY` | Yes | - | Access key for authentication |
98-
| `S3_SECRET_KEY` | Yes | - | Secret key for authentication |
99-
| `S3_PORT` | No | `9000` | Port to listen on |
100-
| `S3_STORAGE_PATH` | No | `./data` | Local directory for file storage |
101-
| `S3_REGION` | No | `us-east-1` | AWS region (for signature validation) |
102-
| `S3_CORS_ORIGINS` | No | `*` | Allowed CORS origins (comma-separated) |
103-
| `S3_MAX_FILE_SIZE` | No | `100MB` | Maximum upload file size |
97+
| Variable | Required | Default | Description |
98+
| ------------------------ | -------- | ------------ | ------------------------------------------------ |
99+
| `S3_BUCKET` | Yes | - | S3 bucket name |
100+
| `S3_ACCESS_KEY` | Yes | - | Access key for authentication |
101+
| `S3_SECRET_KEY` | Yes | - | Secret key for authentication |
102+
| `S3_PORT` | No | `9000` | Port to listen on |
103+
| `S3_STORAGE_PATH` | No | `./data` | Local directory for file storage |
104+
| `S3_REGION` | No | `us-east-1` | AWS region (for signature validation) |
105+
| `S3_CORS_ORIGINS` | No | `*` | Allowed CORS origins (comma-separated) |
106+
| `S3_MAX_FILE_SIZE` | No | `100MB` | Maximum upload file size |
107+
| `S3_PUBLIC_PREFIX` | No | `public/` | Prefix for public files (empty string disables) |
108+
| `S3_PUBLIC_CACHE_MAX_AGE`| No | `31536000` | Cache-Control max-age for public files (seconds) |
104109

105110
## Docker Hub
106111

@@ -140,6 +145,67 @@ data/
140145
- **Files**: Stored at `{storage_path}/{bucket}/{key}`
141146
- **Folders**: Represented as empty files with keys ending in `/`
142147

148+
## Public Access
149+
150+
selfhost_s3 supports serving files publicly without authentication. By default, files under the `public/` prefix are accessible via GET and HEAD requests without AWS Signature V4 authentication.
151+
152+
### How It Works
153+
154+
- The `public/` directory is automatically created on server startup
155+
- GET and HEAD requests to paths starting with the public prefix skip authentication
156+
- PUT and DELETE operations still require authentication (only uploads/deletes are protected)
157+
- Public files include `Cache-Control` headers for CDN/browser caching
158+
159+
### Public File URLs
160+
161+
Files in the public prefix can be accessed directly via browser:
162+
163+
```
164+
http://localhost:9000/my-bucket/public/images/logo.png
165+
http://localhost:9000/my-bucket/public/documents/report.pdf
166+
```
167+
168+
### Download Mode
169+
170+
Add `?download=1` to force the browser to download the file instead of displaying it:
171+
172+
```
173+
http://localhost:9000/my-bucket/public/report.pdf?download=1
174+
```
175+
176+
This sets the `Content-Disposition: attachment` header with the filename.
177+
178+
### Configuration Examples
179+
180+
**Custom public prefix:**
181+
```bash
182+
S3_PUBLIC_PREFIX=assets/ # Files under assets/ are public
183+
```
184+
185+
**Disable public access entirely:**
186+
```bash
187+
S3_PUBLIC_PREFIX= # Empty string disables public access
188+
```
189+
190+
**Custom cache duration (1 hour):**
191+
```bash
192+
S3_PUBLIC_CACHE_MAX_AGE=3600
193+
```
194+
195+
**Disable caching:**
196+
```bash
197+
S3_PUBLIC_CACHE_MAX_AGE=0
198+
```
199+
200+
### Security Recommendations
201+
202+
When exposing files publicly:
203+
204+
1. **Use a reverse proxy** (nginx, Caddy, Traefik) in front of selfhost_s3 for SSL/TLS
205+
2. **Restrict CORS origins** in production: `S3_CORS_ORIGINS=https://yourdomain.com`
206+
3. **Consider a CDN** for frequently accessed public files
207+
4. **Validate uploads** in your application before storing files in the public prefix
208+
143209
## CORS
144210

145211
Since browser clients connect directly to selfhost_s3, CORS headers are automatically included:

0 commit comments

Comments
 (0)