AdapterFS turns a mounted directory into a small, self-contained file gateway. Run it beside an existing application container and the same files become available through a browser, WebDAV, SFTP, FTP, and an S3-compatible API.
- A responsive file manager with drag-and-drop uploads, downloads, folders, rename, move, deletion, filtering, and light/dark themes
- Named filesystem exports shared by every interface
- WebDAV Class 1 operations, SFTP, optional FTP, and a SigV4-authenticated S3 API with ranges, copy, presigned URLs, and multipart uploads
- Generated persistent local credentials, optional OIDC browser login, and dedicated S3 access keys
- A non-root Chainguard JRE image, GraalVM native image, Helm chart, and ephemeral-container helper
- Health probes and Prometheus metrics without a database or external web assets
AdapterFS reads and writes ordinary files directly. A change made over SFTP appears in the browser and through S3 on the next request.
mkdir -p files adapterfs-state
docker run --rm \
-p 8080:8080 -p 9000:9000 -p 2222:2222 \
-v "$PWD/files:/data" \
-v "$PWD/adapterfs-state:/var/lib/adapterfs" \
ghcr.io/wenisch-tech/adapterfs:latestOpen http://localhost:8080. The first start creates the web/protocol password and S3 keys in adapterfs-state/credentials.json; the file is mode 0600. Read them with:
docker exec <container> java -jar /app/adapterfs.jar --print-credentials
# Native image: docker exec <container> /app/adapterfs --print-credentials
# or inspect adapterfs-state/credentials.json on the hostFor unattended deployments, set ADAPTERFS_AUTH_USERNAME, ADAPTERFS_AUTH_PASSWORD, ADAPTERFS_AUTH_S3_ACCESS_KEY, and ADAPTERFS_AUTH_S3_SECRET_KEY from secrets.
With the default files export:
# WebDAV
curl -u admin:password -X PROPFIND -H 'Depth: 1' http://localhost:8080/dav/files/
# SFTP
sftp -P 2222 admin@localhost
# S3 (path-style addressing)
AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... \
aws --endpoint-url http://localhost:9000 s3 ls s3://files/
# FTP, after explicitly enabling it
curl -u admin:password ftp://localhost:2121/files/The WebDAV and browser endpoint normally use TLS at the ingress. SFTP encrypts its transport. FTP is disabled by default because plain FTP exposes credentials and content; enable it only on a protected network.
Exports are configured with Spring Boot YAML. Names must be valid lowercase S3 bucket names and become top-level directories in SFTP/FTP, WebDAV paths, and S3 bucket names.
adapterfs:
state-directory: /var/lib/adapterfs
exports:
- name: uploads
path: /data/uploads
read-only: false
- name: archives
path: /data/archives
read-only: trueFor a single export, the default environment variables are convenient:
| Variable | Default | Purpose |
|---|---|---|
ADAPTERFS_EXPORT_NAME |
files |
Export/bucket name |
ADAPTERFS_EXPORT_PATH |
/data |
Mounted directory |
ADAPTERFS_EXPORT_READ_ONLY |
false |
Deny mutations |
ADAPTERFS_STATE_DIRECTORY |
/var/lib/adapterfs |
Credentials, host key, multipart state |
ADAPTERFS_HTTP_PORT |
8080 |
Browser, REST, WebDAV, health |
ADAPTERFS_S3_PORT |
9000 |
S3 endpoint |
ADAPTERFS_SFTP_PORT |
2222 |
SFTP endpoint |
ADAPTERFS_FTP_ENABLED |
false |
Enable FTP explicitly |
ADAPTERFS_FTP_PORT |
2121 |
FTP control port |
ADAPTERFS_FTP_PASSIVE_PORTS |
30000-30009 |
FTP passive data ports |
ADAPTERFS_FTP_TLS_ENABLED |
false |
Enable explicit TLS (AUTH TLS) |
For explicit FTPS, also set ADAPTERFS_FTP_TLS_KEYSTORE to a PKCS12/JKS keystore and provide ADAPTERFS_FTP_TLS_KEYSTORE_PASSWORD through a secret.
Multiple exports can use indexed environment variables such as ADAPTERFS_EXPORTS_0_NAME, ADAPTERFS_EXPORTS_0_PATH, and ADAPTERFS_EXPORTS_0_READ_ONLY, or a mounted YAML file.
AdapterFS rejects .. traversal, symbolic links anywhere inside an exported path, device/special files, and cross-export moves. Uploads are written to a temporary sibling and renamed into place. Existing destinations return a conflict unless the caller explicitly requests overwrite.
A sidecar mounts the same volume in the same pod. This works with ReadWriteOnce and ReadWriteOncePod; it does not require ReadWriteMany.
Use the chart's reusable adapterfs.container named template from a parent chart, or copy its rendered container block into the existing workload. Mount the application volume and AdapterFS state into the sidecar:
containers:
- name: application
volumeMounts:
- {name: shared-data, mountPath: /app/uploads}
- name: adapterfs
image: ghcr.io/wenisch-tech/adapterfs:latest
env:
- {name: ADAPTERFS_EXPORT_NAME, value: uploads}
- {name: ADAPTERFS_EXPORT_PATH, value: /data/uploads}
volumeMounts:
- {name: shared-data, mountPath: /data/uploads}
- {name: adapterfs-state, mountPath: /var/lib/adapterfs}The chart does not inject a sidecar into existing workloads. To create only Services and supporting resources, set deployment.enabled=false and set service.selector to labels on the existing pod.
helm install adapterfs \
oci://ghcr.io/wenisch-tech/helm-charts/adapterfs \
--namespace adapterfs --create-namespaceFor a standalone deployment, mount existing volumes through extraVolumes and extraVolumeMounts, then describe matching exports. Use credentials.existingSecret in production; it must contain username, password, s3-access-key, and s3-secret-key. The generated Secret remains stable across Helm upgrades.
The browser/WebDAV ingress covers port 8080. Expose SFTP, S3, and FTP with Services appropriate to the cluster. FTP additionally needs every configured passive port routed to the pod.
scripts/kubectl-adapterfs appends an ephemeral container and mounts volumes already declared on a running pod. It does not restart or modify application containers and cannot expose their private image layers.
scripts/kubectl-adapterfs my-pod -n my-namespace \
--mount application-data:files \
--mount archive-volume:archive:roThe command checks pods/ephemeralcontainers permission, prints temporary credentials, a kubectl port-forward command, and an authenticated stop command. Sessions expire after one hour unless --duration changes it. A stopped ephemeral-container record remains on the pod until Kubernetes replaces that pod, so use a different --name or replace the pod for another session.
Local login remains available when OIDC is enabled. Configure Spring Security's adapterfs registration and an allowlist:
spring.security.oauth2.client:
registration.adapterfs:
client-id: adapterfs
client-secret: ${OIDC_CLIENT_SECRET}
scope: [openid, profile, email]
provider.adapterfs.issuer-uri: https://id.example.com/realms/main
adapterfs.auth:
oidc-allowed-subjects: ["248289761001"]
oidc-allowed-groups: ["storage-operators"]An authenticated identity is denied unless its sub claim or one of its groups claims is explicitly listed. OIDC applies to the browser; FTP/SFTP/WebDAV use the local account and S3 uses its access keys.
- S3 supports fixed configured buckets, SigV4 header and query authentication, path-style access, ListBuckets/ListObjectsV2, HEAD/GET/ranges, PUT, copy, delete, presigned URLs, and multipart create/upload/complete/abort.
- Bucket creation/deletion, versioning, policies, ACLs, events, object lock, tagging, and virtual-host bucket addressing are outside v1.
- WebDAV supports Class 1 file operations. Locking (
LOCK/UNLOCK) is not implemented. - Symbolic links are deliberately hidden and rejected. AdapterFS does not preserve S3 metadata outside normal file attributes.
- One AdapterFS process should own its state directory. Files may be changed externally, and listings always read current filesystem state.
- SFTP and FTP expose named exports from a state-directory gateway. Their protocol hooks reject mutations to read-only exports; mounting those exports read-only at the container level adds defense in depth.
- Liveness:
/actuator/health/liveness - Readiness:
/actuator/health/readiness - Prometheus:
/actuator/prometheus - A startup failure means an export is invalid, the state directory is unwritable, or an enabled listener cannot bind.
403 SignatureDoesNotMatchusually means the client clock, endpoint, region, access keys, or path-style setting differs from the signed request.- SFTP host keys live under the state directory. Persist that directory to avoid host-key warnings after a restart.
- For FTP behind NAT, set
ADAPTERFS_FTP_EXTERNAL_ADDRESSand publish the complete passive range.
Requires Java 25 and Maven 3.9+:
./mvnw verify
./mvnw spring-boot:run
./mvnw -Pnative native:compileRun the signed S3 exercise against a local instance with scripts/s3-smoke.py. scripts/check-protocols.sh <image> validates browser/API, WebDAV, SFTP, FTP, and S3 against a container. Browser journeys live under tests/e2e; npm test runs them against ADAPTERFS_BASE_URL.
Recreate the README animation from the running application:
npm install
npx playwright install chromium
ADAPTERFS_BASE_URL=http://localhost:8080 ADAPTERFS_USERNAME=admin ADAPTERFS_PASSWORD=... npm run record-demoGitHub Actions test every change, build amd64/arm64 JVM images, run blocking protocol smoke tests and Trivy scans, generate SBOM/provenance artifacts, publish the chart archive to wenisch-tech/helm-charts, and push the OCI chart to oci://ghcr.io/wenisch-tech/helm-charts/adapterfs. OCI metadata links the package back to this source repository. The native profile and Dockerfile remain available for manual builds but are not part of the CI or release workflow. The workflow needs repository contents: write, packages: write, id-token: write, and attestations: write; HELM_CHARTS_TOKEN (or the legacy GITHUBTOKEN) must additionally have Contents: write access to wenisch-tech/helm-charts; organization package creation must permit this repository.
AdapterFS is licensed under the GNU Affero General Public License v3.0.
