@@ -9,11 +9,13 @@ import (
9
9
"fmt"
10
10
"hash"
11
11
"io"
12
+ "io/fs"
12
13
"os"
13
14
14
15
"github.com/aws/aws-sdk-go-v2/config"
15
16
"github.com/aws/aws-sdk-go-v2/service/s3"
16
17
"github.com/aws/aws-sdk-go-v2/service/s3/types"
18
+ "github.com/creachadair/mds/value"
17
19
)
18
20
19
21
// 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) }
69
71
// ETag returns a correctly-formatted S3 etag for the contents of e that have
70
72
// been read so far.
71
73
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