Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
debb90c
ObjectStorage: context, create-only writes, ranged and streaming reads
theomonnom Oct 17, 2026
7e75fb0
objects: the ObjectStorage interface without provider SDKs, as config…
theomonnom Oct 17, 2026
cae07d2
ObjectStorage: GCS, Azure, OCI and AliOSS backends
theomonnom Oct 17, 2026
14e4be1
ObjectStorage: IfAbsent atomic on S3 uploads, one constructor, local …
theomonnom Oct 17, 2026
41d0d16
README, objects: when to use ObjectStorage rather than Storage
theomonnom Oct 17, 2026
a2c860a
Storage: one interface; ListObjects, DeleteObjects and GeneratePresig…
theomonnom Oct 17, 2026
8970c75
Storage embeds Bucket; one error rule, listing order and range check
theomonnom Oct 17, 2026
0137431
Storage: every call takes a context; DownloadData and DeleteObject re…
theomonnom Oct 17, 2026
7d4eff7
Tests: every backend but AliOSS end to end against floci in Docker
theomonnom Oct 17, 2026
8d4aae6
CI: a floci job per provider; the unit test job skips them
theomonnom Oct 17, 2026
3f67865
Storage: main's wire behavior kept, S3 semantics, cleaner API, e2e mo…
theomonnom Oct 17, 2026
f0297f9
Storage: deletes run 16 at a time on GCS, Azure and OCI; one S3 clien…
theomonnom Oct 17, 2026
90bc5ad
Storage: fixes from seven reviews (API, backends, tests, best practic…
theomonnom Oct 17, 2026
e1156a7
Storage: ErrorWithStatusCode is objects' type, aliased, so errors.As …
theomonnom Oct 17, 2026
24da205
AliOSS: DownloadFile through downloadFile, CRC64 checked on every who…
theomonnom Oct 18, 2026
b831dff
Storage: no single-use helpers
theomonnom Oct 18, 2026
13c7894
AliOSS: aliOSSRetryable named again, as what retries is not obvious i…
theomonnom Oct 18, 2026
dc4109d
DeleteObjects: *DeleteError names each path left, every backend tries…
theomonnom Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,72 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/go/pkg/mod
~/go/bin
~/.cache
key: livekit-storage

# setup-go caches the module and build caches, keyed on both modules.
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: "go.mod"
cache-dependency-path: |
go.sum
e2e/go.sum

- name: Set up gotestfmt
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@9eae5abc81d6d08f73268741ef4a8b97f29b60d8 # v2.4.1

- name: Download Go modules
run: go mod download
run: go mod download && (cd e2e && go mod download)

- name: Lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.14.0

- name: Lint e2e
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
version: v2.14.0
working-directory: e2e

# The e2e module holds the suites: here they run on the local backend. The
# real-account tests skip, as CI passes them no credentials.
- name: Test
run: |
set -euo pipefail
go test -race -json -v ./... 2>&1 | gotestfmt
cd e2e && go test -race -json -v -skip '^Test(Floci|MinIO)' ./... 2>&1 | gotestfmt

# Every backend end to end against its floci emulator (e2e/floci_test.go),
# and S3 against MinIO (e2e/minio_test.go), whose conditional writes are
# atomic where floci's are not. One check each; TestWorkflowRunsEveryEmulatorTest
# fails when a test is missing here.
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
test: [FlociS3, FlociAzure, FlociGCP, FlociOCI, MinIO]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# setup-go caches the module and build caches, keyed on both modules.
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: "go.mod"
cache-dependency-path: |
go.sum
e2e/go.sum

- name: Set up gotestfmt
run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@9eae5abc81d6d08f73268741ef4a8b97f29b60d8 # v2.4.1

- name: Install MinIO
if: matrix.test == 'MinIO'
run: |
go install github.com/minio/minio@v0.0.0-20260212201848-7aac2a2c5b7c
echo "MINIO_BIN=$(go env GOPATH)/bin/minio" >> "$GITHUB_ENV"

- name: Test
working-directory: e2e
run: |
set -euo pipefail
go test -race -json -v -run '^Test${{ matrix.test }}$' ./... 2>&1 | gotestfmt
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
# Storage

A GORM-style storage wrapper with unified API for different cloud storage backends
Object storage on S3 and S3-compatible stores, GCS, Azure Blob, OCI Object Storage, AliOSS and a local directory, behind one interface. `storage.New(conf)` returns a `Storage`; every call takes a context.

- `Bucket` (`storage/objects`): `PutObject`, `UploadObject` (streams), `GetObject`, `GetObjectRange`, `DownloadObject` (to any `io.Writer`), `ObjectExists`, `ListObjects` (byte order), `DeleteObjects`, `PresignedURL`.
- `Storage` is `Bucket` plus `UploadFile` and `DownloadFile`. `DownloadFile` replaces the local file only once the whole object is read.
- `PutOptions{ContentType, IfAbsent}` on every write. `IfAbsent` refuses to replace an object, atomically where the store honors the condition.
- Errors: a missing object is `ErrNotFound`, a write `IfAbsent` refuses is `ErrExists` (`errors.Is`); `ErrorWithStatusCode` carries the store's HTTP status and `DeleteError` the paths a delete left (`errors.As`).
- `storage/objects` imports no provider SDK, so a fake or a library can implement or accept the interfaces without pulling in every SDK.

## Behavior

- **S3 semantics** where stores differ. `DeleteObjects` of a missing path succeeds, as S3's delete does. It tries every path, and when some are not deleted fails with a `*DeleteError` whose `Failed` names each one with its error (`errors.As`); every other path is deleted, and a retry is safe.
- **Keys** are the store's keys as given. The local directory, as a filesystem, drops a leading `/`, cleans `//` and `.`, refuses keys outside it, and cannot hold both `a` and `a/b`.
- **Timeouts**: a connection that moves no bytes either way for 5 minutes is cut; a slow but live transfer never is. The context bounds the call.
- **Retries**: every store retries throttling, 5xx and network errors with backoff, including AliOSS, whose SDK does not. A stream given to `UploadObject` is sent once on AliOSS.
- **Proxy**: `ProxyConfig` (S3, GCS) carries its credentials for http and https targets, honors `NO_PROXY`, and never proxies the cloud metadata services.
- **Local writes** go in place, as a file is written, so mounted volumes and file watchers see one write; `IfAbsent` writes a temporary file (`.upload-*`, never listed) and links it into place.

## Tests

The suites live in the `e2e` module, so this module's users do not inherit Docker's dependencies.

- `go test ./...` here runs the unit tests.
- `cd e2e && go test ./...` runs every backend except AliOSS end to end: against [floci](https://github.com/floci-io) emulators in Docker (S3, Azure Blob, GCS, OCI Object Storage), against MinIO for S3's conditional writes (`MINIO_BIN` or `minio` on `PATH`), and on the local directory. Without Docker or MinIO those tests skip, except when `CI` is set.
- The provider tests (`TestS3`, `TestGCP`, ...) reach real accounts when their environment variables are set; AliOSS has no emulator and is covered only that way.
Loading
Loading