Skip to content

Commit 5b1d549

Browse files
Merge branch 'master' into feature-PutObjectTagging
2 parents 4d9f6f2 + c55a48f commit 5b1d549

File tree

22 files changed

+640
-227
lines changed

22 files changed

+640
-227
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,4 @@ A big thank you to all the [contributors](https://github.com/johannesboyne/gofak
199199
especially [Blake @shabbyrobe](https://github.com/shabbyrobe) who pushed this
200200
little project to the next level!
201201

202-
**Help wanred**
202+
**Help wanted**

backend.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package gofakes3
22

33
import (
4+
"encoding/hex"
45
"io"
6+
"time"
57

68
"github.com/aws/aws-sdk-go/aws/awserr"
79
)
@@ -231,6 +233,8 @@ type Backend interface {
231233
PutObject(bucketName, key string, meta map[string]string, tags map[string]string, input io.Reader, size int64) (PutObjectResult, error)
232234

233235
DeleteMulti(bucketName string, objects ...string) (MultiDeleteResult, error)
236+
237+
CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (CopyObjectResult, error)
234238
}
235239

236240
// VersionedBackend may be optionally implemented by a Backend in order to support
@@ -310,6 +314,43 @@ type VersionedBackend interface {
310314
ListBucketVersions(bucketName string, prefix *Prefix, page *ListBucketVersionsPage) (*ListBucketVersionsResult, error)
311315
}
312316

317+
// MultipartBackend may be optionally implemented by a Backend in order to
318+
// support S3 multiplart uploads.
319+
// If you don't implement MultipartBackend, GoFakeS3 will fall back to an
320+
// in-memory implementation which holds all parts in memory until the upload
321+
// gets finalised and pushed to the backend.
322+
type MultipartBackend interface {
323+
CreateMultipartUpload(bucket, object string, meta map[string]string) (UploadID, error)
324+
UploadPart(bucket, object string, id UploadID, partNumber int, contentLength int64, input io.Reader) (etag string, err error)
325+
326+
ListMultipartUploads(bucket string, marker *UploadListMarker, prefix Prefix, limit int64) (*ListMultipartUploadsResult, error)
327+
ListParts(bucket, object string, uploadID UploadID, marker int, limit int64) (*ListMultipartUploadPartsResult, error)
328+
329+
AbortMultipartUpload(bucket, object string, id UploadID) error
330+
CompleteMultipartUpload(bucket, object string, id UploadID, input *CompleteMultipartUploadRequest) (versionID VersionID, etag string, err error)
331+
}
332+
333+
// CopyObject is a helper function useful for quickly implementing CopyObject on
334+
// a backend that already supports GetObject and PutObject. This isn't very
335+
// efficient so only use this if performance isn't important.
336+
func CopyObject(db Backend, srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result CopyObjectResult, err error) {
337+
c, err := db.GetObject(srcBucket, srcKey, nil)
338+
if err != nil {
339+
return
340+
}
341+
defer c.Contents.Close()
342+
343+
_, err = db.PutObject(dstBucket, dstKey, meta, c.Contents, c.Size)
344+
if err != nil {
345+
return
346+
}
347+
348+
return CopyObjectResult{
349+
ETag: `"` + hex.EncodeToString(c.Hash) + `"`,
350+
LastModified: NewContentTime(time.Now()),
351+
}, nil
352+
}
353+
313354
func MergeMetadata(db Backend, bucketName string, objectName string, meta map[string]string) error {
314355
// get potential existing object to potentially carry metadata over
315356
existingObj, err := db.GetObject(bucketName, objectName, nil)

backend/s3afero/backend_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func TestMultiCreateBucket(t *testing.T) {
298298
}
299299
defer os.RemoveAll(tmp)
300300

301-
fs, err := FsPath(tmp)
301+
fs, err := FsPath(tmp, 0)
302302
if err != nil {
303303
t.Fatal(err)
304304
}

backend/s3afero/multi.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type MultiBucketBackend struct {
3131
bucketFs afero.Fs
3232
metaStore *metaStore
3333
dirMode os.FileMode
34+
flags FsFlags
3435

3536
// FIXME(bw): values in here should not be used beyond the configuration
3637
// step; maybe this can be cleaned up later using a builder struct or
@@ -47,19 +48,28 @@ func MultiBucket(fs afero.Fs, opts ...MultiOption) (*MultiBucketBackend, error)
4748
return nil, err
4849
}
4950

50-
b := &MultiBucketBackend{
51-
baseFs: fs,
52-
bucketFs: afero.NewBasePathFs(fs, "buckets"),
53-
dirMode: 0700,
54-
}
51+
b := &MultiBucketBackend{}
5552
for _, opt := range opts {
5653
if err := opt(b); err != nil {
5754
return nil, err
5855
}
5956
}
6057

58+
bucketsFs, err := NewBasePathFs(fs, "buckets", FsPathCreateAll)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
b.baseFs = fs
64+
b.bucketFs = bucketsFs
65+
b.dirMode = 0700
66+
6167
if b.configOnly.metaFs == nil {
62-
b.configOnly.metaFs = afero.NewBasePathFs(fs, "metadata")
68+
metaFs, err := NewBasePathFs(fs, "metadata", FsPathCreateAll)
69+
if err != nil {
70+
return nil, err
71+
}
72+
b.configOnly.metaFs = metaFs
6373
}
6474
b.metaStore = newMetaStore(b.configOnly.metaFs, modTimeFsCalc(fs))
6575

@@ -141,7 +151,7 @@ func (db *MultiBucketBackend) getBucketWithFilePrefixLocked(bucket string, prefi
141151
}
142152

143153
if entry.IsDir() {
144-
response.AddPrefix(path.Join(prefixPath, prefixPart))
154+
response.AddPrefix(path.Join(prefixPath, prefixPart, entry.Name()) + "/")
145155

146156
} else {
147157
size := entry.Size()
@@ -292,6 +302,8 @@ func (db *MultiBucketBackend) HeadObject(bucketName, objectName string) (*gofake
292302
return nil, gofakes3.KeyNotFound(objectName)
293303
} else if err != nil {
294304
return nil, err
305+
} else if stat.IsDir() {
306+
return nil, gofakes3.KeyNotFound(objectName)
295307
}
296308

297309
size, mtime := stat.Size(), stat.ModTime()
@@ -331,7 +343,6 @@ func (db *MultiBucketBackend) GetObject(bucketName, objectName string, rangeRequ
331343
} else if err != nil {
332344
return nil, err
333345
}
334-
335346
defer func() {
336347
// If an error occurs, the caller may not have access to Object.Body in order to close it:
337348
if obj == nil && rerr != nil {
@@ -342,6 +353,8 @@ func (db *MultiBucketBackend) GetObject(bucketName, objectName string, rangeRequ
342353
stat, err := f.Stat()
343354
if err != nil {
344355
return nil, err
356+
} else if stat.IsDir() {
357+
return nil, gofakes3.KeyNotFound(objectName)
345358
}
346359

347360
size, mtime := stat.Size(), stat.ModTime()
@@ -456,6 +469,10 @@ func (db *MultiBucketBackend) PutObject(
456469
return result, nil
457470
}
458471

472+
func (db *MultiBucketBackend) CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result gofakes3.CopyObjectResult, err error) {
473+
return gofakes3.CopyObject(db, srcBucket, srcKey, dstBucket, dstKey, meta)
474+
}
475+
459476
func (db *MultiBucketBackend) DeleteObject(bucketName, objectName string) (result gofakes3.ObjectDeleteResult, rerr error) {
460477
db.lock.Lock()
461478
defer db.lock.Unlock()

backend/s3afero/option.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,11 @@ func MultiWithMetaFs(fs afero.Fs) MultiOption {
1616
}
1717
}
1818

19+
func MultiFsFlags(flags FsFlags) MultiOption {
20+
return func(b *MultiBucketBackend) error {
21+
b.flags = flags
22+
return nil
23+
}
24+
}
25+
1926
type SingleOption func(b *SingleBucketBackend) error

backend/s3afero/single.go

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package s3afero
33
import (
44
"crypto/md5"
55
"encoding/hex"
6-
"fmt"
6+
"errors"
77
"io"
88
"log"
99
"os"
@@ -131,13 +131,12 @@ func (db *SingleBucketBackend) getBucketWithFilePrefixLocked(bucket string, pref
131131
}
132132

133133
if entry.IsDir() {
134-
response.AddPrefix(path.Join(prefixPath, prefixPart))
134+
response.AddPrefix(path.Join(prefixPath, entry.Name()) + "/")
135135

136136
} else {
137137
size := entry.Size()
138138
mtime := entry.ModTime()
139-
140-
meta, err := db.metaStore.loadMeta(bucket, objectPath, size, mtime)
139+
meta, err := db.ensureMeta(bucket, objectPath, size, mtime)
141140
if err != nil {
142141
return nil, err
143142
}
@@ -157,31 +156,25 @@ func (db *SingleBucketBackend) getBucketWithFilePrefixLocked(bucket string, pref
157156
func (db *SingleBucketBackend) getBucketWithArbitraryPrefixLocked(bucket string, prefix *gofakes3.Prefix) (*gofakes3.ObjectList, error) {
158157
response := gofakes3.NewObjectList()
159158

160-
if err := afero.Walk(db.fs, filepath.FromSlash(bucket), func(path string, info os.FileInfo, err error) error {
159+
if err := afero.Walk(db.fs, filepath.FromSlash("."), func(path string, info os.FileInfo, err error) error {
161160
if err != nil || info.IsDir() {
162161
return err
163162
}
164163

165164
objectPath := filepath.ToSlash(path)
166-
parts := strings.SplitN(objectPath, "/", 2)
167-
if len(parts) != 2 {
168-
panic(fmt.Errorf("unexpected path %q", path)) // should never happen
169-
}
170-
objectName := parts[1]
171-
172-
if !prefix.Match(objectName, nil) {
165+
if !prefix.Match(objectPath, nil) {
173166
return nil
174167
}
175168

176169
size := info.Size()
177170
mtime := info.ModTime()
178-
meta, err := db.metaStore.loadMeta(bucket, objectName, size, mtime)
171+
meta, err := db.ensureMeta(bucket, objectPath, size, mtime)
179172
if err != nil {
180173
return err
181174
}
182175

183176
response.Add(&gofakes3.Content{
184-
Key: objectName,
177+
Key: objectPath,
185178
LastModified: gofakes3.NewContentTime(mtime),
186179
ETag: `"` + hex.EncodeToString(meta.Hash) + `"`,
187180
Size: size,
@@ -196,6 +189,46 @@ func (db *SingleBucketBackend) getBucketWithArbitraryPrefixLocked(bucket string,
196189
return response, nil
197190
}
198191

192+
func (db *SingleBucketBackend) ensureMeta(
193+
bucket string,
194+
objectPath string,
195+
size int64,
196+
mtime time.Time,
197+
) (meta *Metadata, err error) {
198+
existingMeta, err := db.metaStore.loadMeta(bucket, objectPath, size, mtime)
199+
if errors.Is(err, os.ErrNotExist) {
200+
f, err := db.fs.Open(filepath.FromSlash(objectPath))
201+
if err != nil {
202+
return nil, err
203+
}
204+
defer f.Close()
205+
206+
hasher := md5.New()
207+
if _, err := io.Copy(hasher, f); err != nil {
208+
return nil, err
209+
}
210+
211+
hash, err := hasher.Sum(nil), nil
212+
if err != nil {
213+
return nil, err
214+
}
215+
216+
return &Metadata{
217+
objectPath,
218+
mtime,
219+
size,
220+
hash,
221+
map[string]string{},
222+
}, nil
223+
224+
} else if err != nil {
225+
return nil, err
226+
227+
} else {
228+
return existingMeta, nil
229+
}
230+
}
231+
199232
func (db *SingleBucketBackend) HeadObject(bucketName, objectName string) (*gofakes3.Object, error) {
200233
if bucketName != db.name {
201234
return nil, gofakes3.BucketNotFound(bucketName)
@@ -209,11 +242,12 @@ func (db *SingleBucketBackend) HeadObject(bucketName, objectName string) (*gofak
209242
return nil, gofakes3.KeyNotFound(objectName)
210243
} else if err != nil {
211244
return nil, err
245+
} else if stat.IsDir() {
246+
return nil, gofakes3.KeyNotFound(objectName)
212247
}
213248

214249
size, mtime := stat.Size(), stat.ModTime()
215-
216-
meta, err := db.metaStore.loadMeta(bucketName, objectName, size, mtime)
250+
meta, err := db.ensureMeta(bucketName, objectName, size, mtime)
217251
if err != nil {
218252
return nil, err
219253
}
@@ -242,7 +276,6 @@ func (db *SingleBucketBackend) GetObject(bucketName, objectName string, rangeReq
242276
} else if err != nil {
243277
return nil, err
244278
}
245-
246279
defer func() {
247280
// If an error occurs, the caller may not have access to Object.Body in order to close it:
248281
if err != nil && obj == nil {
@@ -253,6 +286,8 @@ func (db *SingleBucketBackend) GetObject(bucketName, objectName string, rangeReq
253286
stat, err := f.Stat()
254287
if err != nil {
255288
return nil, err
289+
} else if stat.IsDir() {
290+
return nil, gofakes3.KeyNotFound(objectName)
256291
}
257292

258293
size, mtime := stat.Size(), stat.ModTime()
@@ -270,7 +305,7 @@ func (db *SingleBucketBackend) GetObject(bucketName, objectName string, rangeReq
270305
rdr = limitReadCloser(rdr, f.Close, rnge.Length)
271306
}
272307

273-
meta, err := db.metaStore.loadMeta(bucketName, objectName, size, mtime)
308+
meta, err := db.ensureMeta(bucketName, objectName, size, mtime)
274309
if err != nil {
275310
return nil, err
276311
}
@@ -387,6 +422,10 @@ func (db *SingleBucketBackend) DeleteMulti(bucketName string, objects ...string)
387422
return result, nil
388423
}
389424

425+
func (db *SingleBucketBackend) CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result gofakes3.CopyObjectResult, err error) {
426+
return gofakes3.CopyObject(db, srcBucket, srcKey, dstBucket, dstKey, meta)
427+
}
428+
390429
func (db *SingleBucketBackend) DeleteObject(bucketName, objectName string) (result gofakes3.ObjectDeleteResult, rerr error) {
391430
if bucketName != db.name {
392431
return result, gofakes3.BucketNotFound(bucketName)

0 commit comments

Comments
 (0)