From 2dbcb5f0eba6e3e435115e975e9a4b2ffb5b6d39 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 14 Jan 2026 14:58:06 -0500 Subject: [PATCH 1/5] refactor: remove Go SDK code and configuration Remove all Go-related code, configuration, and CI workflows from the repository. The Go SDK is now maintained separately at github.com/tigrisdata/storage-go. Deleted: - go/ directory (all Go source code) - go.mod and go.sum - .github/workflows/go.yaml Updated: - README.md - reference external Go SDK repository - AGENTS.md - remove all Go references Assisted-by: GLM 4.7 via Claude Code --- .github/workflows/go.yaml | 46 --- AGENTS.md | 36 +- README.md | 5 +- go.mod | 28 -- go.sum | 38 -- go/README.md | 244 ----------- go/client.go | 95 ----- go/example_test.go | 142 ------- go/storage.go | 108 ----- go/storage_test.go | 320 --------------- go/tigrisheaders/example_test.go | 115 ------ go/tigrisheaders/tigrisheaders.go | 151 ------- go/tigrisheaders/tigrisheaders_test.go | 540 ------------------------- 13 files changed, 8 insertions(+), 1860 deletions(-) delete mode 100644 .github/workflows/go.yaml delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 go/README.md delete mode 100644 go/client.go delete mode 100644 go/example_test.go delete mode 100644 go/storage.go delete mode 100644 go/storage_test.go delete mode 100644 go/tigrisheaders/example_test.go delete mode 100644 go/tigrisheaders/tigrisheaders.go delete mode 100644 go/tigrisheaders/tigrisheaders_test.go diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml deleted file mode 100644 index ebac06a..0000000 --- a/.github/workflows/go.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Go - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -permissions: - contents: read - actions: write - -jobs: - go_tests: - strategy: - matrix: - os: - - ubuntu-24.04 - - windows-2025 - - macos-15 - - ubuntu-24.04-arm - - windows-11-arm - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - fetch-tags: true - - - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - with: - node-version: latest - - - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 - with: - go-version: stable - - - name: Test - run: | - go vet ./... - go test ./... - - - uses: dominikh/staticcheck-action@024238d2898c874f26d723e7d0ff4308c35589a2 # v1.4.0 - with: - version: "latest" - install-go: false diff --git a/AGENTS.md b/AGENTS.md index 775747f..caaca1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,18 +36,17 @@ Assisted-by: GLM 4.6 via Claude Code - Include a clear description of changes - Reference any related issues -- Pass CI (`npm test` for JavaScript, `go test` for Go) +- Pass CI (`npm test` for JavaScript) - Optionally add screenshots for UI changes ### Security Best Practices - Secrets never belong in the repo; use environment variables or the `secrets` directory (ignored by Git) - Run `npm audit` periodically for JavaScript packages and address reported vulnerabilities -- For Go, use `go mod` to manage dependencies and keep them updated ## Project Structure -This is a monorepo for Tigris object storage SDKs and CLI, containing: +This is a monorepo for Tigris object storage SDKs, containing: ### JavaScript/TypeScript Packages @@ -72,23 +71,6 @@ Root-level npm scripts: - `npm run format` - Format all packages with Prettier - `npm run clean` - Clean build artifacts -### Go SDK - -Located in the [`go/`](go/) directory: - -- **Module**: `github.com/tigrisdata/storage` -- **Go version**: 1.25.5 -- **Uses**: AWS SDK v2 for Go -- **Main files**: - - `client.go` - Client implementation - - `storage.go` - Storage operations - - `tigrisheaders/` - Tigris-specific headers - -Go commands: -- `go test ./go/...` - Run all Go tests -- `go build ./go/...` - Build all Go packages -- `go mod tidy` - Clean up dependencies - ## Development Workflow ### JavaScript/TypeScript Development @@ -99,18 +81,9 @@ Go commands: 4. Format code: `npm run format` 5. Lint code: `npm run lint` -### Go Development - -1. Navigate to Go directory: `cd go` -2. Run tests: `go test ./...` -3. Build: `go build ./...` -4. Format code: `go fmt ./...` -5. Manage dependencies: `go mod tidy` - ## Testing - **JavaScript**: Uses Vitest as the test runner -- **Go**: Uses the standard `go test` command - Always run tests before committing changes - Ensure all tests pass in CI before merging @@ -119,11 +92,10 @@ Go commands: - Releases are automated using semantic-release - Commits to `main` trigger automatic releases - Pre-releases are done on the `next` branch -- Both JavaScript packages and Go SDK follow semantic versioning +- JavaScript packages follow semantic versioning ## Additional Notes - The project uses Husky for Git hooks (commitlint, etc.) - Commitizen is configured for conventional commits -- ESLint and Prettier are used for JavaScript/TypeScript code quality -- Go code should follow standard Go conventions and use `go fmt` \ No newline at end of file +- ESLint and Prettier are used for JavaScript/TypeScript code quality \ No newline at end of file diff --git a/README.md b/README.md index 507833b..87fd87c 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,7 @@ This monorepo contains multiple packages for Tigris object storage: ## Go SDK -For more information about the Go SDK, see the [Go SDK README](./go/README.md). +The Go SDK is now maintained in a separate repository: + +- Repository: [https://github.com/tigrisdata/storage-go](https://github.com/tigrisdata/storage-go) +- Documentation: [https://pkg.go.dev/github.com/tigrisdata/storage-go](https://pkg.go.dev/github.com/tigrisdata/storage-go) diff --git a/go.mod b/go.mod deleted file mode 100644 index 9a58b63..0000000 --- a/go.mod +++ /dev/null @@ -1,28 +0,0 @@ -module github.com/tigrisdata/storage - -go 1.25.5 - -require ( - github.com/aws/aws-sdk-go-v2 v1.41.0 - github.com/aws/aws-sdk-go-v2/config v1.32.6 - github.com/aws/aws-sdk-go-v2/credentials v1.19.6 - github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 -) - -require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect - github.com/aws/smithy-go v1.24.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 8b3905e..0000000 --- a/go.sum +++ /dev/null @@ -1,38 +0,0 @@ -github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= -github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= -github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= -github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= -github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0= -github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= -github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= -github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= diff --git a/go/README.md b/go/README.md deleted file mode 100644 index d3cc030..0000000 --- a/go/README.md +++ /dev/null @@ -1,244 +0,0 @@ -# Tigris Storage SDK for Go - -Welcome to the Tigris Storage SDK for Go! This package contains high-level wrappers and helpers to help you take advantage of all of Tigris' features. - -## Overview - -[Tigris](https://www.tigrisdata.com/) is a cloud storage service that provides a simple, scalable, and secure object storage solution. It is based on the S3 API, but has additional features that need these helpers. - -This SDK provides two main packages: - -- **`storage`** - The main package containing the Tigris client with S3-compatible methods plus Tigris-specific features like bucket forking, snapshots, and object renaming. -- **`tigrisheaders`** - Lower-level helpers for setting Tigris-specific HTTP headers on S3 API calls. - -## Installation - -```bash -go get github.com/tigrisdata/storage/go -``` - -## Quick Start - -```go -package main - -import ( - "context" - "fmt" - "log" - - storage "github.com/tigrisdata/storage/go" -) - -func main() { - ctx := context.Background() - - // Create a new Tigris client - client, err := storage.New(ctx) - if err != nil { - log.Fatal(err) - } - - // Use the client like you would use AWS S3 client - // plus Tigris-specific features (see below) - fmt.Println("Connected to Tigris!") -} -``` - -## Client Configuration - -The `New()` function creates a new S3 client optimized for interactions with Tigris. It accepts functional options to configure the client: - -```go -client, err := storage.New(ctx, - storage.WithFlyEndpoint(), // Use fly.io optimized endpoint - storage.WithGlobalEndpoint(), // Use globally available endpoint (default) - storage.WithRegion("auto"), // Specify a region - storage.WithAccessKeypair(key, secret), // Set access credentials -) -``` - -### Configuration Options - -| Option | Description | -| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| `WithFlyEndpoint()` | Connect to Tigris' fly.io optimized endpoint. If you are deployed to fly.io, this zero-rates your traffic to Tigris. | -| `WithGlobalEndpoint()` | Connect to Tigris' globally available endpoint (`https://t3.storage.dev`). This is the default. | -| `WithRegion(region)` | Statically specify a region for interacting with Tigris. You will almost certainly never need this. | -| `WithAccessKeypair(accessKeyID, secretAccessKey)` | Specify a custom access key and secret access key. Useful when loading credentials from non-standard locations. | - -## Bucket Features - -### Snapshots and Forks - -Tigris supports bucket snapshots and forking, allowing you to create point-in-time copies of buckets and branch from them. - -#### Create a Snapshottable Bucket - -```go -output, err := client.CreateSnapshottableBucket(ctx, &s3.CreateBucketInput{ - Bucket: aws.String("my-bucket"), -}) -``` - -#### Create a Snapshot - -```go -output, err := client.CreateBucketSnapshot(ctx, "Initial backup", &s3.CreateBucketInput{ - Bucket: aws.String("my-bucket"), -}) -``` - -#### Fork a Bucket - -```go -// Creates a new bucket "my-bucket-fork" as a fork of "my-bucket" -output, err := client.CreateBucketFork(ctx, "my-bucket", "my-bucket-fork") -``` - -#### List Snapshots - -```go -snapshots, err := client.ListBucketSnapshots(ctx, "my-bucket") -``` - -#### Get Fork/Snapshot Metadata - -```go -info, err := client.HeadBucketForkOrSnapshot(ctx, &s3.HeadBucketInput{ - Bucket: aws.String("my-bucket"), -}) -// info.SnapshotsEnabled - true if snapshots are enabled -// info.SourceBucket - The bucket this was forked from -// info.SourceBucketSnapshot - The snapshot this was forked from -// info.IsForkParent - true if there are forks of this bucket -``` - -## Object Features - -### Rename Objects - -Tigris supports in-place object renaming without copying data: - -```go -_, err := client.RenameObject(ctx, &s3.CopyObjectInput{ - Bucket: aws.String("my-bucket"), - CopySource: aws.String("my-bucket/old-name.txt"), - Key: aws.String("new-name.txt"), -}) -``` - -## Using the tigrisheaders Package - -The `tigrisheaders` package provides lower-level helpers for setting Tigris-specific HTTP headers. These can be used directly with S3 client operations. - -### Static Replication Regions - -Control which regions your objects are replicated to: - -```go -import "github.com/tigrisdata/storage/go/tigrisheaders" - -// Replicate to specific regions -_, err := client.PutObject(ctx, &s3.PutObjectInput{ - Bucket: aws.String("my-bucket"), - Key: aws.String("file.txt"), - Body: bytes.NewReader(data), -}, tigrisheaders.WithStaticReplicationRegions([]tigrisheaders.Region{ - tigrisheaders.FRA, // Frankfurt - tigrisheaders.SJC, // San Jose -})) -``` - -Available regions: - -- `FRA` - Frankfurt, Germany -- `GRU` - São Paulo, Brazil -- `HKG` - Hong Kong, China -- `IAD` - Ashburn, Virginia, USA -- `JNB` - Johannesburg, South Africa -- `LHR` - London, UK -- `MAD` - Madrid, Spain -- `NRT` - Tokyo (Narita), Japan -- `ORD` - Chicago, Illinois, USA -- `SIN` - Singapore -- `SJC` - San Jose, California, USA -- `SYD` - Sydney, Australia -- `Europe` - European datacenters -- `USA` - American datacenters - -### Query Metadata - -Filter objects in a ListObjectsV2 request with a SQL-like WHERE clause: - -```go -_, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ - Bucket: aws.String("my-bucket"), -}, tigrisheaders.WithQuery("metadata.user_id = '123'")) -``` - -### Conditional Operations - -Perform operations based on object state: - -```go -// Create object only if it doesn't exist -_, err := client.PutObject(ctx, input, - tigrisheaders.WithCreateObjectIfNotExists()) - -// Only proceed if ETag matches -_, err := client.PutObject(ctx, input, - tigrisheaders.WithIfEtagMatches("\"abc123\"")) - -// Only proceed if modified since date -_, err := client.GetObject(ctx, input, - tigrisheaders.WithModifiedSince(time.Now().Add(-24 * time.Hour))) - -// Only proceed if unmodified since date -_, err := client.GetObject(ctx, input, - tigrisheaders.WithUnmodifiedSince(time.Now().Add(-24 * time.Hour))) - -// Compare-and-swap (skip cache, read from designated region) -_, err := client.GetObject(ctx, input, - tigrisheaders.WithCompareAndSwap()) -``` - -### Snapshot Operations - -Work with specific snapshot versions: - -```go -// List objects from a specific snapshot -_, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ - Bucket: aws.String("my-bucket"), -}, tigrisheaders.WithSnapshotVersion("snapshot-id")) - -// Get object from a specific snapshot -_, err := client.GetObject(ctx, &s3.GetObjectInput{ - Bucket: aws.String("my-bucket"), - Key: aws.String("file.txt"), -}, tigrisheaders.WithSnapshotVersion("snapshot-id")) -``` - -### Custom Headers - -Set arbitrary HTTP headers on requests: - -```go -_, err := client.PutObject(ctx, input, - tigrisheaders.WithHeader("X-Custom-Header", "value")) -``` - -## Documentation - -For more information on Tigris features, see: - -- [Snapshots and Forks](https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/) -- [Object Rename](https://www.tigrisdata.com/docs/objects/object-rename/) -- [Query Metadata](https://www.tigrisdata.com/docs/objects/query-metadata/) -- [Conditional Operations](https://www.tigrisdata.com/docs/objects/conditionals/) -- [Regions](https://www.tigrisdata.com/docs/concepts/regions/) - -## License - -See [LICENSE](../LICENSE) for details. diff --git a/go/client.go b/go/client.go deleted file mode 100644 index 74c0ff9..0000000 --- a/go/client.go +++ /dev/null @@ -1,95 +0,0 @@ -package storage - -import ( - "context" - "fmt" - "net/http" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/aws/middleware" - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/tigrisdata/storage/go/tigrisheaders" -) - -// Client is a wrapper around the AWS SDK S3 Client with additional methods for integration with Tigris. -type Client struct { - *s3.Client -} - -// CreateBucketFork creates a fork of the source bucket named target. -// -// If you want to specify an exact snapshot version to fork from, use tigrisheaders.WithSnapshotVersion. -func (c *Client) CreateBucketFork(ctx context.Context, source, target string, opts ...func(*s3.Options)) (*s3.CreateBucketOutput, error) { - opts = append(opts, tigrisheaders.WithHeader("X-Tigris-Fork-Source-Bucket", source)) - - return c.Client.CreateBucket(ctx, &s3.CreateBucketInput{ - Bucket: aws.String(target), - }, opts...) -} - -// CreateBucketSnapshot creates a snapshot with the given description for a bucket. -func (c *Client) CreateBucketSnapshot(ctx context.Context, description string, in *s3.CreateBucketInput, opts ...func(*s3.Options)) (*s3.CreateBucketOutput, error) { - opts = append(opts, tigrisheaders.WithTakeSnapshot(description)) - - return c.Client.CreateBucket(ctx, in, opts...) -} - -// CreateSnapshottableBucket creates a new bucket with the ability to take snapshots and fork the contents of it. -func (c *Client) CreateSnapshottableBucket(ctx context.Context, in *s3.CreateBucketInput, opts ...func(*s3.Options)) (*s3.CreateBucketOutput, error) { - opts = append(opts, tigrisheaders.WithEnableSnapshot()) - - return c.Client.CreateBucket(ctx, in, opts...) -} - -// HeadBucketForkOrSnapshotOutput records the fork/snapshot metadata for a bucket. -type HeadBucketForkOrSnapshotOutput struct { - SnapshotsEnabled bool // true if snapshots are enabled, otherwise false. - SourceBucket string // The name of the bucket this bucket was forked from. - SourceBucketSnapshot string // The snapshot this bucket was forked from. - IsForkParent bool // true if there are forks of this bucket, otherwise false. -} - -// HeadBucketForkOrSnapshot fetches the fork/snapshot metadata for a bucket. -// -// For more information, see the Tigris documentation[1]. -// -// [1]: https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/#retrieving-snapshot-and-fork-info-for-a-bucket -func (c *Client) HeadBucketForkOrSnapshot(ctx context.Context, in *s3.HeadBucketInput, opts ...func(*s3.Options)) (*HeadBucketForkOrSnapshotOutput, error) { - resp, err := c.Client.HeadBucket(ctx, in, opts...) - if err != nil { - return nil, err - } - - rawResp, ok := middleware.GetRawResponse(resp.ResultMetadata).(*http.Response) - if !ok { - return nil, fmt.Errorf("unexpected response type from middleware") - } - return &HeadBucketForkOrSnapshotOutput{ - SnapshotsEnabled: rawResp.Header.Get("X-Tigris-Enable-Snapshot") == "true", - SourceBucket: rawResp.Header.Get("X-Tigris-Fork-Source-Bucket"), - SourceBucketSnapshot: rawResp.Header.Get("X-Tigris-Fork-Source-Bucket-Snapshot"), - IsForkParent: rawResp.Header.Get("X-Tigris-Is-Fork-Parent") == "true", - }, nil -} - -// ListBucketSnapshots lists the snapshots for a bucket. -// -// For more information, see the Tigris documentation[1]. -// -// [1]: https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/#listing-snapshots -func (c *Client) ListBucketSnapshots(ctx context.Context, bucketName string, opts ...func(*s3.Options)) (*s3.ListBucketsOutput, error) { - opts = append(opts, tigrisheaders.WithHeader("X-Tigris-Snapshot", bucketName)) - - return c.Client.ListBuckets(ctx, &s3.ListBucketsInput{}, opts...) -} - -// RenameObject performs an in-place rename of objects instead of copying the data. -// -// For more information, see the Tigris documentation[1]. -// -// [1]: https://www.tigrisdata.com/docs/objects/object-rename/ -func (c *Client) RenameObject(ctx context.Context, in *s3.CopyObjectInput, opts ...func(*s3.Options)) (*s3.CopyObjectOutput, error) { - opts = append(opts, tigrisheaders.WithRename()) - - return c.Client.CopyObject(ctx, in, opts...) -} diff --git a/go/example_test.go b/go/example_test.go deleted file mode 100644 index a76c4a5..0000000 --- a/go/example_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package storage_test - -import ( - "context" - "log" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/s3" - storage "github.com/tigrisdata/storage/go" -) - -func ExampleNew() { - ctx := context.Background() - - // Create a new Tigris client with default options - client, err := storage.New(ctx) - if err != nil { - log.Fatal(err) - } - _ = client - - // Create a client with custom options - client, err = storage.New(ctx, - storage.WithFlyEndpoint(), // Use fly.io optimized endpoint - storage.WithGlobalEndpoint(), // Use globally available endpoint (default) - storage.WithRegion("auto"), // Specify a region - // storage.WithAccessKeypair(key, secret), // Set access credentials - ) - if err != nil { - log.Fatal(err) - } - _ = client -} - -func ExampleClient_CreateSnapshottableBucket() { - ctx := context.Background() - - client, err := storage.New(ctx) - if err != nil { - log.Fatal(err) - } - - // Create a bucket with snapshot support enabled - output, err := client.CreateSnapshottableBucket(ctx, &s3.CreateBucketInput{ - Bucket: aws.String("my-bucket"), - }) - if err != nil { - log.Fatal(err) - } - _ = output -} - -func ExampleClient_CreateBucketSnapshot() { - ctx := context.Background() - - client, err := storage.New(ctx) - if err != nil { - log.Fatal(err) - } - - // Create a snapshot with a description - output, err := client.CreateBucketSnapshot(ctx, "Initial backup", &s3.CreateBucketInput{ - Bucket: aws.String("my-bucket"), - }) - if err != nil { - log.Fatal(err) - } - _ = output -} - -func ExampleClient_CreateBucketFork() { - ctx := context.Background() - - client, err := storage.New(ctx) - if err != nil { - log.Fatal(err) - } - - // Creates a new bucket "my-bucket-fork" as a fork of "my-bucket" - output, err := client.CreateBucketFork(ctx, "my-bucket", "my-bucket-fork") - if err != nil { - log.Fatal(err) - } - _ = output -} - -func ExampleClient_ListBucketSnapshots() { - ctx := context.Background() - - client, err := storage.New(ctx) - if err != nil { - log.Fatal(err) - } - - // List all snapshots for a bucket - snapshots, err := client.ListBucketSnapshots(ctx, "my-bucket") - if err != nil { - log.Fatal(err) - } - _ = snapshots -} - -func ExampleClient_HeadBucketForkOrSnapshot() { - ctx := context.Background() - - client, err := storage.New(ctx) - if err != nil { - log.Fatal(err) - } - - // Get fork/snapshot metadata for a bucket - info, err := client.HeadBucketForkOrSnapshot(ctx, &s3.HeadBucketInput{ - Bucket: aws.String("my-bucket"), - }) - if err != nil { - log.Fatal(err) - } - - _ = info.SnapshotsEnabled // true if snapshots are enabled - _ = info.SourceBucket // The bucket this was forked from - _ = info.SourceBucketSnapshot // The snapshot this was forked from - _ = info.IsForkParent // true if there are forks of this bucket -} - -func ExampleClient_RenameObject() { - ctx := context.Background() - - client, err := storage.New(ctx) - if err != nil { - log.Fatal(err) - } - - // Rename an object in-place without copying data - _, err = client.RenameObject(ctx, &s3.CopyObjectInput{ - Bucket: aws.String("my-bucket"), - CopySource: aws.String("my-bucket/old-name.txt"), - Key: aws.String("new-name.txt"), - }) - if err != nil { - log.Fatal(err) - } -} diff --git a/go/storage.go b/go/storage.go deleted file mode 100644 index a61fd70..0000000 --- a/go/storage.go +++ /dev/null @@ -1,108 +0,0 @@ -// Package storage contains a Tigris client and helpers for interacting with Tigris. -// -// Tigris is a cloud storage service that provides a simple, scalable, and secure object storage solution. It is based on the S3 API, but has additional features that need these helpers. -package storage - -import ( - "context" - "fmt" - - "github.com/aws/aws-sdk-go-v2/aws" - awsConfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/s3" -) - -// Options is the set of options that can be configured for the Tigris client. -type Options struct { - BaseEndpoint string - Region string - UsePathStyle bool - - AccessKeyID string - SecretAccessKey string -} - -// defaults returns the default configuration data for the Tigris client. -func (Options) defaults() Options { - return Options{ - BaseEndpoint: "https://t3.storage.dev", - Region: "auto", - UsePathStyle: false, - } -} - -// Option is a functional option for configuring the Tigris client. -type Option func(o *Options) - -// WithFlyEndpoint lets you connect to Tigris' fly.io optimized endpoint. -// -// If you are deployed to fly.io, this zero-rates your traffic to Tigris. -// -// If you are not deployed to fly.io, please use WithGlobalEndpoint instead. -func WithFlyEndpoint() Option { - return func(o *Options) { - o.BaseEndpoint = "https://fly.storage.tigris.dev" - } -} - -// WithGlobalEndpoint lets you connect to Tigris' globally available endpoint. -// -// If you are deployed to fly.io, please use WithFlyEndpoint instead. -func WithGlobalEndpoint() Option { - return func(o *Options) { - o.BaseEndpoint = "https://t3.storage.dev" - } -} - -// WithRegion lets you statically specify a region for interacting with Tigris. -// -// You will almost certainly never need this. This is here for development usecases where the default region is not "auto". -func WithRegion(region string) Option { - return func(o *Options) { - o.Region = region - } -} - -// WithAccessKeypair lets you specify a custom access key and secret access key for interfacing with Tigris. -// -// This is useful when you need to load environment variables from somewhere other than the default AWS configuration path. -func WithAccessKeypair(accessKeyID, secretAccessKey string) Option { - return func(o *Options) { - o.AccessKeyID = accessKeyID - o.SecretAccessKey = secretAccessKey - } -} - -// New returns a new S3 client optimized for interactions with Tigris. -func New(ctx context.Context, options ...Option) (*Client, error) { - o := new(Options).defaults() - - for _, doer := range options { - doer(&o) - } - - var creds aws.CredentialsProvider - - if o.AccessKeyID != "" && o.SecretAccessKey != "" { - creds = credentials.NewStaticCredentialsProvider(o.AccessKeyID, o.SecretAccessKey, "") - } - - cfg, err := awsConfig.LoadDefaultConfig(ctx) - if err != nil { - return nil, fmt.Errorf("failed to load Tigris config: %w", err) - } - - cli := s3.NewFromConfig(cfg, func(opts *s3.Options) { - opts.BaseEndpoint = aws.String(o.BaseEndpoint) - opts.Region = o.Region - opts.UsePathStyle = o.UsePathStyle - if creds != nil { - opts.Credentials = creds - } - }) - - return &Client{ - Client: cli, - }, nil -} diff --git a/go/storage_test.go b/go/storage_test.go deleted file mode 100644 index c5a4ce6..0000000 --- a/go/storage_test.go +++ /dev/null @@ -1,320 +0,0 @@ -package storage - -import ( - "context" - "testing" - - "github.com/aws/aws-sdk-go-v2/service/s3" -) - -func TestOptions_defaults(t *testing.T) { - tests := []struct { - name string - input Options - wantEndpoint string - wantRegion string - wantPathStyle bool - }{ - { - name: "default values", - input: Options{}, - wantEndpoint: "https://t3.storage.dev", - wantRegion: "auto", - wantPathStyle: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.input.defaults() - if got.BaseEndpoint != tt.wantEndpoint { - t.Errorf("BaseEndpoint = %v, want %v", got.BaseEndpoint, tt.wantEndpoint) - } - if got.Region != tt.wantRegion { - t.Errorf("Region = %v, want %v", got.Region, tt.wantRegion) - } - if got.UsePathStyle != tt.wantPathStyle { - t.Errorf("UsePathStyle = %v, want %v", got.UsePathStyle, tt.wantPathStyle) - } - }) - } -} - -func TestWithFlyEndpoint(t *testing.T) { - o := &Options{} - WithFlyEndpoint()(o) - - if o.BaseEndpoint != "https://fly.storage.tigris.dev" { - t.Errorf("BaseEndpoint = %v, want https://fly.storage.tigris.dev", o.BaseEndpoint) - } -} - -func TestWithGlobalEndpoint(t *testing.T) { - o := &Options{} - WithGlobalEndpoint()(o) - - if o.BaseEndpoint != "https://t3.storage.dev" { - t.Errorf("BaseEndpoint = %v, want https://t3.storage.dev", o.BaseEndpoint) - } -} - -func TestWithRegion(t *testing.T) { - tests := []struct { - name string - region string - }{ - {"auto region", "auto"}, - {"us-west-2", "us-west-2"}, - {"eu-central-1", "eu-central-1"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := &Options{} - WithRegion(tt.region)(o) - - if o.Region != tt.region { - t.Errorf("Region = %v, want %v", o.Region, tt.region) - } - }) - } -} - -func TestWithAccessKeypair(t *testing.T) { - o := &Options{} - accessKeyID := "test-access-key" - secretAccessKey := "test-secret-key" - - WithAccessKeypair(accessKeyID, secretAccessKey)(o) - - if o.AccessKeyID != accessKeyID { - t.Errorf("AccessKeyID = %v, want %v", o.AccessKeyID, accessKeyID) - } - if o.SecretAccessKey != secretAccessKey { - t.Errorf("SecretAccessKey = %v, want %v", o.SecretAccessKey, secretAccessKey) - } -} - -func TestWithAccessKeypair_overrides(t *testing.T) { - o := &Options{ - AccessKeyID: "old-key", - SecretAccessKey: "old-secret", - } - - WithAccessKeypair("new-key", "new-secret")(o) - - if o.AccessKeyID != "new-key" { - t.Errorf("AccessKeyID = %v, want new-key", o.AccessKeyID) - } - if o.SecretAccessKey != "new-secret" { - t.Errorf("SecretAccessKey = %v, want new-secret", o.SecretAccessKey) - } -} - -func TestOptions_functionalOptions(t *testing.T) { - tests := []struct { - name string - options []Option - want Options - }{ - { - name: "no options uses defaults", - options: nil, - want: Options{ - BaseEndpoint: "https://t3.storage.dev", - Region: "auto", - UsePathStyle: false, - }, - }, - { - name: "fly endpoint", - options: []Option{ - WithFlyEndpoint(), - }, - want: Options{ - BaseEndpoint: "https://fly.storage.tigris.dev", - Region: "auto", - UsePathStyle: false, - }, - }, - { - name: "global endpoint (explicit)", - options: []Option{ - WithGlobalEndpoint(), - }, - want: Options{ - BaseEndpoint: "https://t3.storage.dev", - Region: "auto", - UsePathStyle: false, - }, - }, - { - name: "custom region", - options: []Option{ - WithRegion("us-west-2"), - }, - want: Options{ - BaseEndpoint: "https://t3.storage.dev", - Region: "us-west-2", - UsePathStyle: false, - }, - }, - { - name: "with credentials", - options: []Option{ - WithAccessKeypair("key-id", "secret"), - }, - want: Options{ - BaseEndpoint: "https://t3.storage.dev", - Region: "auto", - UsePathStyle: false, - AccessKeyID: "key-id", - SecretAccessKey: "secret", - }, - }, - { - name: "multiple options", - options: []Option{ - WithFlyEndpoint(), - WithRegion("eu-central-1"), - WithAccessKeypair("key", "secret"), - }, - want: Options{ - BaseEndpoint: "https://fly.storage.tigris.dev", - Region: "eu-central-1", - UsePathStyle: false, - AccessKeyID: "key", - SecretAccessKey: "secret", - }, - }, - { - name: "last option wins for endpoint", - options: []Option{ - WithFlyEndpoint(), - WithGlobalEndpoint(), - }, - want: Options{ - BaseEndpoint: "https://t3.storage.dev", - Region: "auto", - UsePathStyle: false, - }, - }, - { - name: "last option wins for region", - options: []Option{ - WithRegion("us-west-2"), - WithRegion("eu-central-1"), - }, - want: Options{ - BaseEndpoint: "https://t3.storage.dev", - Region: "eu-central-1", - UsePathStyle: false, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := new(Options).defaults() - - for _, opt := range tt.options { - opt(&o) - } - - if o.BaseEndpoint != tt.want.BaseEndpoint { - t.Errorf("BaseEndpoint = %v, want %v", o.BaseEndpoint, tt.want.BaseEndpoint) - } - if o.Region != tt.want.Region { - t.Errorf("Region = %v, want %v", o.Region, tt.want.Region) - } - if o.UsePathStyle != tt.want.UsePathStyle { - t.Errorf("UsePathStyle = %v, want %v", o.UsePathStyle, tt.want.UsePathStyle) - } - if o.AccessKeyID != tt.want.AccessKeyID { - t.Errorf("AccessKeyID = %v, want %v", o.AccessKeyID, tt.want.AccessKeyID) - } - if o.SecretAccessKey != tt.want.SecretAccessKey { - t.Errorf("SecretAccessKey = %v, want %v", o.SecretAccessKey, tt.want.SecretAccessKey) - } - }) - } -} - -func TestNew_createsClient(t *testing.T) { - // This test verifies that New() creates a valid client structure - // It doesn't actually connect to Tigris, just checks initialization - - ctx := context.Background() - - t.Run("with default options", func(t *testing.T) { - client, err := New(ctx) - if err != nil { - t.Fatalf("New() failed: %v", err) - } - if client == nil { - t.Fatal("New() returned nil client") - } - if client.Client == nil { - t.Fatal("New() returned client with nil S3 client") - } - }) - - t.Run("with fly endpoint", func(t *testing.T) { - client, err := New(ctx, WithFlyEndpoint()) - if err != nil { - t.Fatalf("New() failed: %v", err) - } - if client == nil { - t.Fatal("New() returned nil client") - } - }) -} - -func TestNew_withOptions(t *testing.T) { - ctx := context.Background() - - t.Run("with custom region", func(t *testing.T) { - client, err := New(ctx, WithRegion("us-west-2")) - if err != nil { - t.Fatalf("New() failed: %v", err) - } - if client == nil { - t.Fatal("New() returned nil client") - } - if client.Client == nil { - t.Fatal("New() returned client with nil S3 client") - } - }) - - t.Run("with credentials", func(t *testing.T) { - client, err := New(ctx, WithAccessKeypair("test-key", "test-secret")) - if err != nil { - t.Fatalf("New() failed: %v", err) - } - if client == nil { - t.Fatal("New() returned nil client") - } - if client.Client == nil { - t.Fatal("New() returned client with nil S3 client") - } - }) -} - -// MockS3Client is a mock implementation for testing -type MockS3Client struct { - *s3.Client -} - -func (m *MockS3Client) Close() error { - return nil -} - -// Test that Client wraps the S3 client correctly -func TestClient_wrapsS3Client(t *testing.T) { - s3Client := &s3.Client{} - client := &Client{Client: s3Client} - - if client.Client != s3Client { - t.Error("Client.Client is not the provided S3 client") - } -} \ No newline at end of file diff --git a/go/tigrisheaders/example_test.go b/go/tigrisheaders/example_test.go deleted file mode 100644 index 96f2f14..0000000 --- a/go/tigrisheaders/example_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package tigrisheaders_test - -import ( - "bytes" - "context" - "log" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/s3" - storage "github.com/tigrisdata/storage/go" - "github.com/tigrisdata/storage/go/tigrisheaders" -) - -var client *storage.Client -var ctx context.Context -var data []byte - -func ExampleWithStaticReplicationRegions() { - // Replicate to specific regions - _, err := client.PutObject(ctx, &s3.PutObjectInput{ - Bucket: aws.String("my-bucket"), - Key: aws.String("file.txt"), - Body: bytes.NewReader(data), - }, tigrisheaders.WithStaticReplicationRegions([]tigrisheaders.Region{ - tigrisheaders.FRA, // Frankfurt - tigrisheaders.SJC, // San Jose - })) - if err != nil { - log.Fatal(err) - } -} - -func ExampleWithQuery() { - // Filter objects with a SQL-like WHERE clause - _, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ - Bucket: aws.String("my-bucket"), - }, tigrisheaders.WithQuery("WHERE `Last-Modified` > \"2023-01-15T08:30:00Z\" AND `Content-Type` = \"text/javascript\"")) - if err != nil { - log.Fatal(err) - } -} - -func ExampleWithCreateObjectIfNotExists() { - // Create object only if it doesn't exist - _, err := client.PutObject(ctx, &s3.PutObjectInput{}, - tigrisheaders.WithCreateObjectIfNotExists()) - if err != nil { - log.Fatal(err) - } -} - -func ExampleWithIfEtagMatches() { - // Only proceed if ETag matches - _, err := client.PutObject(ctx, &s3.PutObjectInput{}, - tigrisheaders.WithIfEtagMatches(`"abc123"`)) - if err != nil { - log.Fatal(err) - } -} - -func ExampleWithModifiedSince() { - // Only proceed if modified since date - _, err := client.GetObject(ctx, &s3.GetObjectInput{}, - tigrisheaders.WithModifiedSince(time.Now().Add(-24*time.Hour))) - if err != nil { - log.Fatal(err) - } -} - -func ExampleWithUnmodifiedSince() { - // Only proceed if unmodified since date - _, err := client.GetObject(ctx, &s3.GetObjectInput{}, - tigrisheaders.WithUnmodifiedSince(time.Now().Add(-24*time.Hour))) - if err != nil { - log.Fatal(err) - } -} - -func ExampleWithCompareAndSwap() { - // Compare-and-swap (skip cache, read from designated region) - _, err := client.GetObject(ctx, &s3.GetObjectInput{}, - tigrisheaders.WithCompareAndSwap()) - if err != nil { - log.Fatal(err) - } -} - -func ExampleWithSnapshotVersion() { - // List objects from a specific snapshot - _, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ - Bucket: aws.String("my-bucket"), - }, tigrisheaders.WithSnapshotVersion("snapshot-id")) - if err != nil { - log.Fatal(err) - } - - // Get object from a specific snapshot - _, err = client.GetObject(ctx, &s3.GetObjectInput{ - Bucket: aws.String("my-bucket"), - Key: aws.String("file.txt"), - }, tigrisheaders.WithSnapshotVersion("snapshot-id")) - if err != nil { - log.Fatal(err) - } -} - -func ExampleWithHeader() { - // Set arbitrary HTTP header on request - _, err := client.PutObject(ctx, &s3.PutObjectInput{}, - tigrisheaders.WithHeader("X-Custom-Header", "value")) - if err != nil { - log.Fatal(err) - } -} diff --git a/go/tigrisheaders/tigrisheaders.go b/go/tigrisheaders/tigrisheaders.go deleted file mode 100644 index b4ca840..0000000 --- a/go/tigrisheaders/tigrisheaders.go +++ /dev/null @@ -1,151 +0,0 @@ -// Package tigrisheaders contains Tigris-specific header helpers for the AWS S3 SDK and helpers for interacting with Tigris. -// -// Tigris is a cloud storage service that provides a simple, scalable, and secure object storage solution. It is based on the S3 API, but has additional features that need these helpers. -package tigrisheaders - -import ( - "fmt" - "net/url" - "strings" - "time" - - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/aws/smithy-go/transport/http" -) - -// WithHeader sets an arbitrary HTTP header on the request. -func WithHeader(key, value string) func(*s3.Options) { - return func(options *s3.Options) { - options.APIOptions = append(options.APIOptions, http.AddHeaderValue(key, value)) - } -} - -// Region is a Tigris region from the documentation. -// -// https://www.tigrisdata.com/docs/concepts/regions/ -type Region string - -// Possible Tigris regions. -const ( - FRA Region = "fra" // Frankfurt, Germany - GRU Region = "gru" // São Paulo, Brazil - HKG Region = "hkg" // Hong Kong, China - IAD Region = "iad" // Ashburn, Virginia, USA - JNB Region = "jnb" // Johannesburg, South Africa - LHR Region = "lhr" // London, UK - MAD Region = "mad" // Madrid, Spain - NRT Region = "nrt" // Tokyo (Narita), Japan - ORD Region = "ord" // Chicago, Illinois, USA - SIN Region = "sin" // Singapore - SJC Region = "sjc" // San Jose, California, USA - SYD Region = "syd" // Sydney, Australia - - Europe Region = "eur" // European datacenters - USA Region = "usa" // American datacenters -) - -// WithStaticReplicationRegions sets the regions where the object will be replicated. -// -// Note that this will cause you to be charged multiple times for the same object, once per region. -func WithStaticReplicationRegions(regions []Region) func(*s3.Options) { - regionsString := make([]string, 0, len(regions)) - for _, r := range regions { - regionsString = append(regionsString, string(r)) - } - - return WithHeader("X-Tigris-Regions", strings.Join(regionsString, ",")) -} - -// WithQuery lets you filter objects in a ListObjectsV2 request. -// -// This functions like the WHERE clause in SQL, but for S3 objects. For more information, see the Tigris documentation[1]. -// -// [1]: https://www.tigrisdata.com/docs/objects/query-metadata/ -func WithQuery(query string) func(*s3.Options) { - return WithHeader("X-Tigris-Query", query) -} - -// WithCreateObjectIfNotExists will create the object if it doesn't exist. -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/objects/conditionals/ -func WithCreateObjectIfNotExists() func(*s3.Options) { - return WithHeader("If-Match", `""`) -} - -// WithIfEtagMatches sets the ETag that the object must match. -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/objects/conditionals/ -func WithIfEtagMatches(etag string) func(*s3.Options) { - return WithHeader("If-Match", etag) -} - -// WithModifiedSince lets you proceed with operation if object was modified after provided date (RFC1123). -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/objects/conditionals/ -func WithModifiedSince(modifiedSince time.Time) func(*s3.Options) { - return WithHeader("If-Modified-Since", modifiedSince.Format(time.RFC1123)) -} - -// WithUnmodifiedSince lets you proceed with operation if object was not modified after provided date (RFC1123). -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/objects/conditionals/ -func WithUnmodifiedSince(unmodifiedSince time.Time) func(*s3.Options) { - return WithHeader("If-Unmodified-Since", unmodifiedSince.Format(time.RFC1123)) -} - -// WithCompareAndSwap tells Tigris to skip the cache and read the object from its designated region. -// -// This is only used on GET requests. -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/objects/conditionals/ -func WithCompareAndSwap() func(*s3.Options) { - return WithHeader("X-Tigris-CAS", "true") -} - -// WithEnableSnapshot tells Tigris to enable bucket snapshotting when creating buckets. -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/#enabling-snapshots-and-forks -func WithEnableSnapshot() func(*s3.Options) { - return WithHeader("X-Tigris-Enable-Snapshot", "true") -} - -// WithTakeSnapshot tells Tigris to create a snapshot with the given description on a forkable bucket. -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/#creating-a-snapshot -func WithTakeSnapshot(desc string) func(*s3.Options) { - return WithHeader("X-Tigris-Snapshot", fmt.Sprintf("true; name=%s", url.QueryEscape(desc))) -} - -// WithSnapshotVersion tells Tigris to use a given snapshot when doing ListObjectsV2, GetObject, or HeadObject calls. -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/buckets/snapshots-and-forks/#listing-and-retrieving-objects-from-a-snapshot -func WithSnapshotVersion(snapshotVersion string) func(*s3.Options) { - return WithHeader("X-Tigris-Snapshot-Version", snapshotVersion) -} - -// WithRename tells Tigris to do an in-place rename of objects instead of copying them when using a CopyObject call. -// -// See the Tigris documentation[1] for more information. -// -// [1]: https://www.tigrisdata.com/docs/objects/object-rename/#renaming-objects-using-aws-sdks -func WithRename() func(*s3.Options) { - return func(options *s3.Options) { - options.APIOptions = append(options.APIOptions, http.AddHeaderValue("X-Tigris-Rename", "true")) - } -} diff --git a/go/tigrisheaders/tigrisheaders_test.go b/go/tigrisheaders/tigrisheaders_test.go deleted file mode 100644 index ff3fdfe..0000000 --- a/go/tigrisheaders/tigrisheaders_test.go +++ /dev/null @@ -1,540 +0,0 @@ -package tigrisheaders - -import ( - "strings" - "testing" - "time" - - "github.com/aws/aws-sdk-go-v2/service/s3" -) - -// Test that the header functions return valid option functions -func TestHeaderFunctionsAreValid(t *testing.T) { - tests := []struct { - name string - apply func(*s3.Options) - }{ - {"WithHeader", func(o *s3.Options) { WithHeader("X-Test", "value")(o) }}, - {"WithStaticReplicationRegions", func(o *s3.Options) { WithStaticReplicationRegions([]Region{FRA, SJC})(o) }}, - {"WithQuery", func(o *s3.Options) { WithQuery("WHERE key = 'value'")(o) }}, - {"WithCreateObjectIfNotExists", func(o *s3.Options) { WithCreateObjectIfNotExists()(o) }}, - {"WithIfEtagMatches", func(o *s3.Options) { WithIfEtagMatches(`"abc"`)(o) }}, - {"WithModifiedSince", func(o *s3.Options) { WithModifiedSince(time.Now())(o) }}, - {"WithUnmodifiedSince", func(o *s3.Options) { WithUnmodifiedSince(time.Now())(o) }}, - {"WithCompareAndSwap", func(o *s3.Options) { WithCompareAndSwap()(o) }}, - {"WithEnableSnapshot", func(o *s3.Options) { WithEnableSnapshot()(o) }}, - {"WithTakeSnapshot", func(o *s3.Options) { WithTakeSnapshot("test")(o) }}, - {"WithSnapshotVersion", func(o *s3.Options) { WithSnapshotVersion("v1")(o) }}, - {"WithRename", func(o *s3.Options) { WithRename()(o) }}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - // Should not panic - tt.apply(opts) - // Should add to APIOptions - if len(opts.APIOptions) == 0 { - t.Errorf("%s() did not add any APIOptions", tt.name) - } - }) - } -} - -func TestWithStaticReplicationRegions_formatting(t *testing.T) { - tests := []struct { - name string - regions []Region - want string - }{ - { - name: "single region", - regions: []Region{FRA}, - want: "fra", - }, - { - name: "multiple regions", - regions: []Region{FRA, SJC, LHR}, - want: "fra,sjc,lhr", - }, - { - name: "all specific regions", - regions: []Region{FRA, GRU, HKG, IAD, JNB, LHR, MAD, NRT, ORD, SIN, SJC, SYD}, - want: "fra,gru,hkg,iad,jnb,lhr,mad,nrt,ord,sin,sjc,syd", - }, - { - name: "aggregate regions", - regions: []Region{Europe, USA}, - want: "eur,usa", - }, - { - name: "mixed aggregate and specific", - regions: []Region{FRA, Europe, SJC, USA}, - want: "fra,eur,sjc,usa", - }, - { - name: "empty regions", - regions: []Region{}, - want: "", - }, - { - name: "single aggregate", - regions: []Region{Europe}, - want: "eur", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - WithStaticReplicationRegions(tt.regions)(opts) - - if len(opts.APIOptions) == 0 { - t.Fatal("WithStaticReplicationRegions() did not add any APIOptions") - } - }) - } -} - -func TestWithQuery_variousInputs(t *testing.T) { - tests := []struct { - name string - query string - }{ - { - name: "simple query", - query: "WHERE `key` = 'value'", - }, - { - name: "complex query", - query: "WHERE `Last-Modified` > \"2023-01-15T08:30:00Z\" AND `Content-Type` = \"text/javascript\"", - }, - { - name: "empty query", - query: "", - }, - { - name: "query with special characters", - query: "WHERE `name` LIKE '%test%' AND `size` > 1024", - }, - { - name: "query with newlines", - query: "WHERE `key` = 'value'\nAND `other` > 5", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - WithQuery(tt.query)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithQuery() did not add any APIOptions") - } - }) - } -} - -func TestWithIfEtagMatches_variousInputs(t *testing.T) { - tests := []struct { - name string - etag string - }{ - {"simple etag", `"abc123"`}, - {"quoted etag", `"d41d8cd98f00b204e9800998ecf8427e"`}, - {"empty quotes", `""`}, - {"etag with hyphens", `"abc-123-def"`}, - {"etag with numbers", `"123456789"`}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - WithIfEtagMatches(tt.etag)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithIfEtagMatches() did not add any APIOptions") - } - }) - } -} - -func TestWithModifiedSince_formats(t *testing.T) { - times := []struct { - name string - t time.Time - }{ - { - name: "2023-01-01", - t: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), - }, - { - name: "2023-12-31 end of day", - t: time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC), - }, - { - name: "2024-06-15 midday", - t: time.Date(2024, 6, 15, 12, 30, 45, 0, time.UTC), - }, - { - name: "unix epoch", - t: time.Unix(0, 0).UTC(), - }, - { - name: "with nanoseconds", - t: time.Date(2023, 5, 15, 10, 30, 0, 123456789, time.UTC), - }, - } - - for _, tt := range times { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - WithModifiedSince(tt.t)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithModifiedSince() did not add any APIOptions") - } - }) - } -} - -func TestWithUnmodifiedSince_formats(t *testing.T) { - times := []struct { - name string - t time.Time - }{ - { - name: "2023-01-01", - t: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), - }, - { - name: "2023-12-31 end of day", - t: time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC), - }, - { - name: "unix epoch", - t: time.Unix(0, 0).UTC(), - }, - } - - for _, tt := range times { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - WithUnmodifiedSince(tt.t)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithUnmodifiedSince() did not add any APIOptions") - } - }) - } -} - -func TestWithTakeSnapshot_variousDescriptions(t *testing.T) { - tests := []struct { - name string - description string - }{ - { - name: "simple description", - description: "Initial backup", - }, - { - name: "description with spaces", - description: "Backup before migration", - }, - { - name: "description with special chars", - description: "Backup: v1.0.0 (final)", - }, - { - name: "empty description", - description: "", - }, - { - name: "unicode description", - description: "备份 before deployment", - }, - { - name: "description with semicolons", - description: "backup;version;1.0", - }, - { - name: "description with equals", - description: "desc=test backup", - }, - { - name: "very long description", - description: strings.Repeat("a", 500), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - WithTakeSnapshot(tt.description)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithTakeSnapshot() did not add any APIOptions") - } - }) - } -} - -func TestWithSnapshotVersion_variousInputs(t *testing.T) { - tests := []struct { - name string - version string - }{ - {"simple version", "snapshot-id"}, - {"uuid-like version", "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}, - {"numeric version", "12345"}, - {"version with hyphens", "snap-2023-01-15"}, - {"empty version", ""}, - {"version with underscores", "snapshot_v1_0"}, - {"version with dots", "v1.0.0"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - WithSnapshotVersion(tt.version)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithSnapshotVersion() did not add any APIOptions") - } - }) - } -} - -func TestRegionConstants(t *testing.T) { - tests := []struct { - region Region - want string - }{ - {FRA, "fra"}, - {GRU, "gru"}, - {HKG, "hkg"}, - {IAD, "iad"}, - {JNB, "jnb"}, - {LHR, "lhr"}, - {MAD, "mad"}, - {NRT, "nrt"}, - {ORD, "ord"}, - {SIN, "sin"}, - {SJC, "sjc"}, - {SYD, "syd"}, - {Europe, "eur"}, - {USA, "usa"}, - } - - for _, tt := range tests { - t.Run(tt.want, func(t *testing.T) { - if string(tt.region) != tt.want { - t.Errorf("Region %s = %q, want %q", tt.region, tt.region, tt.want) - } - }) - } -} - -// Test that multiple header functions can be composed -func TestMultipleHeadersCanBeComposed(t *testing.T) { - opts := &s3.Options{} - - initialCount := len(opts.APIOptions) - - WithHeader("X-Header-1", "value1")(opts) - WithHeader("X-Header-2", "value2")(opts) - WithStaticReplicationRegions([]Region{FRA, SJC})(opts) - WithCompareAndSwap()(opts) - - if len(opts.APIOptions) <= initialCount { - t.Error("Multiple header functions did not add APIOptions") - } - - // Each call should add one APIOption - expectedAdded := 4 - if len(opts.APIOptions)-initialCount != expectedAdded { - t.Errorf("Expected %d APIOptions to be added, got %d", expectedAdded, len(opts.APIOptions)-initialCount) - } -} - -// Test header function with nil options - these are expected to panic -// The functions require non-nil *s3.Options -func TestHeaderFunctionsWithNilOptions(t *testing.T) { - tests := []struct { - name string - apply func(*s3.Options) - }{ - {"WithHeader", func(o *s3.Options) { WithHeader("X-Test", "value")(o) }}, - {"WithQuery", func(o *s3.Options) { WithQuery("test")(o) }}, - {"WithCompareAndSwap", func(o *s3.Options) { WithCompareAndSwap()(o) }}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - defer func() { - // These functions are expected to panic with nil options - if r := recover(); r == nil { - t.Errorf("%s(nil) did not panic, expected panic with nil options", tt.name) - } - }() - tt.apply(nil) - }) - } -} - -// Test WithHeader with various key/value combinations -func TestWithHeader_variousInputs(t *testing.T) { - tests := []struct { - name string - key string - value string - }{ - {"standard header", "X-Custom-Header", "value"}, - {"header with numbers", "X-Header-123", "value456"}, - {"header with hyphens", "X-My-Custom-Header", "my-value"}, - {"empty value", "X-Empty", ""}, - {"value with spaces", "X-Spaced", "value with spaces"}, - {"value with special chars", "X-Special", "value:with;special=chars"}, - {"unicode key", "X-Unicode", "你好"}, - {"unicode value", "X-Header", "мир"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := &s3.Options{} - WithHeader(tt.key, tt.value)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithHeader() did not add any APIOptions") - } - }) - } -} - -// Test that Region type works as expected -func TestRegionType(t *testing.T) { - // Test that Region is a string type - var r Region = "test-region" - if string(r) != "test-region" { - t.Errorf("Region type conversion failed: got %q", string(r)) - } - - // Test that Region constants can be used in slices - regions := []Region{FRA, SJC, LHR} - if len(regions) != 3 { - t.Errorf("Region slice length: got %d, want 3", len(regions)) - } - - // Test that Region can be compared - if FRA != "fra" { - t.Errorf("Region comparison failed: FRA = %q, want %q", FRA, "fra") - } -} - -// Test that functions can be called directly -func TestFunctionsCanBeCalled(t *testing.T) { - opts := &s3.Options{} - - // These should all work without panicking - WithHeader("X-Test", "value")(opts) - WithQuery("test")(opts) - WithCompareAndSwap()(opts) - WithEnableSnapshot()(opts) - WithRename()(opts) - - // Verify options were added - if len(opts.APIOptions) != 5 { - t.Errorf("Expected 5 APIOptions, got %d", len(opts.APIOptions)) - } -} - -// Test time formatting edge cases -func TestTimeFormattingEdgeCases(t *testing.T) { - tests := []struct { - name string - t time.Time - }{ - { - name: "zero time", - t: time.Time{}, - }, - { - name: "far future", - t: time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC), - }, - { - name: "far past", - t: time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC), - }, - { - name: "with monotonic clock", - t: time.Now().Add(time.Hour), - }, - } - - for _, tt := range tests { - t.Run(tt.name+" modified since", func(t *testing.T) { - opts := &s3.Options{} - WithModifiedSince(tt.t)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithModifiedSince() did not add any APIOptions") - } - }) - - t.Run(tt.name+" unmodified since", func(t *testing.T) { - opts := &s3.Options{} - WithUnmodifiedSince(tt.t)(opts) - - if len(opts.APIOptions) == 0 { - t.Error("WithUnmodifiedSince() did not add any APIOptions") - } - }) - } -} - -// Benchmark tests for header creation -func BenchmarkWithHeader(b *testing.B) { - opts := &s3.Options{} - b.ResetTimer() - for i := 0; i < b.N; i++ { - WithHeader("X-Test", "value")(opts) - } -} - -func BenchmarkWithStaticReplicationRegions(b *testing.B) { - regions := []Region{FRA, SJC, LHR, NRT, SYD} - b.ResetTimer() - for i := 0; i < b.N; i++ { - opts := &s3.Options{} - WithStaticReplicationRegions(regions)(opts) - } -} - -func BenchmarkWithQuery(b *testing.B) { - query := "WHERE `Last-Modified` > \"2023-01-15T08:30:00Z\" AND `Content-Type` = \"text/javascript\"" - b.ResetTimer() - for i := 0; i < b.N; i++ { - opts := &s3.Options{} - WithQuery(query)(opts) - } -} - -func BenchmarkWithModifiedSince(b *testing.B) { - t := time.Date(2023, 1, 15, 8, 30, 0, 0, time.UTC) - b.ResetTimer() - for i := 0; i < b.N; i++ { - opts := &s3.Options{} - WithModifiedSince(t)(opts) - } -} - -func BenchmarkWithTakeSnapshot(b *testing.B) { - desc := "Backup before migration" - b.ResetTimer() - for i := 0; i < b.N; i++ { - opts := &s3.Options{} - WithTakeSnapshot(desc)(opts) - } -} \ No newline at end of file From bccadc46cd556f856498176df0e3c3ed51c501ee Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 14 Jan 2026 15:02:30 -0500 Subject: [PATCH 2/5] docs: add Go SDK installation instructions to README Add installation command and documentation reference for the external Go SDK repository. Assisted-by: GLM 4.7 via Claude Code --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 87fd87c..4bc285f 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,11 @@ The Go SDK is now maintained in a separate repository: - Repository: [https://github.com/tigrisdata/storage-go](https://github.com/tigrisdata/storage-go) - Documentation: [https://pkg.go.dev/github.com/tigrisdata/storage-go](https://pkg.go.dev/github.com/tigrisdata/storage-go) + +Install with: + +```sh +go get github.com/tigrisdata/storage-go +``` + +See the documentation for examples and quickstart advice. From 3fe085289048af98a82bc714ccb1c8c04d08caca Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 14 Jan 2026 15:06:31 -0500 Subject: [PATCH 3/5] docs: remove "now" from Go SDK description Assisted-by: GLM 4.7 via Claude Code --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4bc285f..ca18134 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This monorepo contains multiple packages for Tigris object storage: ## Go SDK -The Go SDK is now maintained in a separate repository: +The Go SDK is maintained in a separate repository: - Repository: [https://github.com/tigrisdata/storage-go](https://github.com/tigrisdata/storage-go) - Documentation: [https://pkg.go.dev/github.com/tigrisdata/storage-go](https://pkg.go.dev/github.com/tigrisdata/storage-go) From a422d136a99884a3455da3aba7d6c679ea141200 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 14 Jan 2026 15:07:58 -0500 Subject: [PATCH 4/5] docs: specify JavaScript/TypeScript packages in README Clarify that this monorepo contains JavaScript/TypeScript packages. Assisted-by: GLM 4.7 via Claude Code --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca18134..9ee986a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Tigris is a globally distributed object storage service that provides low latency anywhere in the world, enabling developers to store and access any amount of data for a wide range of use cases. -This monorepo contains multiple packages for Tigris object storage: +This monorepo contains multiple JavaScript/TypeScript packages for Tigris object storage: - [`@tigrisdata/storage`](./packages/storage) - Tigris Storage SDK - [`@tigrisdata/keyv-tigris`](./packages/keyv-tigris) - Tigris adapter for [Keyv](https://keyv.org/) From be55cfcedc69a0ffffe6f278a25ae5f4e439d71c Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 14 Jan 2026 15:09:32 -0500 Subject: [PATCH 5/5] docs: reorganize README with JavaScript/TypeScript and Go SDK sections - Change title to "Tigris Storage SDKs" - Add "JavaScript/TypeScript SDK" section header - Improve structure to clarify multi-language support Assisted-by: GLM 4.7 via Claude Code --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ee986a..f50af98 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -# Tigris Storage SDK +# Tigris Storage SDKs Tigris is a globally distributed object storage service that provides low latency anywhere in the world, enabling developers to store and access any amount of data for a wide range of use cases. +## JavaScript/TypeScript SDK + This monorepo contains multiple JavaScript/TypeScript packages for Tigris object storage: - [`@tigrisdata/storage`](./packages/storage) - Tigris Storage SDK