[ARVADOS] created: 2.1.0-724-gc2d2234ce
Git user
git at public.arvados.org
Fri Apr 23 14:24:26 UTC 2021
at c2d2234ce0da91881fc63459a30c5efcfbe29a26 (commit)
commit c2d2234ce0da91881fc63459a30c5efcfbe29a26
Author: Tom Clegg <tom at curii.com>
Date: Thu Apr 22 16:14:38 2021 -0400
17507: Fix calling Child() without lock.
Encountered in services/keep-web tests.
START: handler_test.go:654: IntegrationSuite.TestDirectoryListingWithNoAnonymousToken
START: server_test.go:431: IntegrationSuite.SetUpTest
[...]
fatal error: concurrent map iteration and map write
goroutine 2192 [running]:
runtime.throw(0xe545ab, 0x26)
/var/lib/arvados/go/src/runtime/panic.go:1112 +0x72 fp=0xc002544c78 sp=0xc002544c48 pc=0x4366b2
runtime.mapiternext(0xc002544d18)
/var/lib/arvados/go/src/runtime/map.go:853 +0x552 fp=0xc002544cf8 sp=0xc002544c78 pc=0x411442
git.arvados.org/arvados.git/sdk/go/arvados.(*treenode).MemorySize(0xc00202e480, 0x0)
/home/tom/arvados/sdk/go/arvados/fs_base.go:336 +0x106 fp=0xc002544d98 sp=0xc002544cf8 pc=0x8c7de6
git.arvados.org/arvados.git/sdk/go/arvados.(*treenode).MemorySize(0xc0003a66c0, 0x0)
/home/tom/arvados/sdk/go/arvados/fs_base.go:337 +0xe3 fp=0xc002544e38 sp=0xc002544d98 pc=0x8c7dc3
git.arvados.org/arvados.git/sdk/go/arvados.(*treenode).MemorySize(0xc0020c3680, 0x0)
/home/tom/arvados/sdk/go/arvados/fs_base.go:337 +0xe3 fp=0xc002544ed8 sp=0xc002544e38 pc=0x8c7dc3
git.arvados.org/arvados.git/sdk/go/arvados.(*fileSystem).MemorySize(0xc001af0000, 0x0)
/home/tom/arvados/sdk/go/arvados/fs_base.go:631 +0x33 fp=0xc002544ef8 sp=0xc002544ed8 pc=0x8ca1e3
git.arvados.org/arvados.git/services/keep-web.(*cache).collectionBytes(0xc00261da08, 0x3ff0000000000101)
/home/tom/arvados/services/keep-web/cache.go:448 +0x21c fp=0xc002544f70 sp=0xc002544ef8 pc=0xc2903c
git.arvados.org/arvados.git/services/keep-web.(*cache).updateGauges(0xc00261da08)
/home/tom/arvados/services/keep-web/cache.go:170 +0x2f fp=0xc002544f90 sp=0xc002544f70 pc=0xc269bf
git.arvados.org/arvados.git/services/keep-web.(*cache).setup.func1(0xc00261da08)
/home/tom/arvados/services/keep-web/cache.go:164 +0x6d fp=0xc002544fd8 sp=0xc002544f90 pc=0xc65dad
runtime.goexit()
/var/lib/arvados/go/src/runtime/asm_amd64.s:1373 +0x1 fp=0xc002544fe0 sp=0xc002544fd8 pc=0x468dd1
created by git.arvados.org/arvados.git/services/keep-web.(*cache).setup
/home/tom/arvados/services/keep-web/cache.go:162 +0x1db
goroutine 2144 [runnable]:
git.arvados.org/arvados.git/sdk/go/arvados.(*treenode).Child(0xc00202e480, 0xc002c4e117, 0x9, 0xc0005c2070, 0x10c9680, 0xc0021d0240, 0x0, 0x0)
/home/tom/arvados/sdk/go/arvados/fs_base.go:289 +0x171
git.arvados.org/arvados.git/sdk/go/arvados.(*lookupnode).Readdir(0xc00202e480, 0x0, 0x0, 0x0, 0x0, 0x0)
/home/tom/arvados/sdk/go/arvados/fs_lookup.go:53 +0x27b
git.arvados.org/arvados.git/sdk/go/arvados.(*filehandle).Readdir(0xc00263c140, 0x0, 0xc002066360, 0xc00263c140, 0x10c4f40, 0xc00263c140, 0x0)
/home/tom/arvados/sdk/go/arvados/fs_filehandle.go:81 +0x1d2
golang.org/x/net/webdav.walkFS(0x10bbe00, 0xc002b09590, 0x10bd900, 0xc002af1e60, 0xffffffffffffffff, 0xc00161a574, 0xc, 0x10c17c0, 0xc00227c280, 0xc0005c2628, ...)
/home/tom/arvados/tmp/GOPATH/pkg/mod/golang.org/x/net at v0.0.0-20200202094626-16171245cfb2/webdav/file.go:772 +0x25e
golang.org/x/net/webdav.walkFS(0x10bbe00, 0xc002b09590, 0x10bd900, 0xc002af1e60, 0xffffffffffffffff, 0xe2f941, 0x5, 0x10c17c0, 0xc002b1a4c0, 0xc0005c2628, ...)
/home/tom/arvados/tmp/GOPATH/pkg/mod/golang.org/x/net at v0.0.0-20200202094626-16171245cfb2/webdav/file.go:786 +0x564
golang.org/x/net/webdav.walkFS(0x10bbe00, 0xc002b09590, 0x10bd900, 0xc002af1e60, 0xffffffffffffffff, 0xc00216be76, 0x0, 0x10c17c0, 0xc002b1a0c0, 0xc0005c2628, ...)
/home/tom/arvados/tmp/GOPATH/pkg/mod/golang.org/x/net at v0.0.0-20200202094626-16171245cfb2/webdav/file.go:786 +0x564
golang.org/x/net/webdav.(*Handler).handlePropfind(0xc002b1a080, 0x7f1c582d9120, 0xc0029e5400, 0xc002b04900, 0xc0022bc718, 0x415ad3, 0xc002b1a080)
/home/tom/arvados/tmp/GOPATH/pkg/mod/golang.org/x/net at v0.0.0-20200202094626-16171245cfb2/webdav/webdav.go:566 +0x3d7
golang.org/x/net/webdav.(*Handler).ServeHTTP(0xc002b1a080, 0x7f1c582d9120, 0xc0029e5400, 0xc002b04900)
/home/tom/arvados/tmp/GOPATH/pkg/mod/golang.org/x/net at v0.0.0-20200202094626-16171245cfb2/webdav/webdav.go:67 +0x556
git.arvados.org/arvados.git/services/keep-web.(*handler).serveSiteFS(0xc002221ae0, 0x7f1c582d9120, 0xc0029e5400, 0xc002b04900, 0xc002b22010, 0x1, 0x1, 0x101)
/home/tom/arvados/services/keep-web/handler.go:593 +0x80b
git.arvados.org/arvados.git/services/keep-web.(*handler).ServeHTTP(0xc002221ae0, 0x10b6ec0, 0xc002b094a0, 0xc002b04900)
/home/tom/arvados/services/keep-web/handler.go:330 +0x2a0b
git.arvados.org/arvados.git/sdk/go/httpserver.LogRequests.func1(0x7f1c58319c58, 0xc002b09470, 0xc002b04800)
/home/tom/arvados/sdk/go/httpserver/logger.go:56 +0x8d8
net/http.HandlerFunc.ServeHTTP(0xc002282460, 0x7f1c58319c58, 0xc002b09470, 0xc002b04800)
/var/lib/arvados/go/src/net/http/server.go:2012 +0x44
git.arvados.org/arvados.git/sdk/go/httpserver.AddRequestIDs.func1(0x7f1c58319c58, 0xc002b09470, 0xc002b04800)
/home/tom/arvados/sdk/go/httpserver/id_generator.go:57 +0x1a5
net/http.HandlerFunc.ServeHTTP(0xc002282480, 0x7f1c58319c58, 0xc002b09470, 0xc002b04800)
/var/lib/arvados/go/src/net/http/server.go:2012 +0x44
git.arvados.org/arvados.git/sdk/go/httpserver.HandlerWithContext.func1(0x7f1c58319c58, 0xc002b09470, 0xc002b04700)
/home/tom/arvados/sdk/go/httpserver/logger.go:30 +0x107
net/http.HandlerFunc.ServeHTTP(0xc002901b30, 0x7f1c58319c58, 0xc002b09470, 0xc002b04700)
/var/lib/arvados/go/src/net/http/server.go:2012 +0x44
github.com/prometheus/client_golang/prometheus/promhttp.InstrumentHandlerDuration.func1(0x10b8bc0, 0xc002b1a000, 0xc002b04700)
/home/tom/arvados/tmp/GOPATH/pkg/mod/github.com/prometheus/client_golang at v1.2.1/prometheus/promhttp/instrument_server.go:68 +0x11c
net/http.HandlerFunc.ServeHTTP(0xc002901e00, 0x10b8bc0, 0xc002b1a000, 0xc002b04700)
/var/lib/arvados/go/src/net/http/server.go:2012 +0x44
git.arvados.org/arvados.git/sdk/go/httpserver.(*metrics).ServeHTTP(0xc002113e00, 0x10b8bc0, 0xc002b1a000, 0xc002b04700)
/home/tom/arvados/sdk/go/httpserver/metrics.go:70 +0x51
git.arvados.org/arvados.git/services/keep-web.(*IntegrationSuite).testDirectoryListing(0xc000010138, 0xc0007d41e0)
/home/tom/arvados/services/keep-web/handler_test.go:864 +0x1a07
git.arvados.org/arvados.git/services/keep-web.(*IntegrationSuite).TestDirectoryListingWithNoAnonymousToken(0xc000010138, 0xc0007d41e0)
/home/tom/arvados/services/keep-web/handler_test.go:656 +0x67
reflect.Value.call(0xe2bbc0, 0xc000010138, 0x4613, 0xe2deb6, 0x4, 0xc000282f08, 0x1, 0x1, 0x171d800, 0xc000282e48, ...)
/var/lib/arvados/go/src/reflect/value.go:460 +0x8ab
reflect.Value.Call(0xe2bbc0, 0xc000010138, 0x4613, 0xc000282f08, 0x1, 0x1, 0xc0007d42d0, 0xc000128060, 0xc00287a720)
/var/lib/arvados/go/src/reflect/value.go:321 +0xb4
gopkg.in/check%2ev1.(*suiteRunner).forkTest.func1(0xc0007d41e0)
/home/tom/arvados/tmp/GOPATH/pkg/mod/gopkg.in/check.v1 at v1.0.0-20161208181325-20d25e280405/check.go:772 +0x628
gopkg.in/check%2ev1.(*suiteRunner).forkCall.func1(0xc000174c00, 0xc0007d41e0, 0xc00222b760)
/home/tom/arvados/tmp/GOPATH/pkg/mod/gopkg.in/check.v1 at v1.0.0-20161208181325-20d25e280405/check.go:666 +0x98
created by gopkg.in/check%2ev1.(*suiteRunner).forkCall
/home/tom/arvados/tmp/GOPATH/pkg/mod/gopkg.in/check.v1 at v1.0.0-20161208181325-20d25e280405/check.go:663 +0x1fb
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>
diff --git a/sdk/go/arvados/fs_lookup.go b/sdk/go/arvados/fs_lookup.go
index 56b595323..021e8241c 100644
--- a/sdk/go/arvados/fs_lookup.go
+++ b/sdk/go/arvados/fs_lookup.go
@@ -50,9 +50,11 @@ func (ln *lookupnode) Readdir() ([]os.FileInfo, error) {
return nil, err
}
for _, child := range all {
+ ln.treenode.Lock()
_, err = ln.treenode.Child(child.FileInfo().Name(), func(inode) (inode, error) {
return child, nil
})
+ ln.treenode.Unlock()
if err != nil {
return nil, err
}
commit 1b2f1c350c821ee8f15b56922e1b74a785c8308e
Author: Tom Clegg <tom at curii.com>
Date: Thu Apr 22 15:12:17 2021 -0400
17507: Support ListObjectsV2 API.
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at curii.com>
diff --git a/services/keep-web/s3.go b/services/keep-web/s3.go
index 620a21b88..f03ff01b8 100644
--- a/services/keep-web/s3.go
+++ b/services/keep-web/s3.go
@@ -7,6 +7,7 @@ package main
import (
"crypto/hmac"
"crypto/sha256"
+ "encoding/base64"
"encoding/xml"
"errors"
"fmt"
@@ -33,6 +34,42 @@ const (
s3MaxClockSkew = 5 * time.Minute
)
+type commonPrefix struct {
+ Prefix string
+}
+
+type listV1Resp struct {
+ XMLName string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult"`
+ s3.ListResp
+ // s3.ListResp marshals an empty tag when
+ // CommonPrefixes is nil, which confuses some clients.
+ // Fix by using this nested struct instead.
+ CommonPrefixes []commonPrefix
+ // Similarly, we need omitempty here, because an empty
+ // tag confuses some clients (e.g.,
+ // github.com/aws/aws-sdk-net never terminates its
+ // paging loop).
+ NextMarker string `xml:"NextMarker,omitempty"`
+ // ListObjectsV2 has a KeyCount response field.
+ KeyCount int
+}
+
+type listV2Resp struct {
+ XMLName string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult"`
+ IsTruncated bool
+ Contents []s3.Key
+ Name string
+ Prefix string
+ Delimiter string
+ MaxKeys int
+ CommonPrefixes []commonPrefix
+ EncodingType string `xml:",omitempty"`
+ KeyCount int
+ ContinuationToken string `xml:",omitempty"`
+ NextContinuationToken string `xml:",omitempty"`
+ StartAfter string `xml:",omitempty"`
+}
+
func hmacstring(msg string, key []byte) []byte {
h := hmac.New(sha256.New, key)
io.WriteString(h, msg)
@@ -559,19 +596,50 @@ var errDone = errors.New("done")
func (h *handler) s3list(bucket string, w http.ResponseWriter, r *http.Request, fs arvados.CustomFileSystem) {
var params struct {
- delimiter string
- marker string
- maxKeys int
- prefix string
+ v2 bool
+ delimiter string
+ maxKeys int
+ prefix string
+ marker string // decoded continuationToken (v2) or provided by client (v1)
+ startAfter string // v2
+ continuationToken string // v2
+ encodingTypeURL bool // v2
}
params.delimiter = r.FormValue("delimiter")
- params.marker = r.FormValue("marker")
if mk, _ := strconv.ParseInt(r.FormValue("max-keys"), 10, 64); mk > 0 && mk < s3MaxKeys {
params.maxKeys = int(mk)
} else {
params.maxKeys = s3MaxKeys
}
params.prefix = r.FormValue("prefix")
+ switch r.FormValue("list-type") {
+ case "":
+ case "2":
+ params.v2 = true
+ default:
+ http.Error(w, "invalid list-type parameter", http.StatusBadRequest)
+ return
+ }
+ if params.v2 {
+ params.continuationToken = r.FormValue("continuation-token")
+ marker, err := base64.StdEncoding.DecodeString(params.continuationToken)
+ if err != nil {
+ http.Error(w, "invalid continuation token", http.StatusBadRequest)
+ return
+ }
+ params.marker = string(marker)
+ params.startAfter = r.FormValue("start-after")
+ switch r.FormValue("encoding-type") {
+ case "":
+ case "url":
+ params.encodingTypeURL = true
+ default:
+ http.Error(w, "invalid encoding-type parameter", http.StatusBadRequest)
+ return
+ }
+ } else {
+ params.marker = r.FormValue("marker")
+ }
bucketdir := "by_id/" + bucket
// walkpath is the directory (relative to bucketdir) we need
@@ -588,33 +656,16 @@ func (h *handler) s3list(bucket string, w http.ResponseWriter, r *http.Request,
walkpath = ""
}
- type commonPrefix struct {
- Prefix string
- }
- type listResp struct {
- XMLName string `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult"`
- s3.ListResp
- // s3.ListResp marshals an empty tag when
- // CommonPrefixes is nil, which confuses some clients.
- // Fix by using this nested struct instead.
- CommonPrefixes []commonPrefix
- // Similarly, we need omitempty here, because an empty
- // tag confuses some clients (e.g.,
- // github.com/aws/aws-sdk-net never terminates its
- // paging loop).
- NextMarker string `xml:"NextMarker,omitempty"`
- // ListObjectsV2 has a KeyCount response field.
- KeyCount int
- }
- resp := listResp{
- ListResp: s3.ListResp{
- Name: bucket,
- Prefix: params.prefix,
- Delimiter: params.delimiter,
- Marker: params.marker,
- MaxKeys: params.maxKeys,
- },
- }
+ resp := listV2Resp{
+ Name: bucket,
+ Prefix: params.prefix,
+ Delimiter: params.delimiter,
+ MaxKeys: params.maxKeys,
+ ContinuationToken: r.FormValue("continuation-token"),
+ StartAfter: params.startAfter,
+ }
+ nextMarker := ""
+
commonPrefixes := map[string]bool{}
err := walkFS(fs, strings.TrimSuffix(bucketdir+"/"+walkpath, "/"), true, func(path string, fi os.FileInfo) error {
if path == bucketdir {
@@ -654,7 +705,7 @@ func (h *handler) s3list(bucket string, w http.ResponseWriter, r *http.Request,
return errDone
}
}
- if path < params.marker || path < params.prefix {
+ if path < params.marker || path < params.prefix || path <= params.startAfter {
return nil
}
if fi.IsDir() && !h.Config.cluster.Collections.S3FolderObjects {
@@ -665,6 +716,13 @@ func (h *handler) s3list(bucket string, w http.ResponseWriter, r *http.Request,
// finding a regular file inside it.
return nil
}
+ if len(resp.Contents)+len(commonPrefixes) >= params.maxKeys {
+ resp.IsTruncated = true
+ if params.delimiter != "" || params.v2 {
+ nextMarker = path
+ }
+ return errDone
+ }
if params.delimiter != "" {
idx := strings.Index(path[len(params.prefix):], params.delimiter)
if idx >= 0 {
@@ -676,13 +734,6 @@ func (h *handler) s3list(bucket string, w http.ResponseWriter, r *http.Request,
return filepath.SkipDir
}
}
- if len(resp.Contents)+len(commonPrefixes) >= params.maxKeys {
- resp.IsTruncated = true
- if params.delimiter != "" {
- resp.NextMarker = path
- }
- return errDone
- }
resp.Contents = append(resp.Contents, s3.Key{
Key: path,
LastModified: fi.ModTime().UTC().Format("2006-01-02T15:04:05.999") + "Z",
@@ -702,9 +753,66 @@ func (h *handler) s3list(bucket string, w http.ResponseWriter, r *http.Request,
sort.Slice(resp.CommonPrefixes, func(i, j int) bool { return resp.CommonPrefixes[i].Prefix < resp.CommonPrefixes[j].Prefix })
}
resp.KeyCount = len(resp.Contents)
+ var respV1orV2 interface{}
+
+ if params.encodingTypeURL {
+ // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
+ // "If you specify the encoding-type request
+ // parameter, Amazon S3 includes this element in the
+ // response, and returns encoded key name values in
+ // the following response elements:
+ //
+ // Delimiter, Prefix, Key, and StartAfter.
+ //
+ // Type: String
+ //
+ // Valid Values: url"
+ //
+ // This is somewhat vague but in practice it appears
+ // to mean x-www-form-urlencoded as in RFC1866 8.2.1
+ // para 1 (encode space as "+") rather than straight
+ // percent-encoding as in RFC1738 2.2. Presumably,
+ // the intent is to allow the client to decode XML and
+ // then paste the strings directly into another URI
+ // query or POST form like "https://host/path?foo=" +
+ // foo + "&bar=" + bar.
+ resp.EncodingType = "url"
+ resp.Delimiter = url.QueryEscape(resp.Delimiter)
+ resp.Prefix = url.QueryEscape(resp.Prefix)
+ resp.StartAfter = url.QueryEscape(resp.StartAfter)
+ for i, ent := range resp.Contents {
+ ent.Key = url.QueryEscape(ent.Key)
+ resp.Contents[i] = ent
+ }
+ for i, ent := range resp.CommonPrefixes {
+ ent.Prefix = url.QueryEscape(ent.Prefix)
+ resp.CommonPrefixes[i] = ent
+ }
+ }
+
+ if params.v2 {
+ resp.NextContinuationToken = base64.StdEncoding.EncodeToString([]byte(nextMarker))
+ respV1orV2 = resp
+ } else {
+ respV1orV2 = listV1Resp{
+ CommonPrefixes: resp.CommonPrefixes,
+ NextMarker: nextMarker,
+ KeyCount: resp.KeyCount,
+ ListResp: s3.ListResp{
+ IsTruncated: resp.IsTruncated,
+ Name: bucket,
+ Prefix: params.prefix,
+ Delimiter: params.delimiter,
+ Marker: params.marker,
+ MaxKeys: params.maxKeys,
+ Contents: resp.Contents,
+ },
+ }
+ }
+
w.Header().Set("Content-Type", "application/xml")
io.WriteString(w, xml.Header)
- if err := xml.NewEncoder(w).Encode(resp); err != nil {
+ if err := xml.NewEncoder(w).Encode(respV1orV2); err != nil {
ctxlog.FromContext(r.Context()).WithError(err).Error("error writing xml response")
}
}
diff --git a/services/keep-web/s3_test.go b/services/keep-web/s3_test.go
index e60b55c93..4f70168b5 100644
--- a/services/keep-web/s3_test.go
+++ b/services/keep-web/s3_test.go
@@ -6,6 +6,7 @@ package main
import (
"bytes"
+ "context"
"crypto/rand"
"crypto/sha256"
"fmt"
@@ -25,6 +26,10 @@ import (
"git.arvados.org/arvados.git/sdk/go/keepclient"
"github.com/AdRoll/goamz/aws"
"github.com/AdRoll/goamz/s3"
+ aws_aws "github.com/aws/aws-sdk-go/aws"
+ aws_credentials "github.com/aws/aws-sdk-go/aws/credentials"
+ aws_session "github.com/aws/aws-sdk-go/aws/session"
+ aws_s3 "github.com/aws/aws-sdk-go/service/s3"
check "gopkg.in/check.v1"
)
@@ -886,6 +891,196 @@ func (s *IntegrationSuite) testS3CollectionListRollup(c *check.C) {
}
}
+func (s *IntegrationSuite) TestS3ListObjectsV2(c *check.C) {
+ stage := s.s3setup(c)
+ defer stage.teardown(c)
+ dirs := 2
+ filesPerDir := 40
+ stage.writeBigDirs(c, dirs, filesPerDir)
+
+ sess := aws_session.Must(aws_session.NewSession(&aws_aws.Config{
+ Region: aws_aws.String("auto"),
+ Endpoint: aws_aws.String("http://" + s.testServer.Addr),
+ Credentials: aws_credentials.NewStaticCredentials(url.QueryEscape(arvadostest.ActiveTokenV2), url.QueryEscape(arvadostest.ActiveTokenV2), ""),
+ S3ForcePathStyle: aws_aws.Bool(true),
+ }))
+
+ stringOrNil := func(s string) *string {
+ if s == "" {
+ return nil
+ } else {
+ return &s
+ }
+ }
+
+ client := aws_s3.New(sess)
+ ctx := context.Background()
+
+ for _, trial := range []struct {
+ prefix string
+ delimiter string
+ startAfter string
+ maxKeys int
+ expectKeys int
+ expectCommonPrefixes map[string]bool
+ }{
+ {
+ // Expect {filesPerDir plus the dir itself}
+ // for each dir, plus emptydir, emptyfile, and
+ // sailboat.txt.
+ expectKeys: (filesPerDir+1)*dirs + 3,
+ },
+ {
+ maxKeys: 15,
+ expectKeys: (filesPerDir+1)*dirs + 3,
+ },
+ {
+ startAfter: "dir0/z",
+ maxKeys: 15,
+ // Expect {filesPerDir plus the dir itself}
+ // for each dir except dir0, plus emptydir,
+ // emptyfile, and sailboat.txt.
+ expectKeys: (filesPerDir+1)*(dirs-1) + 3,
+ },
+ {
+ maxKeys: 1,
+ delimiter: "/",
+ expectKeys: 2, // emptyfile, sailboat.txt
+ expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true, "emptydir/": true},
+ },
+ {
+ startAfter: "dir0/z",
+ maxKeys: 15,
+ delimiter: "/",
+ expectKeys: 2, // emptyfile, sailboat.txt
+ expectCommonPrefixes: map[string]bool{"dir1/": true, "emptydir/": true},
+ },
+ {
+ startAfter: "dir0/file10.txt",
+ maxKeys: 15,
+ delimiter: "/",
+ expectKeys: 2,
+ expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true, "emptydir/": true},
+ },
+ {
+ startAfter: "dir0/file10.txt",
+ maxKeys: 15,
+ prefix: "d",
+ delimiter: "/",
+ expectKeys: 0,
+ expectCommonPrefixes: map[string]bool{"dir0/": true, "dir1/": true},
+ },
+ } {
+ c.Logf("[trial %+v]", trial)
+ params := aws_s3.ListObjectsV2Input{
+ Bucket: aws_aws.String(stage.collbucket.Name),
+ Prefix: stringOrNil(trial.prefix),
+ Delimiter: stringOrNil(trial.delimiter),
+ StartAfter: stringOrNil(trial.startAfter),
+ MaxKeys: aws_aws.Int64(int64(trial.maxKeys)),
+ }
+ keySeen := map[string]bool{}
+ prefixSeen := map[string]bool{}
+ for {
+ result, err := client.ListObjectsV2WithContext(ctx, ¶ms)
+ if !c.Check(err, check.IsNil) {
+ break
+ }
+ c.Check(result.Name, check.DeepEquals, aws_aws.String(stage.collbucket.Name))
+ c.Check(result.Prefix, check.DeepEquals, aws_aws.String(trial.prefix))
+ c.Check(result.Delimiter, check.DeepEquals, aws_aws.String(trial.delimiter))
+ // The following two fields are expected to be
+ // nil (i.e., no tag in XML response) rather
+ // than "" when the corresponding request
+ // field was empty or nil.
+ c.Check(result.StartAfter, check.DeepEquals, stringOrNil(trial.startAfter))
+ c.Check(result.ContinuationToken, check.DeepEquals, params.ContinuationToken)
+
+ if trial.maxKeys > 0 {
+ c.Check(result.MaxKeys, check.DeepEquals, aws_aws.Int64(int64(trial.maxKeys)))
+ c.Check(len(result.Contents)+len(result.CommonPrefixes) <= trial.maxKeys, check.Equals, true)
+ } else {
+ c.Check(result.MaxKeys, check.DeepEquals, aws_aws.Int64(int64(s3MaxKeys)))
+ }
+
+ for _, ent := range result.Contents {
+ c.Assert(ent.Key, check.NotNil)
+ c.Check(*ent.Key > trial.startAfter, check.Equals, true)
+ c.Check(keySeen[*ent.Key], check.Equals, false, check.Commentf("dup key %q", *ent.Key))
+ keySeen[*ent.Key] = true
+ }
+ for _, ent := range result.CommonPrefixes {
+ c.Assert(ent.Prefix, check.NotNil)
+ c.Check(strings.HasSuffix(*ent.Prefix, trial.delimiter), check.Equals, true, check.Commentf("bad CommonPrefix %q", *ent.Prefix))
+ if strings.HasPrefix(trial.startAfter, *ent.Prefix) {
+ // If we asked for
+ // startAfter=dir0/file10.txt,
+ // we expect dir0/ to be
+ // returned as a common prefix
+ } else {
+ c.Check(*ent.Prefix > trial.startAfter, check.Equals, true)
+ }
+ c.Check(prefixSeen[*ent.Prefix], check.Equals, false, check.Commentf("dup common prefix %q", *ent.Prefix))
+ prefixSeen[*ent.Prefix] = true
+ }
+ if *result.IsTruncated && c.Check(result.NextContinuationToken, check.Not(check.Equals), "") {
+ params.ContinuationToken = aws_aws.String(*result.NextContinuationToken)
+ } else {
+ break
+ }
+ }
+ c.Check(keySeen, check.HasLen, trial.expectKeys)
+ c.Check(prefixSeen, check.HasLen, len(trial.expectCommonPrefixes))
+ if len(trial.expectCommonPrefixes) > 0 {
+ c.Check(prefixSeen, check.DeepEquals, trial.expectCommonPrefixes)
+ }
+ }
+}
+
+func (s *IntegrationSuite) TestS3ListObjectsV2EncodingTypeURL(c *check.C) {
+ stage := s.s3setup(c)
+ defer stage.teardown(c)
+ dirs := 2
+ filesPerDir := 40
+ stage.writeBigDirs(c, dirs, filesPerDir)
+
+ sess := aws_session.Must(aws_session.NewSession(&aws_aws.Config{
+ Region: aws_aws.String("auto"),
+ Endpoint: aws_aws.String("http://" + s.testServer.Addr),
+ Credentials: aws_credentials.NewStaticCredentials(url.QueryEscape(arvadostest.ActiveTokenV2), url.QueryEscape(arvadostest.ActiveTokenV2), ""),
+ S3ForcePathStyle: aws_aws.Bool(true),
+ }))
+
+ client := aws_s3.New(sess)
+ ctx := context.Background()
+
+ result, err := client.ListObjectsV2WithContext(ctx, &aws_s3.ListObjectsV2Input{
+ Bucket: aws_aws.String(stage.collbucket.Name),
+ Prefix: aws_aws.String("dir0/"),
+ Delimiter: aws_aws.String("/"),
+ StartAfter: aws_aws.String("dir0/"),
+ EncodingType: aws_aws.String("url"),
+ })
+ c.Assert(err, check.IsNil)
+ c.Check(*result.Prefix, check.Equals, "dir0%2F")
+ c.Check(*result.Delimiter, check.Equals, "%2F")
+ c.Check(*result.StartAfter, check.Equals, "dir0%2F")
+ for _, ent := range result.Contents {
+ c.Check(*ent.Key, check.Matches, "dir0%2F.*")
+ }
+ result, err = client.ListObjectsV2WithContext(ctx, &aws_s3.ListObjectsV2Input{
+ Bucket: aws_aws.String(stage.collbucket.Name),
+ Delimiter: aws_aws.String("/"),
+ EncodingType: aws_aws.String("url"),
+ })
+ c.Assert(err, check.IsNil)
+ c.Check(*result.Delimiter, check.Equals, "%2F")
+ c.Check(result.CommonPrefixes, check.HasLen, dirs+1)
+ for _, ent := range result.CommonPrefixes {
+ c.Check(*ent.Prefix, check.Matches, ".*%2F")
+ }
+}
+
// TestS3cmd checks compatibility with the s3cmd command line tool, if
// it's installed. As of Debian buster, s3cmd is only in backports, so
// `arvados-server install` don't install it, and this test skips if
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list