Skip to content

Commit 65c55db

Browse files
committed
internal/s3util: add a client wrapper for common operations
1 parent 7289605 commit 65c55db

File tree

1 file changed

+95
-0
lines changed

1 file changed

+95
-0
lines changed

internal/s3util/s3util.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
"fmt"
1010
"hash"
1111
"io"
12+
"io/fs"
1213
"os"
1314

1415
"github.com/aws/aws-sdk-go-v2/config"
1516
"github.com/aws/aws-sdk-go-v2/service/s3"
1617
"github.com/aws/aws-sdk-go-v2/service/s3/types"
18+
"github.com/creachadair/mds/value"
1719
)
1820

1921
// IsNotExist reports whether err is an error indicating the requested resource
@@ -69,3 +71,96 @@ func (e ETagReader) Read(data []byte) (int, error) { return e.r.Read(data) }
6971
// ETag returns a correctly-formatted S3 etag for the contents of e that have
7072
// been read so far.
7173
func (e ETagReader) ETag() string { return fmt.Sprintf("%x", e.hash.Sum(nil)) }
74+
75+
// Client is a wrapper for an S3 client that provides basic read and write
76+
// facilities to a specific bucket.
77+
type Client struct {
78+
Client *s3.Client
79+
Bucket string
80+
}
81+
82+
// Put writes the specified data to S3 under the given key.
83+
func (c *Client) Put(ctx context.Context, key string, data io.Reader) error {
84+
// Attempt to find the size of the input to send as a content length.
85+
// If we can't do this, let the SDK figure it out.
86+
var sizePtr *int64
87+
switch t := data.(type) {
88+
case sizer:
89+
sizePtr = value.Ptr(t.Size())
90+
case statter:
91+
fi, err := t.Stat()
92+
if err == nil {
93+
sizePtr = value.Ptr(fi.Size())
94+
}
95+
case io.Seeker:
96+
v, err := t.Seek(0, io.SeekEnd)
97+
if err == nil {
98+
sizePtr = &v
99+
100+
// Try to seek back to the beginning. If we cannot do this, fail out
101+
// so we don't try to write a partial object.
102+
_, err = t.Seek(0, io.SeekStart)
103+
if err != nil {
104+
return fmt.Errorf("[unexpected] seek failed: %w", err)
105+
}
106+
}
107+
}
108+
_, err := c.Client.PutObject(ctx, &s3.PutObjectInput{
109+
Bucket: &c.Bucket,
110+
Key: &key,
111+
Body: data,
112+
ContentLength: sizePtr,
113+
})
114+
return err
115+
}
116+
117+
// Get returns the contents of the specified key from S3. On success, the
118+
// returned reader contains the contents of the object, and the caller must
119+
// close the reader when finished.
120+
//
121+
// If the key is not found, the resulting error satisfies [fs.ErrNotExist].
122+
func (c *Client) Get(ctx context.Context, key string) (io.ReadCloser, error) {
123+
rsp, err := c.Client.GetObject(ctx, &s3.GetObjectInput{
124+
Bucket: &c.Bucket,
125+
Key: &key,
126+
})
127+
if err != nil {
128+
if IsNotExist(err) {
129+
return nil, fmt.Errorf("key %q: %w", key, fs.ErrNotExist)
130+
}
131+
return nil, err
132+
}
133+
return rsp.Body, nil
134+
}
135+
136+
// GetData returns the contents of the specified key from S3. It is a shorthand
137+
// for calling Get followed by io.ReadAll on the result.
138+
func (c *Client) GetData(ctx context.Context, key string) ([]byte, error) {
139+
rc, err := c.Get(ctx, key)
140+
if err != nil {
141+
return nil, err
142+
}
143+
defer rc.Close()
144+
return io.ReadAll(rc)
145+
}
146+
147+
// PutCond writes the specified data to S3 under the given key if the key does
148+
// not already exist, or if its content differs from the given etag.
149+
// The etag is an MD5 of the expected contents, encoded as lowercase hex digits.
150+
// On success, written reports whether the object was written.
151+
func (c *Client) PutCond(ctx context.Context, key, etag string, data io.Reader) (written bool, _ error) {
152+
if _, err := c.Client.HeadObject(ctx, &s3.HeadObjectInput{
153+
Bucket: &c.Bucket,
154+
Key: &key,
155+
IfMatch: &etag,
156+
}); err == nil {
157+
return false, nil
158+
}
159+
return true, c.Put(ctx, key, data)
160+
}
161+
162+
// A sizer exports a Size method, e.g., [bytes.Reader] and similar.
163+
type sizer interface{ Size() int64 }
164+
165+
// A statter exports a Stat method, e.g., [os.File] and similar.
166+
type statter interface{ Stat() (fs.FileInfo, error) }

0 commit comments

Comments
 (0)