Discobucket is a lightweight S3-compatible proxy backed by the local filesystem. Buckets are directories, object keys map to file paths, and object bytes always remain on disk. Metadata is stored either in .s3meta sidecars or in SQL through JetBrains Exposed.
- JDK 25 for JVM builds and local execution
- GraalVM 25 for
nativeCompileandnativeTest - Bun for the bundled React UI build
The Gradle wrapper downloads Gradle automatically.
./gradlew build
./gradlew bootRunDiscobucket listens on port 8080 and stores data under /tmp/discobucket by default. Override the storage root with DISCOBUCKET_ROOT:
DISCOBUCKET_ROOT=/data/my-buckets ./gradlew bootRunThe packaged default allows anonymous S3 requests when no credentials are configured. Point a path-style S3 client at the server:
aws s3 --endpoint-url http://localhost:8080 mb s3://my-bucket
aws s3 --endpoint-url http://localhost:8080 cp file.txt s3://my-bucket/file.txt
aws s3 --endpoint-url http://localhost:8080 ls s3://my-bucketThe bundled bucket browser is available at http://localhost:8080/ui/. It signs requests in the browser with the supplied access key and secret key; credentials are kept in browser session storage.
| Operation | Method | Path |
|---|---|---|
| ListBuckets | GET |
/ |
| CreateBucket | PUT |
/{bucket} |
| HeadBucket | HEAD |
/{bucket} |
| DeleteBucket | DELETE |
/{bucket} |
| GetBucketLocation | GET |
/{bucket}?location |
| ListObjectsV2 | GET |
/{bucket}?list-type=2 |
| ListObjects (v1) | GET |
/{bucket} |
| PutObject | PUT |
/{bucket}/{key} |
| GetObject | GET |
/{bucket}/{key} |
| HeadObject | HEAD |
/{bucket}/{key} |
| DeleteObject | DELETE |
/{bucket}/{key} |
| DeleteObjects | POST |
/{bucket}?delete |
| CopyObject | PUT |
/{bucket}/{key} with x-amz-copy-source |
| CreateMultipartUpload | POST |
/{bucket}/{key}?uploads |
| UploadPart | PUT |
/{bucket}/{key}?partNumber=&uploadId= |
| CompleteMultipartUpload | POST |
/{bucket}/{key}?uploadId= |
| AbortMultipartUpload | DELETE |
/{bucket}/{key}?uploadId= |
| ListMultipartUploads | GET |
/{bucket}?uploads |
| ListParts | GET |
/{bucket}/{key}?uploadId= |
Bucket-level mappings accept both /{bucket} and /{bucket}/. Object operations use path-style addressing only.
/tmp/discobucket/
my-bucket/
photos/
cat.jpg
cat.jpg.s3meta
readme.txt
readme.txt.s3meta
.multipart/
{uploadId}/
upload.json
part.00001
part.00001.meta.json
- Bucket creation dates come from the bucket directory modification time.
- Object ETags are quoted MD5 digests. Completed multipart objects use a quoted whole-object MD5 followed by the part count.
.s3metafiles are used only by the filesystem metadata backend and are hidden from listings.- Multipart bytes always stage under
.multipart/. Filesystem metadata mode also writesupload.jsonand part metadata sidecars there. - Keys containing
..and reserved metadata names are rejected.
| Property | Environment variable | Packaged default | Description |
|---|---|---|---|
discobucket.root-path |
DISCOBUCKET_ROOT |
/tmp/discobucket |
Base directory for buckets and multipart staging |
discobucket.allow-anonymous |
DISCOBUCKET_ALLOW_ANONYMOUS |
true |
Allow requests when no credentials are configured |
discobucket.metadata.backend |
DISCOBUCKET_METADATA_BACKEND |
filesystem |
filesystem or database |
discobucket.credentials[N].access-key |
- | - | SigV4 access key ID |
discobucket.credentials[N].secret-key |
- | - | SigV4 secret key |
discobucket.credentials[N].buckets.<name> |
- | - | Per-bucket permission |
discobucket.credentials[N].default-permission |
- | - | Permission for buckets not explicitly listed |
Virtual threads are enabled through spring.threads.virtual.enabled=true. Actuator exposes only /actuator/health and /actuator/info; health details use Spring Boot's when-authorized policy.
Discobucket supports SigV4 authorization headers, presigned URLs, and AWS streaming chunk signatures.
- With one or more configured credentials, S3 requests must have a valid SigV4 header or presigned query parameters.
- With no configured credentials, requests are accepted only when
discobucket.allow-anonymous=true. /ui/**is excluded from the S3 authentication and bucket-authorization filters so the UI shell can load. Its API calls are still signed by the browser client.- Header-signed requests must be within 15 minutes of server time. Presigned URLs are checked for start time, expiration, credential date, and signature.
Example credentials:
discobucket:
allow-anonymous: false
credentials:
- access-key: ADMIN_KEY
secret-key: ADMIN_SECRET
- access-key: READER_KEY
secret-key: READER_SECRET
buckets:
shared-data: READ_ONLY
default-permission: READ_ONLY| Permission | Access |
|---|---|
READ_ONLY |
GET and HEAD |
READ_WRITE |
Object reads and writes; bucket creation and deletion remain denied |
FULL_ACCESS |
All operations, including bucket creation and deletion |
A credential with no bucket map and no default permission has unrestricted access. Restricted credentials see only accessible buckets in ListBuckets; copy operations also require access to the source bucket.
Enable database storage for both object and multipart metadata:
discobucket:
metadata:
backend: database
spring:
datasource:
url: jdbc:postgresql://localhost:5432/discobucket
username: discobucket
password: secretPostgreSQL and SQLite JDBC drivers are bundled. H2 is a test dependency. The object_metadata, multipart_upload, and multipart_part tables are created on startup. Object bodies and multipart part bytes remain on disk in database mode. See docs/multipart.md for the multipart design and limitations.
The React UI under /ui/ connects to one bucket at a time and supports:
- prefix and folder navigation
- object listing and client-side filtering
- uploads and downloads
- empty folder-marker creation
- object deletion
- paginated loading through
ListObjectsV2
The access key, secret key, bucket, and region are stored in sessionStorage, not persistent browser storage. The UI requires credentials even when anonymous server access is enabled.
Discobucket intentionally does not implement full S3 semantics. Unsupported areas include virtual-hosted addressing, bucket policies and ACLs, versioning, lifecycle rules, encryption, tagging, retention, object lock, replication, notifications, and website hosting. Custom x-amz-meta-* request headers are not currently captured by PutObject.
Multipart listing pagination and several AWS multipart validation rules are also not implemented; see docs/multipart.md.
./gradlew build # frontend, JVM build, and tests
./gradlew test # all JVM tests
./gradlew bootRun # local server
./gradlew nativeCompile # GraalVM native executable
./gradlew nativeTest # native tests
./gradlew bootBuildImage # OCI image; requires Docker
./gradlew docs # Dokka output in build/dokka/html/The suite currently contains 93 JUnit tests. It covers MockMvc, the AWS SDK v2, MinIO, simple-s3-client, SigV4 and presigned URLs, bucket authorization, protocol edge cases, both metadata backends, and multipart listing.
CI runs ./gradlew test for pull requests to main. Pushes to main publish latest and commit-SHA container tags to GHCR. Tags matching v* build release artifacts and versioned container images.
- Kotlin 2.3.10 and Java 25
- Spring Boot 4.0.3 with Spring WebMVC and virtual threads
- Jackson 3.x
- JetBrains Exposed 1.2.0
- SpringDoc OpenAPI 3.0.2 at
/swagger-ui.html - React, TypeScript, Vite, and Bun for the bundled UI
- GraalVM Native Image support
Copyright 2025 MadVentures. Licensed under the Apache License 2.0. See LICENSE.
