Build, test, project structure
Thanks for your interest in contributing! Whether it's a bug report, feature idea, or code change, we appreciate your help.
- Rust stable — install via rustup (the repo's
rust-toolchain.tomlpins the channel automatically) - Node.js 20+ — needed to build the embedded demo UI
- Docker — optional, used for running MinIO in integration tests
# 1. Clone the repo
git clone https://github.com/beshu-tech/deltaglider_proxy.git
cd deltaglider_proxy
# 2. Build the demo UI (rust-embed bakes it into the binary)
cd demo/s3-browser/ui && npm install && npm run build && cd -
# 3. Build the proxy
cargo build
# 4. Run it
DGP_DATA_DIR=./data cargo runThe S3 API and demo UI both start on http://localhost:9000. The UI is available at http://localhost:9000/_/.
# Unit tests (no MinIO)
cargo test --lib --locked
# One integration binary (many need MinIO on localhost:9000 — see tests/common/mod.rs)
cargo test --locked --test s3_integration_test
# Full matrix (run before a release or after changing shared test harness / CI lists)
cargo test --all --lockedPR CI does not run cargo test --all (wall-clock); it runs cargo test --lib plus explicit integration binaries listed in .github/workflows/ci.yml. Every tests/<name>.rs must appear there — ./scripts/check-integration-tests-in-ci.sh enforces it. A nightly workflow (test-all-nightly.yml) runs cargo test --all with MinIO.
The merge gate matches .github/workflows/ci.yml — run these before submitting a PR:
cargo fmt --all -- --check
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo test --lib --locked
./scripts/check-integration-tests-in-ci.sh
cd demo/s3-browser/ui && npm ci && npm run build && npm run lint && npm run typecheck && npm run knip \
&& npm run test:permissions && npm run test:storage-path
# Optional local parity with CI integration batches (needs MinIO):
cargo test --locked --test s3_integration_testEmbedded UI smoke (Playwright — same as e2e-smoke CI job): from repo root, cargo build --release --bin deltaglider_proxy with UI already built, then cd demo/s3-browser/ui && npx playwright install chromium && cd ../../../.. && ./scripts/e2e-smoke.sh.
The S3 protocol surface (GET/PUT/HEAD/DELETE, ListObjectsV2, copy, multipart) is
served by the s3s framework — s3_adapter_s3s.rs implements s3s::S3 (~32
verb methods) and is
mounted as the axum fallback_service in startup.rs. The hand-rolled axum S3
handlers were retired; what survives in api/handlers/ is just shared state +
the helpers s3s can't model (browser form-POST, health/stats). The admin API and
demo UI stay axum, under /_/.
src/
├── s3_adapter_s3s.rs # THE S3 implementation: impls s3s::S3 (~32 verb methods); delegates product logic to the engine
├── startup.rs # Server startup: builds the s3s service + axum router, middleware stack (admission → auth → interceptors → fallback)
├── api/
│ ├── mod.rs # API module root, re-exports S3Error
│ ├── handlers/ # Post-s3s survivors: mod.rs (AppState + helpers), form_post.rs (browser PostObject), object_helpers.rs (quota gate + outbox enqueue), status.rs (/_/health, /_/stats)
│ ├── auth.rs # SigV4 + IAM authorization middleware + public prefix bypass
│ ├── admin/ # Admin API (login, config, users, groups, external_auth, backup, replication, lifecycle, event_outbox, scanner, audit, …)
│ ├── aws_chunked.rs # AWS chunked transfer encoding decoder
│ └── errors.rs # S3 error responses
├── deltaglider/
│ ├── engine/ # Core engine (mod, store, retrieve submodules)
│ ├── codec.rs # xdelta3 encode/decode (subprocess)
│ ├── cache.rs # Reference file LRU cache (moka)
│ ├── savings.rs # Delta-savings accounting
│ └── file_router.rs # File type routing (delta-eligible vs passthrough)
├── storage/
│ ├── traits.rs # StorageBackend trait (async_trait, object-safe)
│ ├── filesystem.rs # Local filesystem backend (xattr metadata)
│ ├── s3.rs # S3 backend (AWS SDK; classify_s3_error / classify_get_error pure fns)
│ ├── encrypting.rs # At-rest encryption wrapper backend (per-backend AES key)
│ ├── routing.rs # Multi-backend routing (virtual bucket → real backend)
│ └── xattr_meta.rs # Extended attribute helpers
├── iam/
│ ├── mod.rs # IamState enum, IamIndex, hot-swap, IAM_VERSION counter
│ ├── types.rs # IamUser, Permission, AuthenticatedUser, S3Action
│ ├── permissions.rs # ABAC evaluation (legacy + iam-rs with conditions)
│ ├── middleware.rs # Per-request IAM authorization middleware
│ ├── keygen.rs # Secure access key generation
│ ├── declarative.rs # Declarative-mode reconciler (diff_iam → apply_iam_reconcile)
│ └── external_auth/ # OAuth/OIDC providers (Google, generic OIDC)
├── config_db/ # Encrypted SQLCipher DB (users, groups, auth_providers, declarative; classify_sqlite_error)
├── config.rs # Flat Config struct + ENV_VAR_REGISTRY + env_parse/env_bool helpers + classify_auth_config (YAML is the only config format)
├── config_sections.rs # Sectioned YAML wire shape (admission/access/storage/advanced) + shorthand expanders
├── config_db_sync.rs # Multi-instance IAM sync via S3 (reopen_and_rebuild_iam)
├── admission/ # Pre-auth admission chain (operator-authored + synthesized blocks)
├── replication/ # Event-driven + scheduled bucket replication (planner, worker, scheduler, event_consumer, state_store)
├── lifecycle/ # Delete-only object lifecycle rules (planner, scheduler, worker, state_store)
├── transfer.rs # Shared engine-routed copy primitive (used by replication + lifecycle)
├── event_outbox.rs # Durable object-event outbox (append on mutation)
├── event_delivery.rs # Background dispatcher → webhook JSON or Slack message
├── slack_format.rs # Pure Slack Block Kit formatter + notification filter
├── security.rs # Pure security primitives (validate_bucket_name, bucket_name_is_ip_like, outbound-URL SSRF policy)
├── secret.rs # Secret trait (opaque material + non-secret id) — future KMS/Vault home
├── tls.rs # TLS setup (user PEM or self-signed via rcgen)
├── background.rs # Shared background-runner helpers (parse_duration_or)
├── init.rs # Interactive --init config wizard
├── bucket_policy.rs # Per-bucket policies + PublicPrefixSnapshot
├── session.rs # Admin session store (OsRng tokens, IP binding)
├── rate_limiter.rs # Per-IP rate limiting (token bucket)
├── metadata_cache.rs # Object metadata LRU cache (moka)
├── usage_scanner.rs # Background prefix size scanner (cached)
├── audit.rs # Audit logging + in-memory AuditEntry ring
├── multipart.rs # In-memory multipart upload state
├── types.rs # Core types (FileMetadata, StorageInfo, etc.)
├── demo.rs # Embedded UI (rust-embed) + admin API router
├── cli/ # CLI subcommands (cp, sync, migrate, ls, rm, purge, verify, config, …)
├── lib.rs # Library root
└── main.rs # Entry point
demo/s3-browser/ui/ # React 18 + TypeScript + Ant Design 6 admin GUI
tests/ # Integration tests (S3 ops, auth, IAM, public prefixes)
docs/ # Documentation
- DeltaSpace: A group of objects under the same directory prefix that share a single baseline for delta compression. For example, all objects under
releases/form one deltaspace. - Reference file: The internal baseline stored once per deltaspace. All deltas are computed against it (no chaining), so reconstruction is always O(1).
- StorageBackend: A trait abstracting where bytes live — local filesystem or upstream S3. Adding a new backend means implementing this trait.
- File router: Decides whether a file is delta-eligible based on its extension (
.zip,.jar,.tar, etc.) or should be stored as passthrough (.jpg,.mp4, etc.).
The admin GUI (demo/s3-browser/ui) has converged on a small set of canonical
patterns. New panels and edits should follow these rather than re-inventing —
divergence here has historically produced a recurring "admin-editor bug class"
(stale closures, array-index keys, double sources of truth).
Two data families, two pipelines.
- Config sections (
admission/access/storage/advanced) → theuseSectionEditorhook. It owns fetch → dirty-tracking → validate →ApplyDialog→ section-PUT → re-fetch, withpick(wire→form) /toPayload(form→wire) hooks. The editorvalueIS the single source of truth — never keep a parallel state mirror of it. Examples: Admission, Credentials, all Advanced sub-panels, Webhook delivery, Replication, Lifecycle, Buckets. - IAM DB resources (users / groups / OAuth providers / mapping rules) →
react-query (
queries/, keyed byqk.*) for reads + per-record mutations. Read the cached admin config withuseAdminConfig()— do NOT hand-rolluseEffect(getAdminConfig().then(setState)). After a config mutation, invalidateqk.config()(the section editor already does this on apply).
Single source of truth for forms. A form's React state should be the only
copy of its editable data. For master-detail forms (User/Group/etc.), initialize
state from the selected record with lazy useState(() => ...) initializers +
a key on the form (key={record.id} for edit, key="new" for create) so a
keyed remount resets state — never a useEffect([record]) prop→state mirror.
Stable row ids, never array index. Row-list editors (endpoints, headers,
glob rows, routes, permissions, rules) key React lists by a stable id (a
per-instance nextId() counter or the record's own id), and mutate rows by id
(rows.map(r => r.id === id ? {...} : r)), never by index.
Pure helpers + Node regression tests. Validation, payload-building, and
normalization live in pure functions (e.g. webhookDeliveryPayload.ts's
formFromWire / buildPayloadFromForm), not inline in components, and get a
scripts/*-regression-test.mjs Node test (registered in package.json + CI).
Mirrors the Rust convention of pure decision-fns at seams (classify_*,
validate_*, resolve_*) with colocated unit tests.
Secret round-trip. Secret fields (webhook headers, Slack bot token, SigV4)
are masked to the REDACTED_SENTINEL (__redacted__) on GET; the UI shows a
"•••• (unchanged — type to replace)" placeholder and, on save, passes the
sentinel through untouched so the backend preserves the real value — on BOTH the
section-PUT and document export→apply paths. Removed map entries emit an explicit
null (RFC 7396 delete).
Shared visual primitives (reuse, don't re-style): useCardStyles /
SectionHeader (cards — owns its own header gap), FormField (label +
YAML-path breadcrumb + helpText + example chips — wrap every field; help should
always be present; the yaml-path chip shows on hover/focus only so the bold label
leads), StickyDirtyBar (the slim floating unsaved-changes bar — use floating
when it can't be the last child in the scroll flow), ApplyDialog (plan → diff →
apply). Every editable field should have a helpText and a placeholder/example.
The PR #24 convergence pass added three more (use these instead of re-rolling):
MaskedSecretInput— the one masked-secret field.mode="sentinel"(shows empty while the value is theREDACTED_SENTINEL, passes it through untouched on save) for webhook headers / Slack bot token;mode="blank-keeps"(blank = keep existing, non-blank = rotate) for SigV4-style secrets.RowListEditor<T>— stable-id-keyed add/remove/update-by-id list scaffolding (the "stable row ids, never array index" rule, centralised). You supplyrenderRow+newItem(); it hands each rowupdate/remove-by-id and emits the next array for you to fold into your single source of truth.StatePlaceholders—LoadingState/EmptyStatechrome (consistent centering, padding, icon size, type scale) so panels stop hand-rolling spinners.
IAM DB reads go through react-query, not useEffect(load). The
queries/{groups,authProviders,mappingRules,users,backends,…}.ts hooks (keyed by
qk.*) own reads + per-record mutations and invalidate qk.* on success — the
old loadData()-after-every-mutation + prop→state-mirror idiom is gone.
- Fork the repo and create a branch from
main - Make your changes
- Run
cargo fmt,cargo clippy, andcargo test - Open a pull request with a clear description of what and why
Open an issue on GitHub. If it's a bug, include:
- What you expected vs. what happened
- Steps to reproduce
- DeltaGlider Proxy version (
deltaglider_proxy --version) - Backend type (filesystem or S3)
DeltaGlider Proxy is licensed under the Business Source License 1.1: production use is free up to 15 TB of compressed stored data, and every release converts to Apache-2.0 two years after it ships. Releases up to and including v1.17.0 remain GPL-3.0. Every Rust source file must start with the SPDX header:
// SPDX-License-Identifier: BUSL-1.1CI fails if a .rs file is missing this header. Run
./scripts/check-spdx-headers.sh locally before pushing.
To contribute, you must sign the Contributor License Agreement. By signing, you assign copyright in your contribution to Beshu Limited. This lets us license the project (BUSL-1.1 + commercial) — the same model used by ReadonlyREST and many other commercial products built in the open.
How to sign: when you open your first pull request, a CLA Assistant bot will comment with a link and signing instructions. You sign once; future PRs from the same GitHub account are automatically accepted.
If the bot has trouble or you need to sign by other means, email a
signed copy of the CLA to contact@beshu.tech with subject:
DeltaGlider Proxy — CLA signed by [Your Name].