-
Notifications
You must be signed in to change notification settings - Fork 1
feat(go): add simplestorage package for high-level interactions #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7c4a1b3
59424d7
e4f15c2
5883078
90cd535
18ef571
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,285 @@ | ||||||||||
| package simplestorage | ||||||||||
|
|
||||||||||
| import ( | ||||||||||
| "context" | ||||||||||
| "errors" | ||||||||||
| "fmt" | ||||||||||
| "io" | ||||||||||
| "time" | ||||||||||
|
|
||||||||||
| "github.com/aws/aws-sdk-go-v2/aws" | ||||||||||
| "github.com/aws/aws-sdk-go-v2/service/s3" | ||||||||||
| storage "github.com/tigrisdata/storage/go" | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| // ErrNoBucketName is returned when no bucket name is provided via the | ||||||||||
| // TIGRIS_STORAGE_BUCKET environment variable or the WithBucket option. | ||||||||||
| var ErrNoBucketName = errors.New("bucket name not set: provide the TIGRIS_STORAGE_BUCKET environment variable or use WithBucket option") | ||||||||||
|
|
||||||||||
| // Client is a high-level client for Tigris that simplifies common interactions | ||||||||||
| // to very high level calls. | ||||||||||
| type Client struct { | ||||||||||
| cli *storage.Client | ||||||||||
| options Options | ||||||||||
| } | ||||||||||
|
|
||||||||||
Xe marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
| // ClientOption is a function option that allows callers to override settings in | ||||||||||
| // calls to Tigris via Client. | ||||||||||
| type ClientOption func(*ClientOptions) | ||||||||||
|
|
||||||||||
| // OverrideBucket overrides the bucket used for Tigris calls. | ||||||||||
| func OverrideBucket(bucket string) ClientOption { | ||||||||||
| return func(co *ClientOptions) { | ||||||||||
| co.BucketName = bucket | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // WithS3Options sets S3 options for individual Tigris calls. | ||||||||||
| func WithS3Options(opts ...func(*s3.Options)) ClientOption { | ||||||||||
| return func(co *ClientOptions) { | ||||||||||
| co.S3Options = append(co.S3Options, opts...) | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // WithStartAfter sets the StartAfter setting in List calls. Use this if you need | ||||||||||
| // pagination in your List calls. | ||||||||||
| func WithStartAfter(startAfter string) ClientOption { | ||||||||||
| return func(co *ClientOptions) { | ||||||||||
| co.StartAfter = aws.String(startAfter) | ||||||||||
| } | ||||||||||
| } | ||||||||||
Xe marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| // WithMaxKeys sets the maximum number of keys in List calls. Use this along with | ||||||||||
| // WithStartAfter for pagination in your List calls. | ||||||||||
| func WithMaxKeys(maxKeys int32) ClientOption { | ||||||||||
| return func(co *ClientOptions) { | ||||||||||
| co.MaxKeys = &maxKeys | ||||||||||
| } | ||||||||||
| } | ||||||||||
Xe marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| // ClientOptions is the collection of options that are set for individual Tigris | ||||||||||
| // calls. | ||||||||||
| type ClientOptions struct { | ||||||||||
| BucketName string | ||||||||||
| S3Options []func(*s3.Options) | ||||||||||
|
|
||||||||||
| // List options | ||||||||||
| StartAfter *string | ||||||||||
| MaxKeys *int32 | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // defaults populates client options from the global Options. | ||||||||||
| func (ClientOptions) defaults(o Options) ClientOptions { | ||||||||||
| return ClientOptions{ | ||||||||||
| BucketName: o.BucketName, | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
Xe marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
| // New creates a new Client based on the options provided and defaults loaded from the environment. | ||||||||||
| // | ||||||||||
| // By default New reads the following environment variables for setting its defaults: | ||||||||||
| // | ||||||||||
| // * `TIGRIS_STORAGE_BUCKET`: the name of the bucket for all Tigris operations. If this is not set in the environment or via the WithBucket, New() will return an error containing ErrNoBucketName. | ||||||||||
| // * `TIGRIS_STORAGE_ACCESS_KEY_ID`: The access key ID of the Tigris authentication keypair. If this is not set in the environment or via WithAccessKeypair, New() will load configuration via the AWS configuration resolution method. | ||||||||||
| // * `TIGRIS_STORAGE_SECRET_ACCESS_KEY`: The secret access key of the Tigris authentication keypair. If this is not set in the environment or via WithAccessKeypair, New() will load configuration via the AWS configuration resolution method. | ||||||||||
| // | ||||||||||
| // The returned Client will default to having its operations performed on the specified bucket. If | ||||||||||
| // individual calls need to operate against arbitrary buckets, override it with OverrideBucket. | ||||||||||
| func New(ctx context.Context, options ...Option) (*Client, error) { | ||||||||||
| o := new(Options).defaults() | ||||||||||
|
|
||||||||||
| for _, doer := range options { | ||||||||||
| doer(&o) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| var errs []error | ||||||||||
| if o.BucketName == "" { | ||||||||||
| errs = append(errs, ErrNoBucketName) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if len(errs) != 0 { | ||||||||||
| return nil, fmt.Errorf("simplestorage: can't create client: %w", errors.Join(errs...)) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| var storageOpts []storage.Option | ||||||||||
|
|
||||||||||
| if o.BaseEndpoint != storage.GlobalEndpoint { | ||||||||||
| storageOpts = append(storageOpts, storage.WithEndpoint(o.BaseEndpoint)) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| storageOpts = append(storageOpts, storage.WithRegion(o.Region)) | ||||||||||
| storageOpts = append(storageOpts, storage.WithPathStyle(o.UsePathStyle)) | ||||||||||
|
|
||||||||||
| if o.AccessKeyID != "" && o.SecretAccessKey != "" { | ||||||||||
| storageOpts = append(storageOpts, storage.WithAccessKeypair(o.AccessKeyID, o.SecretAccessKey)) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| cli, err := storage.New(ctx, storageOpts...) | ||||||||||
| if err != nil { | ||||||||||
| return nil, fmt.Errorf("simplestorage: can't create storage client: %w", err) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return &Client{ | ||||||||||
| cli: cli, | ||||||||||
| options: o, | ||||||||||
| }, nil | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Object contains metadata about an individual object read from or put into Tigris. | ||||||||||
| // | ||||||||||
| // Some calls may not populate all fields. Ensure that the values are valid before | ||||||||||
| // consuming them. | ||||||||||
| type Object struct { | ||||||||||
| Bucket string // Bucket the object is in | ||||||||||
| Key string // Key for the object | ||||||||||
| ContentType string // MIME type for the object or application/octet-stream | ||||||||||
| Etag string // Entity tag for the object (usually a checksum) | ||||||||||
| Version string // Version tag for the object | ||||||||||
| Size int64 // Size of the object in bytes or 0 if unknown | ||||||||||
| LastModified time.Time // Creation date of the object | ||||||||||
| Body io.ReadCloser // Body of the object so it can be read, don't forget to close it. | ||||||||||
| } | ||||||||||
Xe marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
Xe marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
| // Get fetches the contents of an object and its metadata from Tigris. | ||||||||||
| func (c *Client) Get(ctx context.Context, key string, opts ...ClientOption) (*Object, error) { | ||||||||||
| o := new(ClientOptions).defaults(c.options) | ||||||||||
|
|
||||||||||
| for _, doer := range opts { | ||||||||||
| doer(&o) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| resp, err := c.cli.GetObject( | ||||||||||
| ctx, | ||||||||||
| &s3.GetObjectInput{ | ||||||||||
| Bucket: aws.String(o.BucketName), | ||||||||||
| Key: aws.String(key), | ||||||||||
| }, | ||||||||||
| o.S3Options..., | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| if err != nil { | ||||||||||
| return nil, fmt.Errorf("simplestorage: can't get %s/%s: %v", o.BucketName, key, err) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return &Object{ | ||||||||||
| Bucket: o.BucketName, | ||||||||||
| Key: key, | ||||||||||
| ContentType: lower(resp.ContentType, "application/octet-stream"), | ||||||||||
| Etag: lower(resp.ETag, ""), | ||||||||||
| Size: lower(resp.ContentLength, 0), | ||||||||||
| Version: lower(resp.VersionId, ""), | ||||||||||
| LastModified: lower(resp.LastModified, time.Time{}), | ||||||||||
| Body: resp.Body, | ||||||||||
| }, nil | ||||||||||
| } | ||||||||||
|
|
||||||||||
Xe marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
| // Put puts the contents of an object into Tigris. | ||||||||||
| func (c *Client) Put(ctx context.Context, obj *Object, opts ...ClientOption) (*Object, error) { | ||||||||||
| o := new(ClientOptions).defaults(c.options) | ||||||||||
|
|
||||||||||
| for _, doer := range opts { | ||||||||||
| doer(&o) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| resp, err := c.cli.PutObject( | ||||||||||
| ctx, | ||||||||||
| &s3.PutObjectInput{ | ||||||||||
| Bucket: aws.String(o.BucketName), | ||||||||||
| Key: aws.String(obj.Key), | ||||||||||
| Body: obj.Body, | ||||||||||
| ContentType: raise(obj.ContentType), | ||||||||||
| ContentLength: raise(obj.Size), | ||||||||||
| }, | ||||||||||
| o.S3Options..., | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| if err != nil { | ||||||||||
| return nil, fmt.Errorf("simplestorage: can't put %s/%s: %v", o.BucketName, obj.Key, err) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| obj.Bucket = o.BucketName | ||||||||||
| obj.Etag = *resp.ETag | ||||||||||
| obj.Version = *resp.VersionId | ||||||||||
Xe marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+201
to
+202
|
||||||||||
| obj.Etag = *resp.ETag | |
| obj.Version = *resp.VersionId | |
| obj.Etag = lower(resp.ETag, "") | |
| obj.Version = lower(resp.VersionId, "") |
Xe marked this conversation as resolved.
Show resolved
Hide resolved
Xe marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential nil pointer dereference. The code directly dereferences obj.Key without checking if it's nil. Consider using the lower helper function to safely handle potentially nil values: Key: lower(obj.Key, "").
| Key: *obj.Key, | |
| Key: lower(obj.Key, ""), |
Copilot
AI
Jan 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Get, Put, Delete, and List methods lack test coverage. Since the repository has comprehensive testing for other packages (as seen in storage_test.go), these new public methods should also have tests to ensure correctness and maintain the project's testing standards.
Uh oh!
There was an error while loading. Please reload this page.