Skip to content

Commit 01d23f2

Browse files
authored
feat(storage): pluggable dump storage backends (#7)
1 parent ac91634 commit 01d23f2

29 files changed

Lines changed: 1324 additions & 231 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Phase G (ops) — **cloud storage backends** (first G cycle):
13+
- New `internal/storage` leaf defining a backend-neutral `Store` interface (`Put`/`Get`/`Delete`/`List`/`Stat`, all context-aware) with two implementations: **local** (the previous filesystem layout, now behind the interface — temp-write+rename for atomic publish, keys map verbatim to file names so existing catalogs need no migration) and **s3** (S3 and S3-compatible services like MinIO/R2 via AWS SDK v2; streaming transfer-manager upload that is atomic on completion, path-style for custom endpoints).
14+
- `internal/dumps.Catalog` refactored to hold a `Store` and address dumps by key (`<id>.dump`, `<id>.meta.json`) instead of filesystem paths; `Path`/`MetaPath`/`Root` removed. Catalog methods (`PutDump`/`OpenDump`/`ReadMeta`/`WriteMeta`/`List`/`Delete`/`ResolveChain`/`PruneDryRun`) are now context-aware. `backup`, `restore`, `verify`, and `dumps` stream through the store; SHA-256 integrity is computed app-side over `envelope ++ body`, so a dump verifies identically whether it lives locally or in S3.
15+
- New `storage:` config block (`type: local|s3`, `bucket`, `prefix`, `region`, `endpoint`) with fail-fast validation; S3 credentials resolve from the standard AWS chain (never stored in config). See [docs/STORAGE.md](docs/STORAGE.md).
16+
- A shared `RunStoreSuite` contract test (mirroring Phase D's `RunDriverSuite`) runs against every backend: the local backend runs it as a unit test, and the S3 backend runs it against MinIO via testcontainers under the `integration` tag, so the live object-store path executes in CI. GCS/Azure backends are a fast-follow.
17+
1218
- Phase A skeleton: Go module, Cobra CLI with placeholder subcommands, Bubble Tea TUI placeholder, Driver interface + registry, errs package, config stub, Makefile, golangci-lint with depguard, CI workflow.
1319
- Phase B Postgres walking skeleton: `backup`, `restore`, `sync`, `verify`, `inspect`, `dumps`, and `profile` working end-to-end against PostgreSQL (shelling out to `pg_dump`/`pg_restore`, pgx for inspect), SHA-256 dump checksums with sidecar metadata, named profiles with `env:` secret refs, and POSIX exit-code taxonomy.
1420
- Phase C interactive TUI dashboard: multi-panel Bubble Tea UI (profiles · dumps · jobs) with live job-progress subscription, backup/restore modal forms, an error overlay, and golden snapshot tests.

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ A single binary that turns the painful, error-prone sprawl of `pg_dump` → `pg_
1717
---
1818

1919
> [!WARNING]
20-
> **Pre-1.0 — active development.** Postgres, MySQL, and MariaDB backup/restore/sync/verify/inspect work end-to-end today (Phases B + E), and bare `siphon` opens an interactive multi-panel dashboard (Phase C). The driver layer is hardened with a shared cross-driver test harness, capability gating, and connection retry (Phase D). Advanced transfer is complete (Phase F): incremental backup/restore, cross-engine sync, and CDC continuous replication all work end-to-end. Ops features (cloud storage, secret backends, retention, telemetry) are on the [roadmap](#roadmap). APIs, flags, and the on-disk dump format may change before 1.0. Track progress via the milestone tags (`phase-a``phase-f`).
20+
> **Pre-1.0 — active development.** Postgres, MySQL, and MariaDB backup/restore/sync/verify/inspect work end-to-end today (Phases B + E), and bare `siphon` opens an interactive multi-panel dashboard (Phase C). The driver layer is hardened with a shared cross-driver test harness, capability gating, and connection retry (Phase D). Advanced transfer is complete (Phase F): incremental backup/restore, cross-engine sync, and CDC continuous replication all work end-to-end. Ops features (Phase G) are underway — the dump catalog can now live in an S3 / S3-compatible bucket (`storage:` config block); secret backends, retention, and telemetry are still on the [roadmap](#roadmap). APIs, flags, and the on-disk dump format may change before 1.0. Track progress via the milestone tags (`phase-a``phase-f`).
2121
2222
## Table of contents
2323

@@ -55,7 +55,7 @@ A single binary that turns the painful, error-prone sprawl of `pg_dump` → `pg_
5555
| **D** — Driver hardening | Shared cross-driver test harness (`RunDriverSuite`), capability-gating helper (`RequireCapability`), Postgres connection-probe retry, and a `docs/DRIVERS.md` contributor guide | ✅ Complete |
5656
| **E** — MySQL + MariaDB | Both drivers via a shared `_mysqlcommon` package (shared `Conn`, `mysqldump`/`mariadb-dump` backup, client-pipe restore), exercised by the Phase D `RunDriverSuite` harness | ✅ Complete |
5757
| **F** — Advanced transfer | All four advanced-transfer modes work end-to-end: bounded-buffer streaming sync; **incremental** backup/restore (`backup --incremental --base <id>` captures a bounded change set via Postgres logical decoding / MySQL-MariaDB binlog, `restore` replays the base→incremental chain, Postgres orphan-slot sweep); **cross-engine** sync (`sync --cross-engine` — typed `SchemaInspector` introspection → canonical type-mapping, e.g. Postgres → MySQL); and **CDC** (`siphon cdc` / `sync --continuous` — unbounded change streaming with snapshot→stream handoff, resumable, same- and cross-engine). Live DB paths are integration-tested in CI — see [docs/INCREMENTAL.md](docs/INCREMENTAL.md), [docs/CROSS_ENGINE.md](docs/CROSS_ENGINE.md), [docs/CDC.md](docs/CDC.md) | ✅ Complete |
58-
| **G** — Ops features | Cloud storage, secret backends, profile groups + 2FA, team mode, audit log, retention, telemetry | ⏳ Planned |
58+
| **G** — Ops features | **Cloud storage** ships first: the dump catalog can live in an S3 / S3-compatible bucket via a pluggable `storage.Store` backend (local + S3 today; GCS/Azure are a fast-follow), selected by a `storage:` config block, with SHA-256 integrity preserved end-to-end — see [docs/STORAGE.md](docs/STORAGE.md). Secret backends, profile groups + 2FA, team mode, audit log, retention, and telemetry follow in later G cycles. | 🟡 In progress |
5959
| **H** — Distribution | GoReleaser, Homebrew tap, Scoop bucket, install script, docs site | ⏳ Planned |
6060

6161
## Requirements
@@ -168,6 +168,19 @@ profiles:
168168
169169
Secret references (`env:VAR`) are resolved at runtime, so the config file is safe to commit. Plain values are also accepted.
170170

171+
By default the dump catalog lives on local disk at `defaults.dump_dir`. To store dumps in an S3 / S3-compatible bucket instead, add a `storage:` block:
172+
173+
```yaml
174+
storage:
175+
type: s3 # "local" (default) | "s3"
176+
bucket: my-siphon-dumps # required for s3
177+
prefix: prod # optional key prefix
178+
region: us-east-1
179+
endpoint: "" # optional: custom endpoint for MinIO / R2
180+
```
181+
182+
Credentials are resolved from the standard AWS chain (env vars, `~/.aws`, instance role) — never stored in the config file. See [docs/STORAGE.md](docs/STORAGE.md) for details.
183+
171184
## Architecture
172185

173186
siphon is a strictly layered Go application; imports flow **downward only**, enforced at lint time by `golangci-lint`'s `depguard` (an upward import fails CI):

docs/STORAGE.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Storage backends
2+
3+
By default siphon keeps the dump catalog — every dump body plus its sidecar
4+
metadata — in a local directory. Phase G adds a pluggable storage layer so the
5+
catalog can instead live in an S3 (or S3-compatible) bucket, with `backup`,
6+
`restore`, `verify`, and `dumps` all reading and writing through it transparently.
7+
8+
## Table of contents
9+
10+
- [How it works](#how-it-works)
11+
- [Configuration](#configuration)
12+
- [Integrity across backends](#integrity-across-backends)
13+
- [Scope and limitations](#scope-and-limitations)
14+
15+
## How it works
16+
17+
The dump catalog (`internal/dumps`) holds a `storage.Store` and addresses
18+
objects by opaque keys — `<id>.dump` for the envelope-prefixed dump body and
19+
`<id>.meta.json` for the sidecar — never by filesystem path. The `Store`
20+
interface is small and backend-neutral:
21+
22+
```go
23+
type Store interface {
24+
Put(ctx, key, io.Reader) error // durable, atomic-on-complete
25+
Get(ctx, key) (io.ReadCloser, error) // one-shot forward stream
26+
Delete(ctx, key) error // idempotent
27+
List(ctx) ([]string, error)
28+
Stat(ctx, key) (size int64, exists bool, err error)
29+
}
30+
```
31+
32+
Two backends ship today (`internal/storage`):
33+
34+
- **local** — a single directory. `Put` writes to a temp file and renames, so a
35+
failed or cancelled write never leaves a partial dump under its final key.
36+
Keys map verbatim to file names, so a pre-Phase-G local catalog keeps working
37+
with no migration.
38+
- **s3** — an S3 or S3-compatible bucket (AWS, MinIO, Cloudflare R2). `Put`
39+
streams through the SDK's transfer manager, so the object only becomes
40+
addressable once the upload completes — the same atomic-on-complete guarantee
41+
the local backend gets from rename.
42+
43+
A backup stages the dump body to a local temp file (the dump tool needs a real
44+
fd), then streams `envelope ++ body` into the store in a single `Put`, teeing
45+
through SHA-256 as the bytes flow. Restore and verify open the dump with `Get`
46+
and stream it straight into the envelope reader — no full local download.
47+
48+
## Configuration
49+
50+
Storage is selected by a top-level `storage:` block in the config file. Omitting
51+
it (or `type: local`) uses the local filesystem at `defaults.dump_dir`.
52+
53+
```yaml
54+
version: 1
55+
defaults:
56+
dump_dir: ~/.local/share/siphon/dumps # used by the local backend
57+
58+
storage:
59+
type: s3 # "local" (default) | "s3"
60+
bucket: my-siphon-dumps # required for s3
61+
prefix: prod # optional key prefix within the bucket
62+
region: us-east-1
63+
endpoint: "" # optional: custom endpoint for S3-compatible services
64+
```
65+
66+
For an S3-compatible service such as MinIO or R2, set `endpoint` to its URL
67+
(path-style addressing is used automatically).
68+
69+
**Credentials are never stored in the config file.** The S3 backend resolves
70+
them from the standard AWS chain — `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
71+
environment variables, the shared `~/.aws/config`, or an instance/role profile —
72+
so the config stays safe to commit.
73+
74+
## Integrity across backends
75+
76+
The SHA-256 checksum is computed over the `envelope ++ body` stream at write
77+
time and recomputed over the `Get` stream at verify time — both in siphon, not
78+
in the backend. A dump written to S3 and a dump written locally therefore verify
79+
identically, and `siphon verify` catches corruption regardless of where the dump
80+
lives. siphon does not trust the backend's own ETag/MD5 (multipart uploads do
81+
not expose a plain object MD5).
82+
83+
The live S3 path is integration-tested in CI against MinIO via testcontainers
84+
(`internal/storage/s3_integration_test.go`) using the same `RunStoreSuite`
85+
contract that the local backend runs, so the streaming upload, ranged read,
86+
listing, and not-found mapping all execute against a real object store — not
87+
just compile.
88+
89+
## Scope and limitations
90+
91+
- Backends covered: **local** and **s3** (incl. S3-compatible). **GCS and Azure
92+
Blob are not implemented yet** — they are a fast-follow, and will get the full
93+
correctness bar for free by running the same `RunStoreSuite` contract.
94+
- `dumps list` over S3 issues one `ListObjectsV2` plus one read per metadata
95+
object (N+1). This is fine at expected catalog sizes; no pagination/parallel
96+
optimization is done yet.
97+
- Each `Get` is a fresh one-shot forward stream — callers must not assume the
98+
returned reader is seekable.
99+
- Retention/lifecycle (chain-aware pruning over remote storage) remains a
100+
separate Phase G concern; the `Store.Delete` it needs is delivered here.

go.mod

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ module github.com/nixrajput/siphon
33
go 1.26.3
44

55
require (
6+
github.com/aws/aws-sdk-go-v2 v1.42.0
7+
github.com/aws/aws-sdk-go-v2/config v1.32.25
8+
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
9+
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.11
10+
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0
11+
github.com/aws/smithy-go v1.27.1
612
github.com/charmbracelet/bubbles v1.0.0
713
github.com/charmbracelet/bubbletea v1.3.10
814
github.com/charmbracelet/huh v1.0.0
@@ -16,6 +22,7 @@ require (
1622
github.com/spf13/cobra v1.10.2
1723
github.com/testcontainers/testcontainers-go v0.42.0
1824
github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0
25+
github.com/testcontainers/testcontainers-go/modules/minio v0.42.0
1926
github.com/testcontainers/testcontainers-go/modules/mysql v0.42.0
2027
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
2128
gopkg.in/yaml.v3 v3.0.1
@@ -27,6 +34,19 @@ require (
2734
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
2835
github.com/Microsoft/go-winio v0.6.2 // indirect
2936
github.com/atotto/clipboard v0.1.4 // indirect
37+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect
38+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
39+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect
40+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect
41+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect
42+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect
43+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect
44+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect
45+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect
46+
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect
47+
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect
48+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect
49+
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect
3050
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
3151
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
3252
github.com/catppuccin/go v0.3.0 // indirect

0 commit comments

Comments
 (0)