forked from johannesboyne/gofakes3
-
Notifications
You must be signed in to change notification settings - Fork 0
/
backend.go
372 lines (327 loc) · 14.7 KB
/
backend.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
package gofakes3
import (
"encoding/hex"
"io"
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
)
const (
DefaultBucketVersionKeys = 1000
)
// Object contains the data retrieved from a backend for the specified bucket
// and object key.
//
// You MUST always call Contents.Close() otherwise you may leak resources.
type Object struct {
Name string
Metadata map[string]string
Size int64
Contents io.ReadCloser
Hash []byte
Range *ObjectRange
// VersionID will be empty if bucket versioning has not been enabled.
VersionID VersionID
// If versioning is enabled for the bucket, this is true if this object version
// is a delete marker.
IsDeleteMarker bool
}
type ObjectList struct {
CommonPrefixes []CommonPrefix
Contents []*Content
IsTruncated bool
NextMarker string
// prefixes maintains an index of prefixes that have already been seen.
// This is a convenience for backend implementers like s3bolt and s3mem,
// which operate on a full, flat list of keys.
prefixes map[string]bool
}
func NewObjectList() *ObjectList {
return &ObjectList{}
}
func (b *ObjectList) Add(item *Content) {
b.Contents = append(b.Contents, item)
}
func (b *ObjectList) AddPrefix(prefix string) {
if b.prefixes == nil {
b.prefixes = map[string]bool{}
} else if b.prefixes[prefix] {
return
}
b.prefixes[prefix] = true
b.CommonPrefixes = append(b.CommonPrefixes, CommonPrefix{Prefix: prefix})
}
type ObjectDeleteResult struct {
// Specifies whether the versioned object that was permanently deleted was
// (true) or was not (false) a delete marker. In a simple DELETE, this
// header indicates whether (true) or not (false) a delete marker was
// created.
IsDeleteMarker bool
// Returns the version ID of the delete marker created as a result of the
// DELETE operation. If you delete a specific object version, the value
// returned by this header is the version ID of the object version deleted.
VersionID VersionID
}
type ListBucketVersionsPage struct {
// Specifies the key in the bucket that you want to start listing from.
// If HasKeyMarker is true, this must be non-empty.
KeyMarker string
HasKeyMarker bool
// Specifies the object version you want to start listing from. If
// HasVersionIDMarker is true, this must be non-empty.
VersionIDMarker VersionID
HasVersionIDMarker bool
// Sets the maximum number of keys returned in the response body. The
// response might contain fewer keys, but will never contain more. If
// additional keys satisfy the search criteria, but were not returned
// because max-keys was exceeded, the response contains
// <isTruncated>true</isTruncated>. To return the additional keys, see
// key-marker and version-id-marker.
//
// MaxKeys MUST be > 0, otherwise it is ignored.
MaxKeys int64
}
type ListBucketPage struct {
// Specifies the key in the bucket that represents the last item in
// the previous page. The first key in the returned page will be the
// next lexicographically (UTF-8 binary) sorted key after Marker.
// If HasMarker is true, this must be non-empty.
Marker string
HasMarker bool
// Sets the maximum number of keys returned in the response body. The
// response might contain fewer keys, but will never contain more. If
// additional keys satisfy the search criteria, but were not returned
// because max-keys was exceeded, the response contains
// <isTruncated>true</isTruncated>. To return the additional keys, see
// key-marker and version-id-marker.
//
// MaxKeys MUST be > 0, otherwise it is ignored.
MaxKeys int64
}
func (p ListBucketPage) IsEmpty() bool {
return p == ListBucketPage{}
}
type PutObjectResult struct {
// If versioning is enabled on the bucket, this should be set to the
// created version ID. If versioning is not enabled, this should be
// empty.
VersionID VersionID
}
// Backend provides a set of operations to be implemented in order to support
// gofakes3.
//
// The Backend API is not yet stable; if you create your own Backend, breakage
// is likely until this notice is removed.
type Backend interface {
// ListBuckets returns a list of all buckets owned by the authenticated
// sender of the request.
// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTServiceGET.html
ListBuckets() ([]BucketInfo, error)
// ListBucket returns the contents of a bucket. Backends should use the
// supplied prefix to limit the contents of the bucket and to sort the
// matched items into the Contents and CommonPrefixes fields.
//
// ListBucket must return a gofakes3.ErrNoSuchBucket error if the bucket
// does not exist. See gofakes3.BucketNotFound() for a convenient way to create one.
//
// The prefix MUST be correctly handled for the backend to be valid. Each
// item you consider returning should be checked using prefix.Match(name),
// even if the prefix is empty. The Backend MUST treat a nil prefix
// identically to a zero prefix.
//
// At this stage, implementers MAY return gofakes3.ErrInternalPageNotImplemented
// if the page argument is non-empty. In this case, gofakes3 may or may
// not, depending on how it was configured, retry the same request with no page.
// We have observed (though not yet confirmed) that simple clients tend to
// work fine if you ignore the pagination request, but this may not suit
// your application. Not all backends bundled with gofakes3 correctly
// support this pagination yet, but that will change.
ListBucket(name string, prefix *Prefix, page ListBucketPage) (*ObjectList, error)
// CreateBucket creates the bucket if it does not already exist. The name
// should be assumed to be a valid name.
//
// If the bucket already exists, a gofakes3.ResourceError with
// gofakes3.ErrBucketAlreadyExists MUST be returned.
CreateBucket(name string) error
// BucketExists should return a boolean indicating the bucket existence, or
// an error if the backend was unable to determine existence.
BucketExists(name string) (exists bool, err error)
// DeleteBucket deletes a bucket if and only if it is empty.
//
// If the bucket is not empty, gofakes3.ResourceError with
// gofakes3.ErrBucketNotEmpty MUST be returned.
//
// If the bucket does not exist, gofakes3.ErrNoSuchBucket MUST be returned.
//
// AWS does not validate the bucket's name for anything other than existence.
DeleteBucket(name string) error
// GetObject must return a gofakes3.ErrNoSuchKey error if the object does
// not exist. See gofakes3.KeyNotFound() for a convenient way to create
// one.
//
// If the returned Object is not nil, you MUST call Object.Contents.Close(),
// otherwise you will leak resources. Implementers should return a no-op
// implementation of io.ReadCloser.
//
// If rnge is nil, it is assumed you want the entire object. If rnge is not
// nil, but the underlying backend does not support range requests,
// implementers MUST return ErrNotImplemented.
//
// If the backend is a VersionedBackend, GetObject retrieves the latest version.
GetObject(bucketName, objectName string, rangeRequest *ObjectRangeRequest) (*Object, error)
// HeadObject fetches the Object from the backend, but reading the Contents
// will return io.EOF immediately.
//
// If the returned Object is not nil, you MUST call Object.Contents.Close(),
// otherwise you will leak resources. Implementers should return a no-op
// implementation of io.ReadCloser.
//
// HeadObject should return a NotFound() error if the object does not
// exist.
HeadObject(bucketName, objectName string) (*Object, error)
// DeleteObject deletes an object from the bucket.
//
// If the backend is a VersionedBackend and versioning is enabled, this
// should introduce a delete marker rather than actually delete the object.
//
// DeleteObject must return a gofakes3.ErrNoSuchBucket error if the bucket
// does not exist. See gofakes3.BucketNotFound() for a convenient way to create one.
// FIXME: confirm with S3 whether this is the correct behaviour.
//
// DeleteObject must not return an error if the object does not exist. Source:
// https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#S3.DeleteObject:
//
// Removes the null version (if there is one) of an object and inserts a
// delete marker, which becomes the latest version of the object. If there
// isn't a null version, Amazon S3 does not remove any objects.
//
DeleteObject(bucketName, objectName string) (ObjectDeleteResult, error)
// PutObject should assume that the key is valid. The map containing meta
// may be nil.
//
// The size can be used if the backend needs to read the whole reader; use
// gofakes3.ReadAll() for this job rather than ioutil.ReadAll().
PutObject(bucketName, key string, meta map[string]string, input io.Reader, size int64) (PutObjectResult, error)
DeleteMulti(bucketName string, objects ...string) (MultiDeleteResult, error)
CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (CopyObjectResult, error)
}
// VersionedBackend may be optionally implemented by a Backend in order to support
// operations on S3 object versions.
//
// If you don't implement VersionedBackend, requests to GoFakeS3 that attempt to
// make use of versions will return ErrNotImplemented if GoFakesS3 is unable to
// find another way to satisfy the request.
type VersionedBackend interface {
// VersioningConfiguration must return a gofakes3.ErrNoSuchBucket error if the bucket
// does not exist. See gofakes3.BucketNotFound() for a convenient way to create one.
//
// If the bucket has never had versioning enabled, VersioningConfiguration MUST return
// empty strings (S300001).
VersioningConfiguration(bucket string) (VersioningConfiguration, error)
// SetVersioningConfiguration must return a gofakes3.ErrNoSuchBucket error if the bucket
// does not exist. See gofakes3.BucketNotFound() for a convenient way to create one.
SetVersioningConfiguration(bucket string, v VersioningConfiguration) error
// GetObject must return a gofakes3.ErrNoSuchKey error if the object does
// not exist. See gofakes3.KeyNotFound() for a convenient way to create
// one.
//
// If the returned Object is not nil, you MUST call Object.Contents.Close(),
// otherwise you will leak resources. Implementers should return a no-op
// implementation of io.ReadCloser.
//
// GetObject must return gofakes3.ErrNoSuchVersion if the version does not
// exist.
//
// If versioning has been enabled on a bucket, but subsequently suspended,
// GetObjectVersion should still return the object version (S300001).
//
// FIXME: s3assumer test; what happens when versionID is empty? Does it
// return the latest?
GetObjectVersion(
bucketName, objectName string,
versionID VersionID,
rangeRequest *ObjectRangeRequest) (*Object, error)
// HeadObjectVersion fetches the Object version from the backend, but the Contents will be
// a no-op ReadCloser.
//
// If the returned Object is not nil, you MUST call Object.Contents.Close(),
// otherwise you will leak resources. Implementers should return a no-op
// implementation of io.ReadCloser.
//
// HeadObjectVersion should return a NotFound() error if the object does not
// exist.
HeadObjectVersion(bucketName, objectName string, versionID VersionID) (*Object, error)
// DeleteObjectVersion permanently deletes a specific object version.
//
// DeleteObjectVersion must return a gofakes3.ErrNoSuchBucket error if the bucket
// does not exist. See gofakes3.BucketNotFound() for a convenient way to create one.
//
// If the bucket exists and either the object does not exist (S300003) or
// the version does not exist (S300002), you MUST return an empty
// ObjectDeleteResult and a nil error.
DeleteObjectVersion(bucketName, objectName string, versionID VersionID) (ObjectDeleteResult, error)
// DeleteMultiVersions permanently deletes all of the specified Object Versions
DeleteMultiVersions(bucketName string, objects ...ObjectID) (MultiDeleteResult, error)
// Backend implementers can assume the ListBucketVersionsPage is valid:
// KeyMarker and VersionIDMarker will either both be set, or both be unset. No
// other combination will be present (S300004).
//
// https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETVersion.html
//
// This MUST return the list of current versions with an empty VersionID
// even if versioning has never been enabled for the bucket (S300005).
//
// The Backend MUST treat a nil prefix identically to a zero prefix, and a
// nil page identically to a zero page.
ListBucketVersions(bucketName string, prefix *Prefix, page *ListBucketVersionsPage) (*ListBucketVersionsResult, error)
}
// MultipartBackend may be optionally implemented by a Backend in order to
// support S3 multiplart uploads.
// If you don't implement MultipartBackend, GoFakeS3 will fall back to an
// in-memory implementation which holds all parts in memory until the upload
// gets finalised and pushed to the backend.
type MultipartBackend interface {
CreateMultipartUpload(bucket, object string, meta map[string]string) (UploadID, error)
UploadPart(bucket, object string, id UploadID, partNumber int, contentLength int64, input io.Reader) (etag string, err error)
ListMultipartUploads(bucket string, marker *UploadListMarker, prefix Prefix, limit int64) (*ListMultipartUploadsResult, error)
ListParts(bucket, object string, uploadID UploadID, marker int, limit int64) (*ListMultipartUploadPartsResult, error)
AbortMultipartUpload(bucket, object string, id UploadID) error
CompleteMultipartUpload(bucket, object string, id UploadID, input *CompleteMultipartUploadRequest) (versionID VersionID, etag string, err error)
}
// CopyObject is a helper function useful for quickly implementing CopyObject on
// a backend that already supports GetObject and PutObject. This isn't very
// efficient so only use this if performance isn't important.
func CopyObject(db Backend, srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result CopyObjectResult, err error) {
c, err := db.GetObject(srcBucket, srcKey, nil)
if err != nil {
return
}
defer c.Contents.Close()
_, err = db.PutObject(dstBucket, dstKey, meta, c.Contents, c.Size)
if err != nil {
return
}
return CopyObjectResult{
ETag: `"` + hex.EncodeToString(c.Hash) + `"`,
LastModified: NewContentTime(time.Now()),
}, nil
}
func MergeMetadata(db Backend, bucketName string, objectName string, meta map[string]string) error {
// get potential existing object to potentially carry metadata over
existingObj, err := db.GetObject(bucketName, objectName, nil)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() != string(ErrNoSuchKey) {
return err
}
}
// carry over metadata if it exists
if existingObj != nil {
for k, v := range existingObj.Metadata {
// new metadata overwrites old but keep the rest
// TODO: check how metadata can be deleted?!
if _, ok := meta[k]; !ok {
meta[k] = v
}
}
}
return nil
}