[
  {
    "path": ".github/workflows/go.yml",
    "content": "name: Go\n\non:\n  push:\n    branches: [ main, v1, v2 ]\n  pull_request:\n    branches: [ main, v1, v2 ]\n\njobs:\n  localstack:\n    runs-on: ubuntu-latest\n    services:\n      minio:\n        image: localstack/localstack:0.14.0\n        ports:\n          - \"4566:4566\"\n          - \"4571:4571\"\n        env:\n          SERVICES: s3\n\n    steps:\n    - uses: actions/checkout@v2\n\n    - name: Set up Go\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.21\n    \n    - name: Wait for localstack \n      run: 'for i in {1..20}; do sleep 3 && curl --silent --fail http://localhost:4566/health | grep \"\\\"s3\\\": \\\"available\\\"\" > /dev/null && break; done'\n\n    - name: Test\n      run: go test -v -endpoint='http://localhost:4566' -cover\n\n  minio:\n    runs-on: ubuntu-latest\n    \n    steps:\n    - uses: actions/checkout@v2\n\n    - name: Set up Go\n      uses: actions/setup-go@v2\n      with:\n        go-version: 1.21\n    \n    - name: Test\n      env:\n          SERVER_ENDPOINT: http://localhost:9000\n          ACCESS_KEY: minioadmin\n          SECRET_KEY: minioadmin\n          MINIO_ACCESS_KEY: minioadmin\n          MINIO_SECRET_KEY: minioadmin\n          S3FS_TEST_AWS_ACCESS_KEY_ID: minioadmin\n          S3FS_TEST_AWS_SECRET_ACCESS_KEY: minioadmin\n      run: |\n          wget -O /tmp/minio -q https://dl.minio.io/server/minio/release/linux-amd64/minio\n          chmod +x /tmp/minio\n          /tmp/minio server /tmp/data &\n          go test -v -endpoint='http://localhost:9000' -cover\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Jacek Szwec\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# s3fs [![Go Reference](https://pkg.go.dev/badge/github.com/jszwec/s3fs.svg)](https://pkg.go.dev/github.com/jszwec/s3fs) ![Go](https://github.com/jszwec/s3fs/workflows/Go/badge.svg?branch=main)\n\nPackage s3fs provides a S3 implementation for Go1.16 [filesystem](https://tip.golang.org/pkg/io/fs/#FS) interface.\n\nSince S3 is a flat structure, s3fs simulates directories by using\nprefixes and \"/\" delim. ModTime on directories is always zero value.\n\n# SDK Versions\n```github.com/jszwec/s3fs``` uses aws sdk v1\n\n```github.com/jszwec/s3fs/v2``` uses aws sdk v2\n\n\n# Example (SDK v1)\n```go\nconst bucket = \"my-bucket\"\n\ns, err := session.NewSession()\nif err != nil {\n    log.Fatal(err)\n}\n\ns3fs := s3fs.New(s3.New(s), bucket)\n\n// print out all files in s3 bucket.\n_ = fs.WalkDir(s3fs, \".\", func(path string, d fs.DirEntry, err error) error {\n    if err != nil {\n        return err\n    }\n\n    if d.IsDir() {\n        fmt.Println(\"dir:\", path)\n        return nil\n    }\n    fmt.Println(\"file:\", path)\n    return nil\n})\n```\n\n# Installation\n\n```\ngo get github.com/jszwec/s3fs\n```\n\n# Requirements\n\n* Go1.16+\n"
  },
  {
    "path": "dir.go",
    "content": "package s3fs\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"io/fs\"\n\t\"path\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n)\n\nvar _ fs.ReadDirFile = (*dir)(nil)\n\ntype dir struct {\n\tfileInfo\n\ts3cl   Client\n\tbucket string\n\tmarker *string\n\tdone   bool\n\tbuf    []fs.DirEntry\n\tdirs   map[dirEntry]bool\n}\n\nfunc (d *dir) Stat() (fs.FileInfo, error) {\n\treturn &d.fileInfo, nil\n}\n\nfunc (d *dir) Read([]byte) (int, error) {\n\treturn 0, &fs.PathError{\n\t\tOp:   \"read\",\n\t\tPath: d.name,\n\t\tErr:  errors.New(\"is a directory\"),\n\t}\n}\n\nfunc (d *dir) Close() error {\n\treturn nil\n}\n\nfunc (d *dir) ReadDir(n int) (des []fs.DirEntry, err error) {\n\tif n <= 0 {\n\t\tswitch err := d.readAll(); {\n\t\tcase err == nil:\n\t\tcase errors.Is(err, io.EOF):\n\t\t\treturn []fs.DirEntry{}, nil\n\t\tdefault:\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdes, d.buf = d.buf, nil\n\t\treturn des, nil\n\t}\n\nloop:\n\tfor len(d.buf) < n {\n\t\tswitch err := d.readNext(); {\n\t\tcase err == nil:\n\t\t\tcontinue\n\t\tcase errors.Is(err, io.EOF):\n\t\t\tbreak loop\n\t\tdefault:\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\toffset := min(n, len(d.buf))\n\tdes, d.buf = d.buf[:offset:offset], d.buf[offset:]\n\n\tif d.done && len(d.buf) == 0 {\n\t\terr = io.EOF\n\t}\n\n\treturn des, err\n}\n\nfunc (d *dir) readAll() error {\n\tfor !d.done {\n\t\tswitch err := d.readNext(); {\n\t\tcase err == nil:\n\t\t\tcontinue\n\t\tcase errors.Is(err, io.EOF):\n\t\t\treturn nil\n\t\tdefault:\n\t\t\treturn err\n\t\t}\n\t}\n\treturn io.EOF\n}\n\nfunc (d *dir) readNext() error {\n\tif d.done {\n\t\treturn io.EOF\n\t}\n\n\tname := strings.TrimRight(d.name, \"/\")\n\tswitch {\n\tcase name == \".\":\n\t\tname = \"\"\n\tdefault:\n\t\tname += \"/\"\n\t}\n\n\tout, err := d.s3cl.ListObjects(\n\t\tcontext.Background(),\n\t\t&s3.ListObjectsInput{\n\t\t\tBucket:    &d.bucket,\n\t\t\tDelimiter: ptr(\"/\"),\n\t\t\tPrefix:    &name,\n\t\t\tMarker:    d.marker,\n\t\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif d.name != \".\" && len(out.CommonPrefixes)+len(out.Contents) == 0 {\n\t\treturn &fs.PathError{\n\t\t\tOp:   \"readdir\",\n\t\t\tPath: strings.TrimSuffix(name, \"/\"),\n\t\t\tErr:  fs.ErrNotExist,\n\t\t}\n\t}\n\n\td.marker = out.NextMarker\n\td.done = out.IsTruncated != nil && !(*out.IsTruncated)\n\n\tif d.dirs == nil {\n\t\td.dirs = make(map[dirEntry]bool)\n\t}\n\n\tfor _, p := range out.CommonPrefixes {\n\t\tif p.Prefix == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tde := dirEntry{\n\t\t\tfileInfo: fileInfo{\n\t\t\t\tname: path.Base(*p.Prefix),\n\t\t\t\tmode: fs.ModeDir,\n\t\t\t},\n\t\t}\n\n\t\tif _, ok := d.dirs[de]; !ok {\n\t\t\td.dirs[de] = false\n\t\t}\n\t}\n\n\tfor _, o := range out.Contents {\n\t\tif o.Key == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\td.buf = append(d.buf, dirEntry{\n\t\t\tfileInfo: fileInfo{\n\t\t\t\tname:    path.Base(*o.Key),\n\t\t\t\tsize:    derefInt64(o.Size),\n\t\t\t\tmodTime: derefTime(o.LastModified),\n\t\t\t},\n\t\t})\n\t}\n\n\td.mergeDirFiles()\n\n\tif d.done {\n\t\treturn io.EOF\n\t}\n\treturn nil\n}\n\nfunc (d *dir) mergeDirFiles() {\n\tif d.buf == nil {\n\t\t// according to fs docs ReadDir should never return nil slice,\n\t\t// so we set it here.\n\t\td.buf = []fs.DirEntry{}\n\t}\n\n\t// we need a current len for sort.Search that doesn't change; otherwise\n\t// we could not append to the same slice.\n\tl := len(d.buf)\n\tfor de, used := range d.dirs {\n\t\tif used {\n\t\t\tcontinue\n\t\t}\n\n\t\ti := sort.Search(l, func(i int) bool {\n\t\t\treturn d.buf[i].Name() >= de.Name()\n\t\t})\n\n\t\tif i == l && !d.done {\n\t\t\tcontinue\n\t\t}\n\t\td.buf = append(d.buf, de)\n\t\td.dirs[de] = true\n\t}\n\n\tsort.Slice(d.buf, func(i, j int) bool {\n\t\treturn d.buf[i].Name() < d.buf[j].Name()\n\t})\n}\n\ntype dirEntry struct {\n\tfileInfo\n}\n\nfunc (de dirEntry) Type() fs.FileMode          { return de.Mode().Type() }\nfunc (de dirEntry) Info() (fs.FileInfo, error) { return de.fileInfo, nil }\n\nfunc min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc derefInt64(n *int64) int64 {\n\tif n != nil {\n\t\treturn *n\n\t}\n\treturn 0\n}\n\nfunc derefTime(t *time.Time) time.Time {\n\tif t != nil {\n\t\treturn *t\n\t}\n\treturn time.Time{}\n}\n"
  },
  {
    "path": "file.go",
    "content": "package s3fs\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"path\"\n\t\"time\"\n\n\tawshttp \"github.com/aws/aws-sdk-go-v2/aws/transport/http\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n)\n\nvar (\n\t_ fs.File     = (*file)(nil)\n\t_ fs.FileInfo = (*fileInfo)(nil)\n\t_ io.Seeker   = (*file)(nil)\n)\n\ntype file struct {\n\tcl     Client\n\tbucket string\n\tname   string\n\n\tio.ReadCloser\n\tstat   func() (fs.FileInfo, error)\n\toffset int64\n\teTag   string\n}\n\nfunc openFile(cl Client, bucket string, name string) (fs.File, error) {\n\tout, err := cl.GetObject(context.Background(), &s3.GetObjectInput{\n\t\tKey:    &name,\n\t\tBucket: &bucket,\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstatFunc := getStatFunc(cl, bucket, name, *out)\n\n\treturn &file{\n\t\tcl:         cl,\n\t\tbucket:     bucket,\n\t\tname:       name,\n\t\tReadCloser: out.Body,\n\t\tstat:       statFunc,\n\t\toffset:     0,\n\t\teTag:       *out.ETag,\n\t}, nil\n}\n\nfunc getStatFunc(cl Client, bucket string, name string, s3ObjOutput s3.GetObjectOutput) func() (fs.FileInfo, error) {\n\tstatFunc := func() (fs.FileInfo, error) {\n\t\treturn stat(cl, bucket, name)\n\t}\n\n\tif s3ObjOutput.ContentLength != nil && s3ObjOutput.LastModified != nil {\n\t\t// if we got all the information from GetObjectOutput\n\t\t// then we can cache fileinfo instead of making\n\t\t// another call in case Stat is called.\n\t\tstatFunc = func() (fs.FileInfo, error) {\n\t\t\treturn &fileInfo{\n\t\t\t\tname:    path.Base(name),\n\t\t\t\tsize:    *s3ObjOutput.ContentLength,\n\t\t\t\tmodTime: *s3ObjOutput.LastModified,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\treturn statFunc\n}\n\nfunc (f *file) Read(p []byte) (int, error) {\n\tn, err := f.ReadCloser.Read(p)\n\tf.offset += int64(n)\n\treturn n, err\n}\n\nfunc (f *file) Seek(offset int64, whence int) (int64, error) {\n\tnewOffset := f.offset\n\n\tstat, err := f.Stat()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tsize := stat.Size()\n\n\tswitch whence {\n\tcase io.SeekStart:\n\t\tnewOffset = offset\n\tcase io.SeekCurrent:\n\t\tnewOffset += offset\n\tcase io.SeekEnd:\n\t\tnewOffset = size + offset\n\tdefault:\n\t\treturn 0, errors.New(\"s3fs.file.Seek: invalid whence\")\n\t}\n\n\t// If the position has not moved, there is no need to make a new query\n\tif f.offset == newOffset {\n\t\treturn newOffset, nil\n\t}\n\n\tif newOffset < 0 {\n\t\treturn 0, errors.New(\"s3fs.file.Seek: seeked to a negative position\")\n\t}\n\n\tif f.eTag == \"\" {\n\t\treturn 0, errors.New(\"s3fs.file.Seek: cannot seek. remote file has no etag\")\n\t}\n\n\tif err := f.Close(); err != nil {\n\t\treturn f.offset, err\n\t}\n\n\tif newOffset >= size {\n\t\tf.ReadCloser = io.NopCloser(eofReader{})\n\t\tf.offset = newOffset\n\t\treturn f.offset, nil\n\t}\n\n\trawObject, err := f.cl.GetObject(\n\t\tcontext.Background(),\n\t\t&s3.GetObjectInput{\n\t\t\tBucket:  &f.bucket,\n\t\t\tKey:     &f.name,\n\t\t\tRange:   ptr(fmt.Sprintf(\"bytes=%d-\", newOffset)),\n\t\t\tIfMatch: &f.eTag,\n\t\t})\n\n\tif err != nil {\n\t\tif e := new(awshttp.ResponseError); errors.As(err, &e) {\n\t\t\tif e.HTTPStatusCode() == http.StatusPreconditionFailed {\n\t\t\t\treturn 0, fmt.Errorf(\"s3fs.file.Seek: file has changed while seeking: %w\", fs.ErrNotExist)\n\t\t\t}\n\t\t}\n\t\treturn 0, err\n\t}\n\n\tf.offset = newOffset\n\tf.ReadCloser = rawObject.Body\n\n\treturn f.offset, nil\n}\n\nfunc (f file) Stat() (fs.FileInfo, error) { return f.stat() }\n\ntype fileInfo struct {\n\tname    string\n\tsize    int64\n\tmode    fs.FileMode\n\tmodTime time.Time\n}\n\nfunc (fi fileInfo) Name() string       { return path.Base(fi.name) }\nfunc (fi fileInfo) Size() int64        { return fi.size }\nfunc (fi fileInfo) Mode() fs.FileMode  { return fi.mode }\nfunc (fi fileInfo) ModTime() time.Time { return fi.modTime }\nfunc (fi fileInfo) IsDir() bool        { return fi.mode.IsDir() }\nfunc (fi fileInfo) Sys() interface{}   { return nil }\n\ntype eofReader struct{}\n\nfunc (eofReader) Read([]byte) (int, error) { return 0, io.EOF }\n\nfunc ptr[T any](v T) *T {\n\treturn &v\n}\n"
  },
  {
    "path": "fs.go",
    "content": "// Package s3fs provides a S3 implementation for Go1.16 filesystem interface.\npackage s3fs\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io/fs\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws/transport/http\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n)\n\nvar (\n\t_ fs.FS        = (*S3FS)(nil)\n\t_ fs.StatFS    = (*S3FS)(nil)\n\t_ fs.ReadDirFS = (*S3FS)(nil)\n)\n\nvar errNotDir = errors.New(\"not a dir\")\n\n// Option is a function that provides optional features to S3FS.\ntype Option func(*S3FS)\n\n// WithReadSeeker enables Seek functionality on files opened with this fs.\n//\n// BUG(WilliamFrei): Seeking on S3 requires reopening the file at the specified\n// position. This can cause problems if the file changed between opening\n// and calling Seek. In that case, fs.ErrNotExist error is returned, which\n// has to be handled by the caller.\nfunc WithReadSeeker(fsys *S3FS) { fsys.readSeeker = true }\n\n// Client wraps the s3 client methods that this package is using.\n// This interface may change in the future and should not be relied on by\n// packages using it.\ntype Client interface {\n\tHeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)\n\tListObjects(ctx context.Context, params *s3.ListObjectsInput, optFns ...func(*s3.Options)) (*s3.ListObjectsOutput, error)\n\tGetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)\n}\n\n// S3FS is a S3 filesystem implementation.\n//\n// S3 has a flat structure instead of a hierarchy. S3FS simulates directories\n// by using prefixes and delims (\"/\"). Because directories are simulated, ModTime\n// is always a default Time value (IsZero returns true).\ntype S3FS struct {\n\tcl         Client\n\tbucket     string\n\treadSeeker bool\n}\n\n// New returns a new filesystem that works on the specified bucket.\nfunc New(cl Client, bucket string, opts ...Option) *S3FS {\n\tfsys := &S3FS{\n\t\tcl:     cl,\n\t\tbucket: bucket,\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(fsys)\n\t}\n\n\treturn fsys\n}\n\n// Open implements fs.FS.\nfunc (f *S3FS) Open(name string) (fs.File, error) {\n\tif !fs.ValidPath(name) {\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"open\",\n\t\t\tPath: name,\n\t\t\tErr:  fs.ErrInvalid,\n\t\t}\n\t}\n\n\tif name == \".\" {\n\t\treturn openDir(f.cl, f.bucket, name)\n\t}\n\n\tfile, err := openFile(f.cl, f.bucket, name)\n\n\tif err != nil {\n\t\tif isNotFoundErr(err) {\n\t\t\tswitch d, err := openDir(f.cl, f.bucket, name); {\n\t\t\tcase err == nil:\n\t\t\t\treturn d, nil\n\t\t\tcase !isNotFoundErr(err) && !errors.Is(err, errNotDir) && !errors.Is(err, fs.ErrNotExist):\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn nil, &fs.PathError{\n\t\t\t\tOp:   \"open\",\n\t\t\t\tPath: name,\n\t\t\t\tErr:  fs.ErrNotExist,\n\t\t\t}\n\t\t}\n\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"open\",\n\t\t\tPath: name,\n\t\t\tErr:  err,\n\t\t}\n\t}\n\n\tif !f.readSeeker {\n\t\tfile = fileNoSeek{file}\n\t}\n\n\treturn file, nil\n}\n\n// Stat implements fs.StatFS.\nfunc (f *S3FS) Stat(name string) (fs.FileInfo, error) {\n\tfi, err := stat(f.cl, f.bucket, name)\n\tif err != nil {\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"stat\",\n\t\t\tPath: name,\n\t\t\tErr:  err,\n\t\t}\n\t}\n\treturn fi, nil\n}\n\n// ReadDir implements fs.ReadDirFS.\nfunc (f *S3FS) ReadDir(name string) ([]fs.DirEntry, error) {\n\td, err := openDir(f.cl, f.bucket, name)\n\tif err != nil {\n\t\treturn nil, &fs.PathError{\n\t\t\tOp:   \"readdir\",\n\t\t\tPath: name,\n\t\t\tErr:  err,\n\t\t}\n\t}\n\treturn d.ReadDir(-1)\n}\n\nfunc stat(s3cl Client, bucket, name string) (fs.FileInfo, error) {\n\tif !fs.ValidPath(name) {\n\t\treturn nil, fs.ErrInvalid\n\t}\n\n\tif name == \".\" {\n\t\treturn &dir{\n\t\t\ts3cl:   s3cl,\n\t\t\tbucket: bucket,\n\t\t\tfileInfo: fileInfo{\n\t\t\t\tname: \".\",\n\t\t\t\tmode: fs.ModeDir,\n\t\t\t},\n\t\t}, nil\n\t}\n\n\thead, err := s3cl.HeadObject(\n\t\tcontext.Background(),\n\t\t&s3.HeadObjectInput{\n\t\t\tBucket: &bucket,\n\t\t\tKey:    &name,\n\t\t})\n\tif err != nil {\n\t\tif !isNotFoundErr(err) {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\treturn &fileInfo{\n\t\t\tname:    name,\n\t\t\tsize:    derefInt64(head.ContentLength),\n\t\t\tmode:    0,\n\t\t\tmodTime: derefTime(head.LastModified),\n\t\t}, nil\n\t}\n\n\tout, err := s3cl.ListObjects(\n\t\tcontext.Background(),\n\t\t&s3.ListObjectsInput{\n\t\t\tBucket:    &bucket,\n\t\t\tDelimiter: ptr(\"/\"),\n\t\t\tPrefix:    ptr(name + \"/\"),\n\t\t\tMaxKeys:   ptr[int32](1),\n\t\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(out.CommonPrefixes) > 0 || len(out.Contents) > 0 {\n\t\treturn &dir{\n\t\t\ts3cl:   s3cl,\n\t\t\tbucket: bucket,\n\t\t\tfileInfo: fileInfo{\n\t\t\t\tname: name,\n\t\t\t\tmode: fs.ModeDir,\n\t\t\t},\n\t\t}, nil\n\t}\n\treturn nil, fs.ErrNotExist\n}\n\nfunc openDir(s3cl Client, bucket, name string) (fs.ReadDirFile, error) {\n\tfi, err := stat(s3cl, bucket, name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif d, ok := fi.(fs.ReadDirFile); ok {\n\t\treturn d, nil\n\t}\n\treturn nil, errNotDir\n}\n\nfunc isNotFoundErr(err error) bool {\n\tif e := new(types.NoSuchKey); errors.As(err, &e) {\n\t\treturn true\n\t}\n\n\tif e := new(http.ResponseError); errors.As(err, &e) {\n\t\t// localstack workaround\n\t\tif e.HTTPStatusCode() == 404 {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\ntype fileNoSeek struct{ fs.File }\n"
  },
  {
    "path": "fs_test.go",
    "content": "package s3fs_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"flag\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"testing/fstest\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/feature/s3/manager\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/jszwec/s3fs/v2\"\n)\n\nvar (\n\tendpoint   = flag.String(\"endpoint\", \"http://localhost:4566\", \"s3 endpoint\")\n\tbucket     = flag.String(\"bucket\", \"test-github.com-jszwec-s3fs\", \"bucket name\")\n\tskipVerify = flag.Bool(\"skip-verify\", true, \"http insecure skip verify\")\n)\n\nvar (\n\taccessKeyID = envDefault(\"S3FS_TEST_AWS_ACCESS_KEY_ID\", \"1\")\n\tsecretKey   = envDefault(\"S3FS_TEST_AWS_SECRET_ACCESS_KEY\", \"1\")\n\tregion      = envDefault(\"S3FS_TEST_AWS_REGION\", \"us-east-1\")\n)\n\nfunc TestMain(m *testing.M) {\n\tflag.Parse()\n\tos.Exit(m.Run())\n}\n\nfunc TestSeeker(t *testing.T) {\n\ts3cl, cl := newClient(t)\n\n\tconst testFile = \"file.txt\"\n\tcontent := []byte(\"content\")\n\n\tcreateBucket(t, s3cl, *bucket)\n\tcleanBucket(t, s3cl, *bucket)\n\n\twriteFile(t, s3cl, *bucket, testFile, content)\n\n\tt.Cleanup(func() {\n\t\tcleanBucket(t, s3cl, *bucket)\n\n\t\tt.Log(\"test stats:\")\n\t\tt.Log(\"ListObjects calls:\", atomic.LoadInt64(&listC))\n\t\tt.Log(\"GetObject calls:\", atomic.LoadInt64(&getC))\n\t})\n\n\tt.Run(\"'s3fs.New' does not implement Seeker\", func(t *testing.T) {\n\t\ttestFs := s3fs.New(cl, *bucket)\n\t\tdata, err := testFs.Open(testFile)\n\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t_, ok := data.(io.Seeker)\n\n\t\tif ok {\n\t\t\tt.Fatalf(\"Expected 'data' to not implement the Seeker interface\")\n\t\t}\n\t})\n\n\tt.Run(\"seek throws error if file changed\", func(t *testing.T) {\n\t\tconst otherTestFile = \"otherFile.txt\"\n\t\toriginalContent := []byte(\"con\")\n\t\tchangedContent := []byte(\"tent\")\n\n\t\twriteFile(t, s3cl, *bucket, otherTestFile, originalContent)\n\n\t\ttestFs := s3fs.New(s3cl, *bucket, s3fs.WithReadSeeker)\n\t\tdata, err := testFs.Open(otherTestFile)\n\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif _, err := data.(io.Seeker).Seek(0, io.SeekEnd); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tdeleteFile(t, s3cl, *bucket, otherTestFile)\n\t\twriteFile(t, s3cl, *bucket, otherTestFile, changedContent)\n\n\t\t_, err = data.(io.Seeker).Seek(0, io.SeekStart)\n\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\tt.Fatalf(\"want=%v; got %v\", fs.ErrNotExist, err)\n\t\t}\n\t})\n\n\tt.Run(\"seek once\", func(t *testing.T) {\n\t\tfixtures := []struct {\n\t\t\tdesc     string\n\t\t\toffset   int64\n\t\t\twhence   int\n\t\t\texpected int64\n\t\t}{\n\t\t\t{\n\t\t\t\tdesc:     \"whence SeekStart \",\n\t\t\t\toffset:   2,\n\t\t\t\twhence:   io.SeekStart,\n\t\t\t\texpected: 2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:     \"whence SeekCurrent\",\n\t\t\t\toffset:   4,\n\t\t\t\twhence:   io.SeekCurrent,\n\t\t\t\texpected: 4,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:     \"whence SeekEnd\",\n\t\t\t\toffset:   -1,\n\t\t\t\twhence:   io.SeekEnd,\n\t\t\t\texpected: int64(len(content)) - 1,\n\t\t\t},\n\t\t}\n\n\t\tfor _, f := range fixtures {\n\t\t\tf := f\n\t\t\tt.Run(f.desc, func(t *testing.T) {\n\t\t\t\ttestFs := s3fs.New(s3cl, *bucket, s3fs.WithReadSeeker)\n\t\t\t\tdata, err := testFs.Open(testFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tactual, err := data.(io.Seeker).Seek(f.offset, f.whence)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tif actual != f.expected {\n\t\t\t\t\tt.Fatalf(\"Expected %d, got %d\", f.expected, actual)\n\t\t\t\t}\n\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"seek with errors\", func(t *testing.T) {\n\t\tfixtures := []struct {\n\t\t\tdesc         string\n\t\t\toffset       int64\n\t\t\twhence       int\n\t\t\terrorMessage string\n\t\t}{\n\t\t\t{\n\t\t\t\tdesc:         \"seek before beginning with whence SeekCurrent\",\n\t\t\t\toffset:       -1,\n\t\t\t\twhence:       io.SeekCurrent,\n\t\t\t\terrorMessage: \"s3fs.file.Seek: seeked to a negative position\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"seek before beginning with whence SeekStart\",\n\t\t\t\toffset:       -1,\n\t\t\t\twhence:       io.SeekStart,\n\t\t\t\terrorMessage: \"s3fs.file.Seek: seeked to a negative position\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"seek with invalid whence\",\n\t\t\t\toffset:       0,\n\t\t\t\twhence:       3,\n\t\t\t\terrorMessage: \"s3fs.file.Seek: invalid whence\",\n\t\t\t},\n\t\t}\n\n\t\tfor _, f := range fixtures {\n\t\t\tf := f\n\t\t\tt.Run(f.desc, func(t *testing.T) {\n\t\t\t\ttestFs := s3fs.New(s3cl, *bucket, s3fs.WithReadSeeker)\n\t\t\t\tdata, err := testFs.Open(testFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\t_, err = data.(io.Seeker).Seek(f.offset, f.whence)\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Fatalf(\"Expected error after seeking to invalid position, got nil\")\n\t\t\t\t}\n\t\t\t\tif err.Error() != f.errorMessage {\n\t\t\t\t\tt.Fatalf(\"Expected %s, got %v\", f.errorMessage, err)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"seek from other starting position\", func(t *testing.T) {\n\t\tfixtures := []struct {\n\t\t\tdesc          string\n\t\t\tinitialOffset int\n\t\t\toffset        int64\n\t\t\twhence        int\n\t\t\texpected      int64\n\t\t}{\n\t\t\t{\n\t\t\t\tdesc:          \"whence SeekStart\",\n\t\t\t\tinitialOffset: 3,\n\t\t\t\toffset:        2,\n\t\t\t\twhence:        io.SeekStart,\n\t\t\t\texpected:      2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:          \"whence SeekCurrent\",\n\t\t\t\tinitialOffset: 3,\n\t\t\t\toffset:        3,\n\t\t\t\twhence:        io.SeekCurrent,\n\t\t\t\texpected:      6,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:          \"whence SeekEnd\",\n\t\t\t\tinitialOffset: 3,\n\t\t\t\toffset:        -1,\n\t\t\t\twhence:        io.SeekEnd,\n\t\t\t\texpected:      int64(len(content)) - 1,\n\t\t\t},\n\t\t}\n\n\t\tfor _, f := range fixtures {\n\t\t\tf := f\n\t\t\tt.Run(f.desc, func(t *testing.T) {\n\t\t\t\ttestFs := s3fs.New(s3cl, *bucket, s3fs.WithReadSeeker)\n\t\t\t\tdata, err := testFs.Open(testFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\treadBuffer := make([]byte, f.initialOffset)\n\t\t\t\treadBytes, err := data.Read(readBuffer)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\tif readBytes != f.initialOffset {\n\t\t\t\t\tt.Fatalf(\"Read failed during test setup\")\n\t\t\t\t}\n\n\t\t\t\tactual, err := data.(io.Seeker).Seek(f.offset, f.whence)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tif actual != f.expected {\n\t\t\t\t\tt.Fatalf(\"Expected %d, got %d\", f.expected, actual)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"seek then read\", func(t *testing.T) {\n\t\tfixtures := []struct {\n\t\t\tdesc         string\n\t\t\treadBytes    int\n\t\t\toffset       int64\n\t\t\twhence       int\n\t\t\texpected     []byte\n\t\t\texpectingEOF bool\n\t\t}{\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekStart\",\n\t\t\t\treadBytes:    3,\n\t\t\t\toffset:       2,\n\t\t\t\twhence:       io.SeekStart,\n\t\t\t\texpected:     content[2:5],\n\t\t\t\texpectingEOF: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekCurrent\",\n\t\t\t\treadBytes:    1,\n\t\t\t\toffset:       1,\n\t\t\t\twhence:       io.SeekCurrent,\n\t\t\t\texpected:     []byte(\"o\"),\n\t\t\t\texpectingEOF: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"seek to end then read 0\",\n\t\t\t\treadBytes:    0,\n\t\t\t\toffset:       0,\n\t\t\t\twhence:       io.SeekEnd,\n\t\t\t\texpected:     []byte(\"\"),\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekStart with EOF\",\n\t\t\t\treadBytes:    2,\n\t\t\t\toffset:       5,\n\t\t\t\twhence:       io.SeekStart,\n\t\t\t\texpected:     content[5:7],\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekCurrent with EOF\",\n\t\t\t\treadBytes:    3,\n\t\t\t\toffset:       4,\n\t\t\t\twhence:       io.SeekCurrent,\n\t\t\t\texpected:     content[4:7],\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekEnd with EOF\",\n\t\t\t\treadBytes:    3,\n\t\t\t\toffset:       -3,\n\t\t\t\twhence:       io.SeekEnd,\n\t\t\t\texpected:     content[len(content)-3:],\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"zero offset and read more than fits the buffer\",\n\t\t\t\treadBytes:    100,\n\t\t\t\toffset:       0,\n\t\t\t\twhence:       io.SeekStart,\n\t\t\t\texpected:     []byte(\"content\"),\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekStart offset and read more than fits the buffer\",\n\t\t\t\treadBytes:    100,\n\t\t\t\toffset:       1,\n\t\t\t\twhence:       io.SeekStart,\n\t\t\t\texpected:     []byte(\"ontent\"),\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekCurrent offset and read more than fits the buffer\",\n\t\t\t\treadBytes:    100,\n\t\t\t\toffset:       1,\n\t\t\t\twhence:       io.SeekCurrent,\n\t\t\t\texpected:     []byte(\"ontent\"),\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekEnd to the end of the file and then read\",\n\t\t\t\treadBytes:    10,\n\t\t\t\toffset:       0,\n\t\t\t\twhence:       io.SeekEnd,\n\t\t\t\texpected:     []byte(\"\"),\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekEnd past the end of the file and then read\",\n\t\t\t\treadBytes:    10,\n\t\t\t\toffset:       1,\n\t\t\t\twhence:       io.SeekEnd,\n\t\t\t\texpected:     []byte(\"\"),\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t}\n\n\t\tfor _, f := range fixtures {\n\t\t\tf := f\n\t\t\tt.Run(f.desc, func(t *testing.T) {\n\t\t\t\ttestFs := s3fs.New(s3cl, *bucket, s3fs.WithReadSeeker)\n\t\t\t\tdata, err := testFs.Open(testFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\treadSeekers := []struct {\n\t\t\t\t\tdesc   string\n\t\t\t\t\tseeker io.ReadSeeker\n\t\t\t\t}{\n\t\t\t\t\t{desc: \"file\", seeker: data.(io.ReadSeeker)},\n\t\t\t\t\t{desc: \"bytes reader\", seeker: bytes.NewReader(content)},\n\t\t\t\t}\n\n\t\t\t\tfor _, rs := range readSeekers {\n\t\t\t\t\trs := rs\n\t\t\t\t\tt.Run(rs.desc, func(t *testing.T) {\n\t\t\t\t\t\t_, err = rs.seeker.Seek(f.offset, f.whence)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar buf bytes.Buffer\n\t\t\t\t\t\t_, err := io.CopyN(&buf, rs.seeker, int64(f.readBytes))\n\t\t\t\t\t\tif err != nil && !errors.Is(err, io.EOF) {\n\t\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif buf.String() != string(f.expected) {\n\t\t\t\t\t\t\tt.Errorf(\"expected %s, got %s\", f.expected, buf.String())\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif f.expectingEOF {\n\t\t\t\t\t\t\tnewlyReadBytes, err := rs.seeker.Read(make([]byte, 0))\n\t\t\t\t\t\t\tif newlyReadBytes != 0 {\n\t\t\t\t\t\t\t\tt.Fatalf(\"Read returned unexpected number of bytes: expected 0, got %d\", newlyReadBytes)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tt.Fatalf(\"Expected io.EOF error, got nil\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif !errors.Is(err, io.EOF) {\n\t\t\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"seek twice then read\", func(t *testing.T) {\n\t\tfixtures := []struct {\n\t\t\tdesc         string\n\t\t\treadBytes    int\n\t\t\tfirstOffset  int64\n\t\t\tfirstWhence  int\n\t\t\tsecondOffset int64\n\t\t\texpected     []byte\n\t\t\texpectingEOF bool\n\t\t}{\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekStart\",\n\t\t\t\treadBytes:    2,\n\t\t\t\tfirstOffset:  1,\n\t\t\t\tfirstWhence:  io.SeekStart,\n\t\t\t\tsecondOffset: 2,\n\t\t\t\texpected:     content[3:5],\n\t\t\t\texpectingEOF: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekCurrent\",\n\t\t\t\treadBytes:    1,\n\t\t\t\tfirstOffset:  2,\n\t\t\t\tfirstWhence:  io.SeekCurrent,\n\t\t\t\tsecondOffset: 3,\n\t\t\t\texpected:     content[5:6],\n\t\t\t\texpectingEOF: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekEnd\",\n\t\t\t\treadBytes:    2,\n\t\t\t\tfirstOffset:  -4,\n\t\t\t\tfirstWhence:  io.SeekEnd,\n\t\t\t\tsecondOffset: 1,\n\t\t\t\texpected:     content[4:6],\n\t\t\t\texpectingEOF: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekStart with EOF\",\n\t\t\t\treadBytes:    5,\n\t\t\t\tfirstOffset:  1,\n\t\t\t\tfirstWhence:  io.SeekStart,\n\t\t\t\tsecondOffset: 2,\n\t\t\t\texpected:     content[3:],\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekCurrent with EOF\",\n\t\t\t\treadBytes:    2,\n\t\t\t\tfirstOffset:  2,\n\t\t\t\tfirstWhence:  io.SeekCurrent,\n\t\t\t\tsecondOffset: 3,\n\t\t\t\texpected:     content[5:],\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc:         \"whence SeekEnd with EOF\",\n\t\t\t\treadBytes:    7,\n\t\t\t\tfirstOffset:  -5,\n\t\t\t\tfirstWhence:  io.SeekEnd,\n\t\t\t\tsecondOffset: 1,\n\t\t\t\texpected:     content[3:],\n\t\t\t\texpectingEOF: true,\n\t\t\t},\n\t\t}\n\n\t\tfor _, f := range fixtures {\n\t\t\tf := f\n\t\t\tt.Run(f.desc, func(t *testing.T) {\n\t\t\t\ttestFs := s3fs.New(s3cl, *bucket, s3fs.WithReadSeeker)\n\t\t\t\tdata, err := testFs.Open(testFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\treadSeekers := []struct {\n\t\t\t\t\tdesc   string\n\t\t\t\t\tseeker io.ReadSeeker\n\t\t\t\t}{\n\t\t\t\t\t{desc: \"file\", seeker: data.(io.ReadSeeker)},\n\t\t\t\t\t{desc: \"bytes reader\", seeker: bytes.NewReader(content)},\n\t\t\t\t}\n\n\t\t\t\tfor _, rs := range readSeekers {\n\t\t\t\t\trs := rs\n\t\t\t\t\tt.Run(rs.desc, func(t *testing.T) {\n\t\t\t\t\t\t_, err = rs.seeker.Seek(f.firstOffset, f.firstWhence)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t_, err = rs.seeker.Seek(f.secondOffset, io.SeekCurrent)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar buf bytes.Buffer\n\t\t\t\t\t\t_, err := io.CopyN(&buf, rs.seeker, int64(f.readBytes))\n\t\t\t\t\t\tif err != nil && !errors.Is(err, io.EOF) {\n\t\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif buf.String() != string(f.expected) {\n\t\t\t\t\t\t\tt.Errorf(\"expected %s, got %s\", f.expected, buf.String())\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif f.expectingEOF {\n\t\t\t\t\t\t\tnewlyReadBytes, err := rs.seeker.Read(make([]byte, 0))\n\t\t\t\t\t\t\tif newlyReadBytes != 0 {\n\t\t\t\t\t\t\t\tt.Fatalf(\"Read returned unexpected number of bytes: expected 0, got %d\", newlyReadBytes)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\t\tt.Fatalf(\"Expected io.EOF error, got nil\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif !errors.Is(err, io.EOF) {\n\t\t\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestFS(t *testing.T) {\n\ts3cl, wrappedCl := newClient(t)\n\n\tconst testFile = \"file.txt\"\n\n\tcontent := []byte(\"content\")\n\n\tallFiles := [...]string{\n\t\ttestFile,\n\t\t\"dir/a.txt\",\n\t\t\"dir1/file1.txt\",\n\t\t\"dir1/file2.txt\",\n\t\t\"dir1/dir11/file.txt\",\n\t\t\"dir2/file1.txt\",\n\t\t\"x/file1.txt\",\n\t\t\"y.txt\",\n\t\t\"y2.txt\",\n\t\t\"y3.txt\",\n\t\t\"z/z/file1.txt\",\n\t\t\"a.txt\",\n\t\t\"a/b.txt\",\n\t}\n\n\tcreateBucket(t, s3cl, *bucket)\n\tcleanBucket(t, s3cl, *bucket)\n\n\tt.Run(\"list empty bucket\", func(t *testing.T) {\n\t\tfi, err := s3fs.New(wrappedCl, *bucket).Open(\".\")\n\t\tif err != nil {\n\t\t\tt.Errorf(\"want err to be nil; got %v\", err)\n\t\t}\n\n\t\tdir := fi.(fs.ReadDirFile)\n\t\tfixtures := []struct {\n\t\t\tdesc string\n\t\t\tn    int\n\t\t\terr  error\n\t\t}{\n\t\t\t{\"n > 0\", 1, io.EOF},\n\t\t\t{\"n <= 0\", -1, nil},\n\t\t}\n\n\t\tfor _, f := range fixtures {\n\t\t\tf := f\n\t\t\tt.Run(f.desc, func(t *testing.T) {\n\t\t\t\tdes, err := dir.ReadDir(f.n)\n\t\t\t\tif err != f.err {\n\t\t\t\t\tt.Errorf(\"want err to be %v; got %v\", f.err, err)\n\t\t\t\t}\n\n\t\t\t\tif des == nil {\n\t\t\t\t\tt.Error(\"want des to not be a nil slice\")\n\t\t\t\t}\n\n\t\t\t\tif len(des) > 0 {\n\t\t\t\t\tt.Errorf(\"expected the directory to be empty; got %d elements\", len(des))\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n\n\tfor _, f := range allFiles {\n\t\twriteFile(t, s3cl, *bucket, f, content)\n\t}\n\n\tt.Cleanup(func() {\n\t\tcleanBucket(t, s3cl, *bucket)\n\n\t\tt.Log(\"test stats:\")\n\t\tt.Log(\"ListObjects calls:\", atomic.LoadInt64(&listC))\n\t\tt.Log(\"GetObject calls:\", atomic.LoadInt64(&getC))\n\t})\n\n\ttestFn := func(t *testing.T, s3fs *s3fs.S3FS) {\n\t\tt.Run(\"testing fstest\", func(t *testing.T) {\n\t\t\tif testing.Short() {\n\t\t\t\tt.Skip(\"short test enabled\")\n\t\t\t}\n\n\t\t\tt.Parallel()\n\t\t\tif err := fstest.TestFS(s3fs, allFiles[:]...); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t})\n\n\t\tt.Run(\"readfile\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tt.Run(\"success\", func(t *testing.T) {\n\t\t\t\tdata, err := fs.ReadFile(s3fs, testFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tif !bytes.Equal(data, []byte(\"content\")) {\n\t\t\t\t\tt.Errorf(\"expect: %s; got %s\", data, []byte(\"content\"))\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"error\", func(t *testing.T) {\n\t\t\t\tt.Run(\"invalid path\", func(t *testing.T) {\n\t\t\t\t\t_, err := fs.ReadFile(s3fs, \"/\")\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tt.Fatal(\"expected error\")\n\t\t\t\t\t}\n\n\t\t\t\t\tvar pathErr *fs.PathError\n\t\t\t\t\tif !errors.As(err, &pathErr) {\n\t\t\t\t\t\tt.Fatal(\"expected err to be *PathError\")\n\t\t\t\t\t}\n\n\t\t\t\t\texpected := fs.PathError{\n\t\t\t\t\t\tOp:   \"open\",\n\t\t\t\t\t\tPath: \"/\",\n\t\t\t\t\t\tErr:  fs.ErrInvalid,\n\t\t\t\t\t}\n\t\t\t\t\tif *pathErr != expected {\n\t\t\t\t\t\tt.Fatalf(\"want %v; got %v\", expected, *pathErr)\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\tt.Run(\"directory\", func(t *testing.T) {\n\t\t\t\t\t_, err := fs.ReadFile(s3fs, \".\")\n\t\t\t\t\tif err == nil {\n\t\t\t\t\t\tt.Fatal(\"expected error\")\n\t\t\t\t\t}\n\n\t\t\t\t\tvar perr *fs.PathError\n\t\t\t\t\tif !errors.As(err, &perr) {\n\t\t\t\t\t\tt.Fatal(\"expected err to be *PathError\")\n\t\t\t\t\t}\n\n\t\t\t\t\tif perr.Op != \"read\" {\n\t\t\t\t\t\tt.Errorf(\"want %v; got %v\", \"read\", perr.Op)\n\t\t\t\t\t}\n\n\t\t\t\t\tif perr.Path != \".\" {\n\t\t\t\t\t\tt.Errorf(\"want %v; got %v\", \".\", perr.Path)\n\t\t\t\t\t}\n\n\t\t\t\t\tif perr.Err.Error() != \"is a directory\" {\n\t\t\t\t\t\tt.Errorf(\"want %v; got %v\", \"is a directory\", perr.Err.Error())\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\n\t\tt.Run(\"stat file\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttest := func(t *testing.T, fi fs.FileInfo) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tif fi.IsDir() {\n\t\t\t\t\tt.Error(\"expected false\")\n\t\t\t\t}\n\n\t\t\t\tif fi.Mode() != 0 {\n\t\t\t\t\tt.Errorf(\"want %d; got %d\", 0, fi.Mode())\n\t\t\t\t}\n\n\t\t\t\tif fi.Sys() != nil {\n\t\t\t\t\tt.Error(\"expected Sys to be nil\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tt.Run(\"file stat\", func(t *testing.T) {\n\t\t\t\tf, err := s3fs.Open(testFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"expected err to be nil\")\n\t\t\t\t}\n\t\t\t\tdefer f.Close()\n\n\t\t\t\tfi, err := f.Stat()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"expected err to be nil\")\n\t\t\t\t}\n\n\t\t\t\ttest(t, fi)\n\t\t\t})\n\n\t\t\tt.Run(\"fs stat\", func(t *testing.T) {\n\t\t\t\tfi, err := s3fs.Stat(testFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"expected err to be nil\")\n\t\t\t\t}\n\n\t\t\t\ttest(t, fi)\n\t\t\t})\n\n\t\t\tt.Run(\"invalid path\", func(t *testing.T) {\n\t\t\t\t_, err := s3fs.Stat(\"/\")\n\t\t\t\tvar pathErr *fs.PathError\n\t\t\t\tif !errors.As(err, &pathErr) {\n\t\t\t\t\tt.Fatal(\"expected err to be *PathError\")\n\t\t\t\t}\n\n\t\t\t\texpected := fs.PathError{\n\t\t\t\t\tOp:   \"stat\",\n\t\t\t\t\tPath: \"/\",\n\t\t\t\t\tErr:  fs.ErrInvalid,\n\t\t\t\t}\n\t\t\t\tif *pathErr != expected {\n\t\t\t\t\tt.Fatalf(\"want %v; got %v\", expected, *pathErr)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"does not exist\", func(t *testing.T) {\n\t\t\t\t_, err := s3fs.Stat(\"not-existing\")\n\t\t\t\tvar pathErr *fs.PathError\n\t\t\t\tif !errors.As(err, &pathErr) {\n\t\t\t\t\tt.Fatal(\"expected err to be *PathError\")\n\t\t\t\t}\n\n\t\t\t\texpected := fs.PathError{\n\t\t\t\t\tOp:   \"stat\",\n\t\t\t\t\tPath: \"not-existing\",\n\t\t\t\t\tErr:  fs.ErrNotExist,\n\t\t\t\t}\n\t\t\t\tif *pathErr != expected {\n\t\t\t\t\tt.Fatalf(\"want %v; got %v\", expected, *pathErr)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\n\t\tt.Run(\"stat dir\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttest := func(t *testing.T, fi fs.FileInfo) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tif !fi.IsDir() {\n\t\t\t\t\tt.Error(\"expected true\")\n\t\t\t\t}\n\n\t\t\t\tif fi.Mode() != fs.ModeDir {\n\t\t\t\t\tt.Errorf(\"want %d; got %d\", fs.ModeDir, fi.Mode())\n\t\t\t\t}\n\n\t\t\t\tif fi.Sys() != nil {\n\t\t\t\t\tt.Error(\"expected Sys to be nil\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tt.Run(\"top level\", func(t *testing.T) {\n\t\t\t\tfi, err := s3fs.Stat(\".\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"expected err to be nil\")\n\t\t\t\t}\n\t\t\t\ttest(t, fi)\n\n\t\t\t\tif fi.Name() != \".\" {\n\t\t\t\t\tt.Errorf(\"want name=%q; got %q\", \".\", fi.Name())\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"open z\", func(t *testing.T) {\n\t\t\t\tfi, err := s3fs.Stat(\"z\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(\"expected err to be nil\")\n\t\t\t\t}\n\t\t\t\ttest(t, fi)\n\t\t\t})\n\t\t})\n\n\t\tt.Run(\"readdir\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tt.Run(\"success\", func(t *testing.T) {\n\t\t\t\tfixtures := []struct {\n\t\t\t\t\tdesc  string\n\t\t\t\t\tpath  string\n\t\t\t\t\tnames []string\n\t\t\t\t\tmodes []fs.FileMode\n\t\t\t\t\tisDir []bool\n\t\t\t\t\tsize  []int\n\t\t\t\t}{\n\t\t\t\t\t{\n\t\t\t\t\t\tdesc:  \"top level\",\n\t\t\t\t\t\tpath:  \".\",\n\t\t\t\t\t\tnames: []string{\"a\", \"a.txt\", \"dir\", \"dir1\", \"dir2\", testFile, \"x\", \"y.txt\", \"y2.txt\", \"y3.txt\", \"z\"},\n\t\t\t\t\t\tmodes: []fs.FileMode{fs.ModeDir, 0, fs.ModeDir, fs.ModeDir, fs.ModeDir, 0, fs.ModeDir, 0, 0, 0, fs.ModeDir},\n\t\t\t\t\t\tisDir: []bool{true, false, true, true, true, false, true, false, false, false, true},\n\t\t\t\t\t\tsize:  []int{0, len(content), 0, 0, 0, len(content), 0, len(content), len(content), len(content), 0},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdesc:  \"dir1\",\n\t\t\t\t\t\tpath:  \"dir1\",\n\t\t\t\t\t\tnames: []string{\"dir11\", \"file1.txt\", \"file2.txt\"},\n\t\t\t\t\t\tmodes: []fs.FileMode{fs.ModeDir, 0, 0},\n\t\t\t\t\t\tisDir: []bool{true, false, false},\n\t\t\t\t\t\tsize:  []int{0, len(content), len(content)},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdesc:  \"dir11\",\n\t\t\t\t\t\tpath:  \"dir1/dir11\",\n\t\t\t\t\t\tnames: []string{\"file.txt\"},\n\t\t\t\t\t\tmodes: []fs.FileMode{0},\n\t\t\t\t\t\tisDir: []bool{false},\n\t\t\t\t\t\tsize:  []int{len(content)},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tfor _, f := range fixtures {\n\t\t\t\t\tf := f\n\t\t\t\t\ttest := func(t *testing.T, des []fs.DirEntry) {\n\t\t\t\t\t\tvar (\n\t\t\t\t\t\t\tnames []string\n\t\t\t\t\t\t\tmodes []fs.FileMode\n\t\t\t\t\t\t\tisDir []bool\n\t\t\t\t\t\t\tsize  []int\n\t\t\t\t\t\t)\n\t\t\t\t\t\tfor _, de := range des {\n\t\t\t\t\t\t\tfi, err := de.Info()\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\tt.Fatal(\"expected nil; got \", err)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tnames = append(names, de.Name())\n\t\t\t\t\t\t\tmodes = append(modes, fi.Mode())\n\t\t\t\t\t\t\tisDir = append(isDir, fi.IsDir())\n\t\t\t\t\t\t\tsize = append(size, int(fi.Size()))\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor _, v := range []struct {\n\t\t\t\t\t\t\tdesc      string\n\t\t\t\t\t\t\twant, got interface{}\n\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t{\"names\", f.names, names},\n\t\t\t\t\t\t\t{\"modes\", f.modes, modes},\n\t\t\t\t\t\t\t{\"isDir\", f.isDir, isDir},\n\t\t\t\t\t\t\t{\"size\", f.size, size},\n\t\t\t\t\t\t} {\n\t\t\t\t\t\t\tif !reflect.DeepEqual(v.want, v.got) {\n\t\t\t\t\t\t\t\tt.Errorf(\"%s: expected %v; got %v\", v.desc, v.want, v.got)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tt.Run(\"fs.ReadDir \"+f.desc, func(t *testing.T) {\n\t\t\t\t\t\tdes, err := s3fs.ReadDir(f.path)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Fatalf(\"expected err to be nil: %v\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttest(t, des)\n\t\t\t\t\t})\n\n\t\t\t\t\tt.Run(\"file.ReadDir \"+f.desc, func(t *testing.T) {\n\t\t\t\t\t\tf, err := s3fs.Open(f.path)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Fatalf(\"expected err to be nil: %v\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\td, ok := f.(fs.ReadDirFile)\n\t\t\t\t\t\tif !ok {\n\t\t\t\t\t\t\tt.Fatal(\"expected file to be a directory\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdes, err := d.ReadDir(-1)\n\t\t\t\t\t\tif err != nil && !errors.Is(err, io.EOF) {\n\t\t\t\t\t\t\tt.Fatalf(\"expected err to be nil: %v\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttest(t, des)\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"error\", func(t *testing.T) {\n\t\t\t\tfixtures := []struct {\n\t\t\t\t\tdesc string\n\t\t\t\t\tpath string\n\t\t\t\t\terr  fs.PathError\n\t\t\t\t}{\n\t\t\t\t\t{\n\t\t\t\t\t\tdesc: \"invalid path\",\n\t\t\t\t\t\tpath: \"/\",\n\t\t\t\t\t\terr:  fs.PathError{Op: \"readdir\", Path: \"/\", Err: fs.ErrInvalid},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdesc: \"does not exist\",\n\t\t\t\t\t\tpath: \"notexist\",\n\t\t\t\t\t\terr:  fs.PathError{Op: \"readdir\", Path: \"notexist\", Err: fs.ErrNotExist},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdesc: \"does not exist\",\n\t\t\t\t\t\tpath: \"dir1/notexist\",\n\t\t\t\t\t\terr:  fs.PathError{Op: \"readdir\", Path: \"dir1/notexist\", Err: fs.ErrNotExist},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tdesc: \"readDir on a file\",\n\t\t\t\t\t\tpath: \"dir1/file1.txt\",\n\t\t\t\t\t\terr:  fs.PathError{Op: \"readdir\", Path: \"dir1/file1.txt\", Err: errors.New(\"not a dir\")},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tfor _, f := range fixtures {\n\t\t\t\t\tt.Run(f.desc, func(t *testing.T) {\n\t\t\t\t\t\t_, err := s3fs.ReadDir(f.path)\n\n\t\t\t\t\t\tvar perr *fs.PathError\n\t\t\t\t\t\tif !errors.As(err, &perr) {\n\t\t\t\t\t\t\tt.Fatalf(\"expected err to be *fs.PathError; got %[1]T: %[1]v\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif perr.Op != f.err.Op {\n\t\t\t\t\t\t\tt.Errorf(\"want %v; got %v\", f.err.Op, perr.Op)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif perr.Path != f.err.Path {\n\t\t\t\t\t\t\tt.Errorf(\"want %v; got %v\", f.err.Path, perr.Path)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif perr.Err.Error() != f.err.Err.Error() {\n\t\t\t\t\t\t\tt.Errorf(\"want %v; got %v\", f.err.Err.Error(), perr.Err.Error())\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\n\t\tt.Run(\"subfs\", func(t *testing.T) {\n\t\t\tt.Run(\"existing\", func(t *testing.T) {\n\t\t\t\tfsys, err := fs.Sub(s3fs, \"dir1/dir11\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tt.Run(\"fs.Stat\", func(t *testing.T) {\n\t\t\t\t\tfi, err := fs.Stat(fsys, \"file.txt\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tif fi.Name() != \"file.txt\" {\n\t\t\t\t\t\tt.Errorf(\"expected file.txt got %s\", fi.Name())\n\t\t\t\t\t}\n\n\t\t\t\t\tt.Run(\"not exist\", func(t *testing.T) {\n\t\t\t\t\t\t_, err = fs.Stat(fsys, \"not-exist\")\n\t\t\t\t\t\tvar perr *fs.PathError\n\t\t\t\t\t\tif !errors.As(err, &perr) {\n\t\t\t\t\t\t\tt.Fatalf(\"expected err to be PathError: got %#v\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// currently we don't implement fs.SubFS.\n\t\t\t\t\t\t// fs.Sub calls open instead of Stat.\n\t\t\t\t\t\tif perr.Op != \"open\" {\n\t\t\t\t\t\t\tt.Errorf(\"expected op to be open; got %s\", perr.Op)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\n\t\t\t\tt.Run(\"fs.ReadDir\", func(t *testing.T) {\n\t\t\t\t\tfiles, err := fs.ReadDir(fsys, \".\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(files) != 1 {\n\t\t\t\t\t\tt.Fatalf(\"expected 1 file in dir1/dir11; got %d\", len(files))\n\t\t\t\t\t}\n\t\t\t\t\tif files[0].Name() != \"file.txt\" {\n\t\t\t\t\t\tt.Errorf(\"expected file to be file.txt; got %s\", files[0].Name())\n\t\t\t\t\t}\n\n\t\t\t\t\tt.Run(\"not exist\", func(t *testing.T) {\n\t\t\t\t\t\t_, err := fs.ReadDir(fsys, \"not-exist\")\n\t\t\t\t\t\tvar perr *fs.PathError\n\t\t\t\t\t\tif !errors.As(err, &perr) {\n\t\t\t\t\t\t\tt.Fatalf(\"expected err to be PathError: got %#v\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif perr.Op != \"readdir\" {\n\t\t\t\t\t\t\tt.Errorf(\"expected op to be readdir; got %s\", perr.Op)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\n\t\t\t\tt.Run(\"open\", func(t *testing.T) {\n\t\t\t\t\tf, err := fsys.Open(\".\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tdefer f.Close()\n\n\t\t\t\t\tdir, ok := f.(fs.ReadDirFile)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tt.Fatal(\"expected file to be a directory\")\n\t\t\t\t\t}\n\n\t\t\t\t\tfi, err := dir.Stat()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\t\t\t\t\tif fi.Name() != \"dir11\" {\n\t\t\t\t\t\tt.Errorf(\"expected dir name to bedir11; got %s\", fi.Name())\n\t\t\t\t\t}\n\n\t\t\t\t\tfiles, err := dir.ReadDir(-1)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatal(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(files) != 1 {\n\t\t\t\t\t\tt.Fatalf(\"expected 1 file in dir1/dir11; got %d\", len(files))\n\t\t\t\t\t}\n\t\t\t\t\tif files[0].Name() != \"file.txt\" {\n\t\t\t\t\t\tt.Errorf(\"expected file to be file.txt; got %s\", files[0].Name())\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\t\t})\n\t}\n\n\tfixtures := []struct {\n\t\tdesc string\n\t\ts3fs *s3fs.S3FS\n\t}{\n\t\t{desc: \"standard\", s3fs: s3fs.New(wrappedCl, *bucket)},\n\t\t{desc: \"max keys = 1\", s3fs: s3fs.New(&client{MaxKeys: ptr[int32](1), Client: wrappedCl}, *bucket)},\n\t\t{desc: \"max keys = 2\", s3fs: s3fs.New(&client{MaxKeys: ptr[int32](2), Client: wrappedCl}, *bucket)},\n\t\t{desc: \"max keys = 3\", s3fs: s3fs.New(&client{MaxKeys: ptr[int32](3), Client: wrappedCl}, *bucket)},\n\t}\n\n\tfor _, f := range fixtures {\n\t\tf := f\n\t\tt.Run(f.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttestFn(t, f.s3fs)\n\t\t})\n\t}\n}\n\nfunc TestDirRead(t *testing.T) {\n\ttype fileinfo struct {\n\t\tname  string\n\t\tisDir bool\n\t}\n\n\ttests := []struct {\n\t\tdesc     string\n\t\tn        int\n\t\touts     []s3.ListObjectsOutput\n\t\texpected [][]fileinfo\n\t}{\n\t\t{\n\t\t\tdesc: \"all in one request - dir first\",\n\t\t\tn:    1,\n\t\t\touts: []s3.ListObjectsOutput{\n\t\t\t\tnewListOutput([]string{\"a\", \"c\", \"e\"}, []string{\"b\", \"d\", \"f\"}),\n\t\t\t},\n\t\t\texpected: [][]fileinfo{\n\t\t\t\t{{\"a\", true}},\n\t\t\t\t{{\"b\", false}},\n\t\t\t\t{{\"c\", true}},\n\t\t\t\t{{\"d\", false}},\n\t\t\t\t{{\"e\", true}},\n\t\t\t\t{{\"f\", false}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"all in one request - n = 0\",\n\t\t\tn:    0,\n\t\t\touts: []s3.ListObjectsOutput{\n\t\t\t\tnewListOutput([]string{\"a\", \"c\", \"e\"}, []string{\"b\", \"d\", \"f\"}),\n\t\t\t},\n\t\t\texpected: [][]fileinfo{\n\t\t\t\t{\n\t\t\t\t\t{\"a\", true},\n\t\t\t\t\t{\"b\", false},\n\t\t\t\t\t{\"c\", true},\n\t\t\t\t\t{\"d\", false},\n\t\t\t\t\t{\"e\", true},\n\t\t\t\t\t{\"f\", false},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"all in one request - n = 2\",\n\t\t\tn:    2,\n\t\t\touts: []s3.ListObjectsOutput{\n\t\t\t\tnewListOutput([]string{\"a\"}, nil),\n\t\t\t\tnewListOutput([]string{\"c\"}, []string{\"b\", \"d\"}),\n\t\t\t\tnewListOutput([]string{\"e\"}, nil),\n\t\t\t\tnewListOutput(nil, []string{\"f\"}),\n\t\t\t},\n\t\t\texpected: [][]fileinfo{\n\t\t\t\t{\n\t\t\t\t\t{\"a\", true},\n\t\t\t\t\t{\"b\", false},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t{\"c\", true},\n\t\t\t\t\t{\"d\", false},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t{\"e\", true},\n\t\t\t\t\t{\"f\", false},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"one per request - dir first\",\n\t\t\tn:    1,\n\t\t\touts: []s3.ListObjectsOutput{\n\t\t\t\tnewListOutput([]string{\"a\"}, nil),\n\t\t\t\tnewListOutput(nil, []string{\"b\"}),\n\t\t\t\tnewListOutput([]string{\"c\"}, []string{\"d\"}),\n\t\t\t\tnewListOutput([]string{\"e\"}, nil),\n\t\t\t\tnewListOutput(nil, []string{\"f\"}),\n\t\t\t},\n\t\t\texpected: [][]fileinfo{\n\t\t\t\t{{\"a\", true}},\n\t\t\t\t{{\"b\", false}},\n\t\t\t\t{{\"c\", true}},\n\t\t\t\t{{\"d\", false}},\n\t\t\t\t{{\"e\", true}},\n\t\t\t\t{{\"f\", false}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"all in one request - file first\",\n\t\t\tn:    1,\n\t\t\touts: []s3.ListObjectsOutput{\n\t\t\t\tnewListOutput([]string{\"b\", \"d\", \"f\"}, []string{\"a\", \"c\", \"e\"}),\n\t\t\t},\n\t\t\texpected: [][]fileinfo{\n\t\t\t\t{{\"a\", false}},\n\t\t\t\t{{\"b\", true}},\n\t\t\t\t{{\"c\", false}},\n\t\t\t\t{{\"d\", true}},\n\t\t\t\t{{\"e\", false}},\n\t\t\t\t{{\"f\", true}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"with dir duplicates\",\n\t\t\tn:    1,\n\t\t\touts: []s3.ListObjectsOutput{\n\t\t\t\tnewListOutput([]string{\"a\", \"c\"}, []string{\"b\"}),\n\t\t\t\tnewListOutput([]string{\"c\", \"e\", \"c\"}, []string{\"d\"}),\n\t\t\t\tnewListOutput([]string{\"e\", \"a\"}, []string{\"f\"}),\n\t\t\t},\n\t\t\texpected: [][]fileinfo{\n\t\t\t\t{{\"a\", true}},\n\t\t\t\t{{\"b\", false}},\n\t\t\t\t{{\"c\", true}},\n\t\t\t\t{{\"d\", false}},\n\t\t\t\t{{\"e\", true}},\n\t\t\t\t{{\"f\", false}},\n\t\t\t},\n\t\t},\n\n\t\t{\n\t\t\tdesc: \"all in one request - dirs only\",\n\t\t\tn:    1,\n\t\t\touts: []s3.ListObjectsOutput{\n\t\t\t\tnewListOutput([]string{\"a\", \"c\", \"e\"}, nil),\n\t\t\t},\n\t\t\texpected: [][]fileinfo{\n\t\t\t\t{{\"a\", true}},\n\t\t\t\t{{\"c\", true}},\n\t\t\t\t{{\"e\", true}},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tdesc: \"single dir per request - dirs only\",\n\t\t\tn:    1,\n\t\t\touts: []s3.ListObjectsOutput{\n\t\t\t\tnewListOutput([]string{\"a\"}, nil),\n\t\t\t\tnewListOutput([]string{\"c\"}, nil),\n\t\t\t\tnewListOutput([]string{\"e\"}, nil),\n\t\t\t},\n\t\t\texpected: [][]fileinfo{\n\t\t\t\t{{\"a\", true}},\n\t\t\t\t{{\"c\", true}},\n\t\t\t\t{{\"e\", true}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.desc, func(t *testing.T) {\n\t\t\tf, err := s3fs.New(&mockClient{\n\t\t\t\touts: test.outs,\n\t\t\t}, \"test\").Open(\".\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"expected err to be nil; got \", err)\n\t\t\t}\n\n\t\t\tfi, err := f.Stat()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"expected err to be nil; got \", err)\n\t\t\t}\n\n\t\t\tif !fi.IsDir() {\n\t\t\t\tt.Fatal(\"expected the file to be a directory\")\n\t\t\t}\n\n\t\t\tvar fis [][]fileinfo\n\t\t\tfor {\n\t\t\t\tfiles, err := f.(fs.ReadDirFile).ReadDir(test.n)\n\t\t\t\tif err != nil && !errors.Is(err, io.EOF) {\n\t\t\t\t\tt.Fatal(\"did not expect err:\", err)\n\t\t\t\t}\n\n\t\t\t\tif len(files) > 0 {\n\t\t\t\t\tvar out []fileinfo\n\t\t\t\t\tfor _, f := range files {\n\t\t\t\t\t\tout = append(out, fileinfo{f.Name(), f.IsDir()})\n\t\t\t\t\t}\n\t\t\t\t\tfis = append(fis, out)\n\t\t\t\t}\n\n\t\t\t\tif test.n <= 0 || errors.Is(err, io.EOF) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(fis, test.expected) {\n\t\t\t\tt.Errorf(\"want %v; got %v\", test.expected, fis)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype mockClient struct {\n\ts3fs.Client\n\touts []s3.ListObjectsOutput\n\ti    int\n}\n\nfunc (c *mockClient) ListObjects(ctx context.Context, in *s3.ListObjectsInput, _ ...func(*s3.Options)) (*s3.ListObjectsOutput, error) {\n\tdefer func() { c.i++ }()\n\tif c.i < len(c.outs) {\n\t\treturn &c.outs[c.i], nil\n\t}\n\n\treturn &s3.ListObjectsOutput{\n\t\tIsTruncated: ptr(false),\n\t}, nil\n}\n\nfunc newListOutput(dirs, files []string) (out s3.ListObjectsOutput) {\n\tfor _, d := range dirs {\n\t\tout.CommonPrefixes = append(out.CommonPrefixes, types.CommonPrefix{\n\t\t\tPrefix: ptr(d),\n\t\t})\n\t}\n\n\tfor _, f := range files {\n\t\tout.Contents = append(out.Contents, types.Object{\n\t\t\tKey:          ptr(f),\n\t\t\tSize:         ptr[int64](0),\n\t\t\tLastModified: ptr(time.Time{}),\n\t\t})\n\t}\n\treturn out\n}\n\ntype Client interface {\n\ts3fs.Client\n}\n\nfunc newClient(t *testing.T) (*s3.Client, Client) {\n\tt.Helper()\n\n\tcl := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tInsecureSkipVerify: *skipVerify,\n\t\t\t},\n\t\t},\n\t}\n\n\tclient := s3.New(s3.Options{\n\t\tBaseEndpoint: endpoint,\n\t\tCredentials: aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) {\n\t\t\treturn aws.Credentials{\n\t\t\t\tAccessKeyID:     accessKeyID,\n\t\t\t\tSecretAccessKey: secretKey,\n\t\t\t}, nil\n\t\t}),\n\t\tRegion:       region,\n\t\tUsePathStyle: true,\n\t\tHTTPClient:   cl,\n\t})\n\n\treturn client, &modTimeTruncateClient{&metricClient{client}}\n}\n\nfunc writeFile(t *testing.T, cl *s3.Client, bucket, name string, data []byte) {\n\tt.Helper()\n\n\tuploader := manager.NewUploader(cl)\n\t_, err := uploader.Upload(context.Background(), &s3.PutObjectInput{\n\t\tBody:   strings.NewReader(string(data)),\n\t\tBucket: &bucket,\n\t\tKey:    &name,\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc deleteFile(t *testing.T, cl *s3.Client, bucket, name string) {\n\tt.Helper()\n\n\t_, err := cl.DeleteObject(\n\t\tcontext.Background(),\n\t\t&s3.DeleteObjectInput{\n\t\t\tBucket: ptr(bucket),\n\t\t\tKey:    &name,\n\t\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc createBucket(t *testing.T, cl *s3.Client, bucket string) {\n\tt.Helper()\n\n\t_, err := cl.CreateBucket(context.Background(), &s3.CreateBucketInput{\n\t\tBucket: &bucket,\n\t})\n\tif err != nil {\n\t\tvar e *types.BucketAlreadyOwnedByYou\n\t\tif errors.As(err, &e) {\n\t\t\treturn\n\t\t}\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc cleanBucket(t *testing.T, cl *s3.Client, bucket string) {\n\tt.Helper()\n\n\tout, err := cl.ListObjects(\n\t\tcontext.Background(),\n\t\t&s3.ListObjectsInput{\n\t\t\tBucket: ptr(bucket),\n\t\t})\n\tif err != nil {\n\t\tt.Fatal(\"failed to delete bucket:\", err)\n\t}\n\n\tfor _, o := range out.Contents {\n\t\t_, err := cl.DeleteObject(\n\t\t\tcontext.Background(),\n\t\t\t&s3.DeleteObjectInput{\n\t\t\t\tBucket: ptr(bucket),\n\t\t\t\tKey:    o.Key,\n\t\t\t})\n\t\tif err != nil {\n\t\t\tt.Error(\"failed to delete file:\", err)\n\t\t}\n\t}\n}\n\nfunc envDefault(env, def string) string {\n\tif os.Getenv(env) == \"\" {\n\t\treturn def\n\t}\n\treturn os.Getenv(env)\n}\n\ntype client struct {\n\tMaxKeys *int32\n\ts3fs.Client\n}\n\nfunc (c *client) ListObjects(ctx context.Context, in *s3.ListObjectsInput, _ ...func(*s3.Options)) (*s3.ListObjectsOutput, error) {\n\tif c.MaxKeys != nil {\n\t\tin.MaxKeys = c.MaxKeys\n\t}\n\treturn c.Client.ListObjects(ctx, in)\n}\n\ntype modTimeTruncateClient struct {\n\tClient\n}\n\n// Minio returns modTime that includes microseconds if data comes from ListObjects\n// while data coming from GetObject's modTimes are accurate down to seconds.\n// To make this test pass while using Minio we build this client that truncates\n// modTimes to Second.\nfunc (c *modTimeTruncateClient) ListObjects(ctx context.Context, in *s3.ListObjectsInput, _ ...func(*s3.Options)) (*s3.ListObjectsOutput, error) {\n\tout, err := c.Client.ListObjects(context.Background(), in)\n\tif err != nil {\n\t\treturn out, err\n\t}\n\n\tfor i, o := range out.Contents {\n\t\tout.Contents[i].LastModified = ptr(o.LastModified.Truncate(time.Second))\n\t}\n\treturn out, err\n}\n\nvar (\n\t// global metrics for this test.\n\tlistC int64\n\tgetC  int64\n)\n\ntype metricClient struct {\n\tClient\n}\n\nfunc (c *metricClient) ListObjects(ctx context.Context, in *s3.ListObjectsInput, _ ...func(*s3.Options)) (*s3.ListObjectsOutput, error) {\n\tatomic.AddInt64(&listC, 1)\n\treturn c.Client.ListObjects(ctx, in)\n}\n\nfunc (c *metricClient) GetObject(ctx context.Context, in *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {\n\tatomic.AddInt64(&getC, 1)\n\treturn c.Client.GetObject(context.Background(), in)\n}\n\nfunc ptr[T any](v T) *T {\n\treturn &v\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/jszwec/s3fs/v2\n\ngo 1.21\n\nrequire (\n\tgithub.com/aws/aws-sdk-go-v2 v1.24.0\n\tgithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.47.5\n)\n\nrequire (\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect\n\tgithub.com/aws/smithy-go v1.19.0 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk=\ngithub.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=\ngithub.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o=\ngithub.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7 h1:FnLf60PtjXp8ZOzQfhJVsqF0OtYKQZWQfqOLshh8YXg=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7/go.mod h1:tDVvl8hyU6E9B8TrnNrZQEVkQlB8hjJwcgpPhgtlnNg=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU=\ngithub.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=\ngithub.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=\ngithub.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\n"
  },
  {
    "path": "test/localstack/docker-compose.yml",
    "content": "version: '3.8'\n\nservices:\n  localstack:\n    container_name: \"localstack\"\n    image: localstack/localstack:0.14.0\n    network_mode: bridge\n    ports:\n      - \"4566:4566\"\n      - \"4571:4571\"\n    environment:\n      - SERVICES=s3"
  },
  {
    "path": "test/minio/docker-compose.yml",
    "content": "version: '3.8'\n\nservices:\n  minio:\n    image: minio/minio\n    ports:\n      - \"9000:9000\"\n    environment:\n      MINIO_ROOT_USER: minioadmin\n      MINIO_ROOT_PASSWORD: minioadmin\n    command: server /data\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:9000/minio/health/live\"]\n      interval: 30s\n      timeout: 20s\n      retries: 3\n"
  }
]