Multi-cloud S3 browser built with Spring Boot WebFlux, Kotlin, OpenAPI-generated controllers, and a React + MUI frontend.
| Module | Purpose |
|---|---|
app |
Standalone Spring Boot demo application |
autoconfigure |
Auto-configuration: service, REST API, UI static assets |
core |
Shared domain interfaces and types |
starter |
Spring Boot starter — add this one dependency to get everything |
ui |
Vite + React + TypeScript + MUI frontend |
Add the starter to your project and you get the full UI + REST API out of the box:
- UI served at
/s3-viewer/ui/ - API served at
/s3-viewer/api/v1/
All endpoints are under /s3-viewer/api/v1. The OpenAPI spec is at autoconfigure/src/main/resources/openapi/api.yaml and controllers are generated via the OpenAPI Generator Gradle plugin — do not edit generated sources directly.
| Method | Path | Description |
|---|---|---|
GET |
/s3-viewer/api/v1/providers |
List configured S3 providers |
GET |
/s3-viewer/api/v1/providers/{id}/buckets |
List buckets for a provider |
GET |
/s3-viewer/api/v1/providers/{id}/buckets/{name}/browse |
Browse objects at a path (?path=prefix) |
GET |
/s3-viewer/api/v1/providers/{id}/buckets/{name}/download |
Download an object (?key=object/key) |
GET |
/s3-viewer/api/v1/providers/{id}/buckets/{name}/preview/text |
Preview supported text objects (?key=object/key&maxBytes=1048576) |
GET |
/s3-viewer/api/v1/providers/{id}/buckets/{name}/preview/parquet/schema |
Preview parquet schema only (?key=object/key) |
POST |
/s3-viewer/api/v1/providers/{id}/buckets/{name}/folders |
Create a virtual folder (JSON body: {"path": "prefix", "folderName": "name"}) |
POST |
/s3-viewer/api/v1/providers/{id}/buckets/{name}/upload |
Upload a file (multipart/form-data, ?path=prefix) |
DELETE |
/s3-viewer/api/v1/providers/{id}/buckets/{name}/objects |
Delete objects (JSON body: {"keys": [...]}) |
GET |
/s3-viewer/api/v1/providers/{id}/buckets/{name}/search |
Search objects by name (?query=term&path=prefix&maxResults=100) |
S3ViewerServiceinterface has new object operation methods:downloadObject,previewTextObject,previewParquetSchema,uploadObject,deleteObjects,searchObjects. Any custom implementation must implement these.ObjectDownload,TextObjectPreview,ParquetSchemaPreview, andSearchResultare domain types added to thecoremodule.
Provider configuration lives under s3-viewer.providers in application.yaml:
s3-viewer:
read-only-access: false
providers:
- id: my-provider
name: My S3
endpoint: https://s3.amazonaws.com
region: us-east-1
access-key: AKIAIOSFODNN7EXAMPLE
secret-key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
path-style-access: false
buckets:
- my-bucket
- another-bucketUse all-buckets: true to list and allow every bucket visible to the provider credentials instead of maintaining an explicit bucket allow-list:
s3-viewer:
providers:
- id: my-provider
name: My S3
endpoint: https://s3.amazonaws.com
region: us-east-1
access-key: AKIAIOSFODNN7EXAMPLE
secret-key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
path-style-access: false
all-buckets: trueFor LocalStack (dev profile), see app/src/main/resources/application-dev.yaml.
Set s3-viewer.read-only-access=true to reject all write operations. Upload and delete requests return 403, and the bundled UI hides upload, delete, and multi-select controls.
Provider configuration can also be supplied entirely through environment variables. Spring Boot maps indexed list properties like s3-viewer.providers[0].id to env vars using uppercase names and underscores:
export S3_VIEWER_READ_ONLY_ACCESS=true
export S3_VIEWER_PROVIDERS_0_ID=aws
export S3_VIEWER_PROVIDERS_0_NAME="AWS S3"
export S3_VIEWER_PROVIDERS_0_ENDPOINT=https://s3.amazonaws.com
export S3_VIEWER_PROVIDERS_0_REGION=us-east-1
export S3_VIEWER_PROVIDERS_0_ACCESS_KEY="$AWS_ACCESS_KEY_ID"
export S3_VIEWER_PROVIDERS_0_SECRET_KEY="$AWS_SECRET_ACCESS_KEY"
export S3_VIEWER_PROVIDERS_0_PATH_STYLE_ACCESS=false
export S3_VIEWER_PROVIDERS_0_BUCKETS_0=my-bucket
export S3_VIEWER_PROVIDERS_0_BUCKETS_1=another-bucketFor multiple providers, increment the provider index:
export S3_VIEWER_PROVIDERS_1_ID=minio
export S3_VIEWER_PROVIDERS_1_NAME=MinIO
export S3_VIEWER_PROVIDERS_1_ENDPOINT=http://minio:9000
export S3_VIEWER_PROVIDERS_1_REGION=us-east-1
export S3_VIEWER_PROVIDERS_1_ACCESS_KEY=minioadmin
export S3_VIEWER_PROVIDERS_1_SECRET_KEY=minioadmin
export S3_VIEWER_PROVIDERS_1_PATH_STYLE_ACCESS=true
export S3_VIEWER_PROVIDERS_1_BUCKETS_0=demoWhen the application sits behind a reverse proxy that does not strip its path prefix, tell the UI where the API lives:
s3-viewer:
ui:
api-base-path: /prod/service/s3-viewer/api # default: /s3-viewer/apiSpring Boot serves GET /s3-viewer/ui/config.js which injects this value into the browser as window.__S3_VIEWER_CONFIG__.apiBase before the React bundle runs. The UI assets themselves use relative paths in the build output so they work under any prefix without configuration.
The ui module is a React + Vite + MUI single-page application. Features:
- Sidebar with provider/bucket navigation tree
- File browser with list and grid view modes
- Breadcrumb path navigation
- File-type icons (images, video, audio, code, archives, data files, etc.)
- Search — live case-insensitive name search within a bucket/path
- Preview — inline review for
.txtand.jsoncontent, plus parquet schema without reading row data - Create folder — create virtual folders under the current bucket path
- Upload — drag & drop or file picker with per-file progress bars
- Download — direct download button per file
- Delete — multi-select with confirmation dialog
Preview content type is read from S3 object metadata first. Because S3 Content-Type is user-supplied and can be missing or wrong, the backend falls back to JVM content probing and filename extensions for supported preview types.
When s3-viewer.read-only-access=true, write controls are hidden and the backend still rejects upload/delete requests with 403.
In dev mode the Vite server proxies /s3-viewer/api requests to the Spring app on port 8081.
# Build a self-contained JAR (builds UI, copies dist into static/)
./gradlew :app:bootJar
./gradlew release -Prelease.useAutomaticVersion=trueRelease builds publish the standalone app image to GitHub Container Registry:
docker pull ghcr.io/openprojectx/s3-viewer:<version>Run with the default application config:
docker run --rm -p 8081:8081 ghcr.io/openprojectx/s3-viewer:<version>The UI is available at http://localhost:8081/s3-viewer/ui/.
For a local multi-provider playground, use the Compose example:
docker compose -f docker-compose.example.yml upIt starts S3 Viewer from ghcr.io/openprojectx/s3-viewer:latest and configures three local AWS/S3-compatible providers:
| Provider | Image | Host endpoint |
|---|---|---|
| LocalStack | ghcr.io/openprojectx/dockerhub/localstack/localstack:4 |
http://localhost:4566 |
| MiniStack | ministackorg/ministack |
http://localhost:4567 |
| Floci | floci/floci:latest |
http://localhost:4568 |
Inside the Compose network, S3 Viewer talks to each provider on its service DNS name and port 4566.
The example config uses buckets named demo-assets and demo-archive. LocalStack creates them through the existing init script. For MiniStack and Floci, create them with the AWS CLI after startup if you want all three providers immediately browsable:
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 \
aws --endpoint-url=http://localhost:4567 s3 mb s3://demo-assets
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 \
aws --endpoint-url=http://localhost:4567 s3 mb s3://demo-archive
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 \
aws --endpoint-url=http://localhost:4568 s3 mb s3://demo-assets
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 \
aws --endpoint-url=http://localhost:4568 s3 mb s3://demo-archiveFor real S3 providers, mount a Spring Boot config file into /config/application.yaml:
docker run --rm -p 8081:8081 \
-v "$PWD/application.yaml:/config/application.yaml:ro" \
ghcr.io/openprojectx/s3-viewer:<version>Example application.yaml:
s3-viewer:
read-only-access: true
providers:
- id: aws
name: AWS S3
endpoint: https://s3.amazonaws.com
region: us-east-1
access-key: ${AWS_ACCESS_KEY_ID}
secret-key: ${AWS_SECRET_ACCESS_KEY}
path-style-access: false
all-buckets: truePass credentials with environment variables:
docker run --rm -p 8081:8081 \
-e AWS_ACCESS_KEY_ID=... \
-e AWS_SECRET_ACCESS_KEY=... \
-v "$PWD/application.yaml:/config/application.yaml:ro" \
ghcr.io/openprojectx/s3-viewer:<version>You can also configure providers directly with container environment variables and skip the mounted YAML file:
docker run --rm -p 8081:8081 \
-e S3_VIEWER_READ_ONLY_ACCESS=true \
-e S3_VIEWER_PROVIDERS_0_ID=aws \
-e S3_VIEWER_PROVIDERS_0_NAME="AWS S3" \
-e S3_VIEWER_PROVIDERS_0_ENDPOINT=https://s3.amazonaws.com \
-e S3_VIEWER_PROVIDERS_0_REGION=us-east-1 \
-e S3_VIEWER_PROVIDERS_0_ACCESS_KEY=... \
-e S3_VIEWER_PROVIDERS_0_SECRET_KEY=... \
-e S3_VIEWER_PROVIDERS_0_PATH_STYLE_ACCESS=false \
-e S3_VIEWER_PROVIDERS_0_BUCKETS_0=my-bucket \
ghcr.io/openprojectx/s3-viewer:<version>Local image builds use Jib and do not require a Dockerfile:
# Build into the local Docker daemon
./gradlew :app:jibDockerBuild
# Push to a registry
JIB_TO_IMAGE=ghcr.io/openprojectx/s3-viewer ./gradlew :app:jibSee CONTRIBUTING.md for the full dev setup, Dev Container instructions, and how to run the app locally.