Repository: peak/s5cmd (go)

Issue:
cp: acl: seems acl set broken in 2.2.1
Hi, the acl seems was omitted, when I was try `cp` with 2.2.1 version

```python
√ /tmp % s5cmd --stat cp --acl "public-read" "*rpm" s3://k0ste/rpm-legacy/
cp netdata-plugin-perf-1.42.2-1.el8.x86_64.rpm s3://k0ste/rpm-legacy/netdata-plugin-perf-1.42.2-1.el8.x86_64.rpm
cp netdata-plugin-pythond-1.42.2-1.el8.x86_64.rpm s3://k0ste/rpm-legacy/netdata-plugin-pythond-1.42.2-1.el8.x86_64.rpm
cp netdata-plugin-apps-1.42.2-1.el8.x86_64.rpm s3://k0ste/rpm-legacy/netdata-plugin-apps-1.42.2-1.el8.x86_64.rpm
cp netdata-plugin-debugfs-1.42.2-1.el8.x86_64.rpm s3://k0ste/rpm-legacy/netdata-plugin-debugfs-1.42.2-1.el8.x86_64.rpm
cp netdata-plugin-ebpf-1.42.2-1.el8.x86_64.rpm s3://k0ste/rpm-legacy/netdata-plugin-ebpf-1.42.2-1.el8.x86_64.rpm
cp netdata-ebpf-legacy-code-1.42.2-1.el8.x86_64.rpm s3://k0ste/rpm-legacy/netdata-ebpf-legacy-code-1.42.2-1.el8.x86_64.rpm
cp netdata-1.42.2-1.el8.x86_64.rpm s3://k0ste/rpm-legacy/netdata-1.42.2-1.el8.x86_64.rpm

Operation	Total	Error	Success
cp		1	0	1
√ 111 % s3cmd info s3://k0ste/rpm-legacy/netdata-1.42.2-1.el8.x86_64.rpm
s3://k0ste/rpm-legacy/netdata-1.42.2-1.el8.x86_64.rpm (object):
   File size: 13778140
   Last mod:  Wed, 30 Aug 2023 13:40:07 GMT
   MIME type: application/octet-stream
   Storage:   STANDARD
   MD5 sum:   10885f5846e179ae10e3b764662e9f05
   SSE:       none
   Policy:    none
   CORS:      none
   ACL:       <account>: FULL_CONTROL <------ no ACL for anon-read
```

Checked with s3cmd:

```python
√ /tmp % s3cmd --recursive --acl-public put 111/ s3://k0ste/rpm-legacy/
upload: '111/netdata-1.42.2-1.el8.x86_64.rpm' -> 's3://k0ste/rpm-legacy/netdata-1.42.2-1.el8.x86_64.rpm'  [1 of 7]
 13778140 of 13778140   100% in    1s    10.12 MB/s  done
upload: '111/netdata-ebpf-legacy-code-1.42.2-1.el8.x86_64.rpm' -> 's3://k0ste/rpm-legacy/netdata-ebpf-legacy-code-1.42.2-1.el8.x86_64.rpm'  [2 of 7]
 3694284 of 3694284   100% in    0s    15.77 MB/s  done
upload: '111/netdata-plugin-apps-1.42.2-1.el8.x86_64.rpm' -> 's3://k0ste/rpm-legacy/netdata-plugin-apps-1.42.2-1.el8.x86_64.rpm'  [3 of 7]
 385860 of 385860   100% in    0s     4.23 MB/s  done
upload: '111/netdata-plugin-debugfs-1.42.2-1.el8.x86_64.rpm' -> 's3://k0ste/rpm-legacy/netdata-plugin-debugfs-1.42.2-1.el8.x86_64.rpm'  [4 of 7]
 348728 of 348728   100% in    0s     3.05 MB/s  done
upload: '111/netdata-plugin-ebpf-1.42.2-1.el8.x86_64.rpm' -> 's3://k0ste/rpm-legacy/netdata-plugin-ebpf-1.42.2-1.el8.x86_64.rpm'  [5 of 7]
 599716 of 599716   100% in    0s     5.07 MB/s  done
upload: '111/netdata-plugin-perf-1.42.2-1.el8.x86_64.rpm' -> 's3://k0ste/rpm-legacy/netdata-plugin-perf-1.42.2-1.el8.x86_64.rpm'  [6 of 7]
 347276 of 347276   100% in    0s  1944.90 KB/s  done
upload: '111/netdata-plugin-pythond-1.42.2-1.el8.x86_64.rpm' -> 's3://k0ste/rpm-legacy/netdata-plugin-pythond-1.42.2-1.el8.x86_64.rpm'  [7 of 7]
 270216 of 270216   100% in    0s     2.80 MB/s  done
√ /tmp % s3cmd info s3://k0ste/rpm-legacy/netdata-1.42.2-1.el8.x86_64.rpm
s3://k0ste/rpm-legacy/netdata-1.42.2-1.el8.x86_64.rpm (object):
   File size: 13778140
   Last mod:  Wed, 30 Aug 2023 13:46:13 GMT
   MIME type: application/x-rpm
   Storage:   STANDARD
   MD5 sum:   10885f5846e179ae10e3b764662e9f05
   SSE:       none
   Policy:    none
   CORS:      none
   ACL:       *anon*: READ  <----- ACL for anon-read present
   ACL:       <account>: FULL_CONTROL
```

Pull request:
cp,pipe: don't omit some of the metadata flags
#657 mentioned that `acl` flag was broken in `v2.2.1`. After looking at the issue, it was discovered that not only the `acl` flag was broken, there are couple of more flags that were being omitted during the mentioned commands, which all of them caused by this faulty [PR](https://github.com/peak/s5cmd/pull/621). 

Resolves #657.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6ec4b5..56043da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,12 @@
 # Changelog
+## v2.2.2 - 13 Sep 2023 
 
+#### Bugfixes
+- Fixed `cp` and `pipe` to not omit some of the metadata flags. ([#657](https://github.com/peak/s5cmd/issues/657))
 ## v2.2.1 - 23 Aug 2023
 
 #### Bugfixes
 - Fixed incorrect `s5cmd version` output ([#650](https://github.com/peak/s5cmd/pull/650))
-
 ## v2.2.0 - 21 Aug 2023
 
 #### Features
diff --git a/command/cp.go b/command/cp.go
index 3e608da..775e311 100644
--- a/command/cp.go
+++ b/command/cp.go
@@ -689,10 +689,17 @@ func (c Copy) doUpload(ctx context.Context, srcurl *url.URL, dsturl *url.URL, ex
 	if err != nil {
 		return err
 	}
-	metadata := storage.Metadata{UserDefined: extradata}
 
-	if c.storageClass != "" {
-		metadata.StorageClass = string(c.storageClass)
+	metadata := storage.Metadata{
+		UserDefined:        extradata,
+		ACL:                c.acl,
+		CacheControl:       c.cacheControl,
+		Expires:            c.expires,
+		StorageClass:       string(c.storageClass),
+		ContentEncoding:    c.contentEncoding,
+		ContentDisposition: c.contentDisposition,
+		EncryptionMethod:   c.encryptionMethod,
+		EncryptionKeyID:    c.encryptionKeyID,
 	}
 
 	if c.contentType != "" {
@@ -701,14 +708,6 @@ func (c Copy) doUpload(ctx context.Context, srcurl *url.URL, dsturl *url.URL, ex
 		metadata.ContentType = guessContentType(file)
 	}
 
-	if c.contentEncoding != "" {
-		metadata.ContentEncoding = c.contentEncoding
-	}
-
-	if c.contentDisposition != "" {
-		metadata.ContentDisposition = c.contentDisposition
-	}
-
 	reader := newCountingReaderWriter(file, c.progressbar)
 	err = dstClient.Put(ctx, reader, dsturl, metadata, c.concurrency, c.partSize)
 
@@ -755,20 +754,17 @@ func (c Copy) doCopy(ctx context.Context, srcurl, dsturl *url.URL, extradata map
 		return err
 	}
 
-	metadata := storage.Metadata{UserDefined: extradata}
-	if c.storageClass != "" {
-		metadata.StorageClass = string(c.storageClass)
-	}
-
-	if c.contentType != "" {
-		metadata.ContentType = c.contentType
-	}
-
-	if c.contentEncoding != "" {
-		metadata.ContentEncoding = c.contentEncoding
-	}
-	if c.contentDisposition != "" {
-		metadata.ContentDisposition = c.contentDisposition
+	metadata := storage.Metadata{
+		UserDefined:        extradata,
+		ACL:                c.acl,
+		CacheControl:       c.cacheControl,
+		Expires:            c.expires,
+		StorageClass:       string(c.storageClass),
+		ContentType:        c.contentType,
+		ContentEncoding:    c.contentEncoding,
+		ContentDisposition: c.contentDisposition,
+		EncryptionMethod:   c.encryptionMethod,
+		EncryptionKeyID:    c.encryptionKeyID,
 	}
 
 	err = c.shouldOverride(ctx, srcurl, dsturl)
diff --git a/command/pipe.go b/command/pipe.go
index dfbaeb0..77793f7 100644
--- a/command/pipe.go
+++ b/command/pipe.go
@@ -221,9 +221,16 @@ func (c Pipe) Run(ctx context.Context) error {
 		return err
 	}
 
-	metadata := storage.Metadata{UserDefined: c.metadata}
-	if c.storageClass != "" {
-		metadata.StorageClass = string(c.storageClass)
+	metadata := storage.Metadata{
+		UserDefined:        c.metadata,
+		ACL:                c.acl,
+		CacheControl:       c.cacheControl,
+		Expires:            c.expires,
+		StorageClass:       string(c.storageClass),
+		ContentEncoding:    c.contentEncoding,
+		ContentDisposition: c.contentDisposition,
+		EncryptionMethod:   c.encryptionMethod,
+		EncryptionKeyID:    c.encryptionKeyID,
 	}
 
 	if c.contentType != "" {
@@ -232,13 +239,6 @@ func (c Pipe) Run(ctx context.Context) error {
 		metadata.ContentType = guessContentTypeByExtension(c.dst)
 	}
 
-	if c.contentEncoding != "" {
-		metadata.ContentEncoding = c.contentEncoding
-	}
-	if c.contentDisposition != "" {
-		metadata.ContentDisposition = c.contentDisposition
-	}
-
 	err = client.Put(ctx, &stdin{file: os.Stdin}, c.dst, metadata, c.concurrency, c.partSize)
 	if err != nil {
 		return err
diff --git a/go.mod b/go.mod
index 791f53a..9162601 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/google/go-cmp v0.5.9
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
-	github.com/igungor/gofakes3 v0.0.14
+	github.com/igungor/gofakes3 v0.0.15
 	github.com/karrick/godirwalk v1.15.3
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
 	github.com/lanrat/extsort v1.0.0
diff --git a/go.sum b/go.sum
index 51a72a8..8569a39 100644
--- a/go.sum
+++ b/go.sum
@@ -23,8 +23,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
 github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
-github.com/igungor/gofakes3 v0.0.14 h1:jpKujPDFe6iF3rJ34AyafvK7+LatbSbjRTnmBVPnsuY=
-github.com/igungor/gofakes3 v0.0.14/go.mod h1:+rwAKRO9RTGCIeE8SRvRPLSj7PVhaMBLlm1zPXzu7Cs=
+github.com/igungor/gofakes3 v0.0.15 h1:/57KiuC2Nc0Heh1cjnTOe6mWrDNxIr8kfF7xgah55OA=
+github.com/igungor/gofakes3 v0.0.15/go.mod h1:+rwAKRO9RTGCIeE8SRvRPLSj7PVhaMBLlm1zPXzu7Cs=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
diff --git a/storage/s3.go b/storage/s3.go
index 66729e1..f6ebcff 100644
--- a/storage/s3.go
+++ b/storage/s3.go
@@ -819,6 +819,7 @@ func (s *S3) Put(
 	if storageClass != "" {
 		input.StorageClass = aws.String(storageClass)
 	}
+
 	acl := metadata.ACL
 	if acl != "" {
 		input.ACL = aws.String(acl)
diff --git a/storage/storage.go b/storage/storage.go
index 8de7d7c..9823416 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -225,10 +225,10 @@ type Metadata struct {
 	Expires            string
 	StorageClass       string
 	ContentType        string
+	ContentEncoding    string
 	ContentDisposition string
 	EncryptionMethod   string
 	EncryptionKeyID    string
-	ContentEncoding    string
 
 	UserDefined map[string]string
 }
diff --git a/vendor/github.com/igungor/gofakes3/gofakes3.go b/vendor/github.com/igungor/gofakes3/gofakes3.go
index 9db2ed7..dfa7565 100644
--- a/vendor/github.com/igungor/gofakes3/gofakes3.go
+++ b/vendor/github.com/igungor/gofakes3/gofakes3.go
@@ -1039,10 +1039,13 @@ func metadataHeaders(headers map[string][]string, at time.Time, sizeLimit int) (
 		if strings.HasPrefix(hk, "X-Amz-") ||
 			hk == "Content-Type" ||
 			hk == "Content-Disposition" ||
-			hk == "Content-Encoding" {
+			hk == "Content-Encoding" ||
+			hk == "Expires" ||
+			hk == "Cache-Control" {
 			meta[hk] = hv[0]
 		}
 	}
+
 	meta["Last-Modified"] = formatHeaderTime(at)
 
 	if sizeLimit > 0 && metadataSize(meta) > sizeLimit {
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 0fffb84..bb15129 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -88,7 +88,7 @@ github.com/hashicorp/go-multierror
 # github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
 ## explicit
 github.com/iancoleman/strcase
-# github.com/igungor/gofakes3 v0.0.14
+# github.com/igungor/gofakes3 v0.0.15
 ## explicit; go 1.13
 github.com/igungor/gofakes3
 github.com/igungor/gofakes3/backend/s3bolt


Test patch:
diff --git a/e2e/cp_test.go b/e2e/cp_test.go
index ed5a6f4..48867bd 100644
--- a/e2e/cp_test.go
+++ b/e2e/cp_test.go
@@ -25,6 +25,7 @@ package e2e
 import (
 	"fmt"
 	"net"
+	"net/http"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -720,6 +721,75 @@ func TestCopySingleFileToS3(t *testing.T) {
 	assert.Assert(t, ensureS3Object(s3client, bucket, filename, content, ensureContentType(expectedContentType), ensureContentDisposition(expectedContentDisposition)))
 }
 
+func TestCopySingleFileToS3WithAllMetadataFlags(t *testing.T) {
+	t.Parallel()
+
+	s3client, s5cmd := setup(t)
+
+	bucket := s3BucketFromTestName(t)
+
+	createBucket(t, s3client, bucket)
+
+	const (
+		filename           = "index"
+		content            = `testfilecontent`
+		cacheControl       = "public, max-age=3600"
+		expires            = "2025-01-01T00:00:00Z"
+		storageClass       = "STANDARD_IA"
+		ContentType        = "text/html; charset=utf-8"
+		ContentDisposition = "inline"
+		ContentEncoding    = "utf-8"
+		EncryptionMethod   = "aws:kms"
+		EncryptionKeyID    = "1234abcd-12ab-34cd-56ef-1234567890ab"
+	)
+
+	// expected expires flag is the parsed version of the date in RFC3339 format
+	parsedTime, err := time.Parse(time.RFC3339, expires)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expectedExpires := parsedTime.Format(http.TimeFormat)
+
+	workdir := fs.NewDir(t, bucket, fs.WithFile(filename, content))
+	defer workdir.Remove()
+
+	srcpath := workdir.Join(filename)
+	dstpath := fmt.Sprintf("s3://%v/", bucket)
+
+	srcpath = filepath.ToSlash(srcpath)
+	cmd := s5cmd("cp",
+		"--cache-control", cacheControl,
+		"--expires", expires,
+		"--storage-class", storageClass,
+		"--content-type", ContentType,
+		"--content-disposition", ContentDisposition,
+		"--content-encoding", ContentEncoding,
+		"--sse", EncryptionMethod,
+		"--sse-kms-key-id", EncryptionKeyID,
+		srcpath, dstpath,
+	)
+
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	expected := fs.Expected(t, fs.WithFile(filename, content))
+	assert.Assert(t, fs.Equal(workdir.Path(), expected))
+
+	assert.Assert(t, ensureS3Object(s3client, bucket, filename, content,
+		ensureExpires(expectedExpires),
+		ensureCacheControl(cacheControl),
+		ensureStorageClass(storageClass),
+		ensureContentType(ContentType),
+		ensureContentDisposition(ContentDisposition),
+		ensureContentEncoding(ContentEncoding),
+		ensureEncryptionMethod(EncryptionMethod),
+		ensureEncryptionKeyID(EncryptionKeyID),
+	))
+
+}
+
 // cp dir/file s3://bucket/ --metadata key1=val1 --metadata key2=val2 ...
 func TestCopySingleFileToS3WithArbitraryMetadata(t *testing.T) {
 	t.Parallel()
@@ -800,10 +870,11 @@ func TestCopyS3ToS3WithArbitraryMetadata(t *testing.T) {
 		"Key1": aws.String("foo"),
 		"Key2": aws.String("bar"),
 	}
+
 	srcpath := fmt.Sprintf("s3://%v/%v", bucket, filename)
 	dstpath := fmt.Sprintf("s3://%v/%v_cp", bucket, filename)
 
-	putFileWithMetadata(t, s3client, bucket, filename, content, srcmetadata)
+	putFile(t, s3client, bucket, filename, content, putArbitraryMetadata(srcmetadata))
 	cmd := s5cmd("cp", "--metadata", foo, "--metadata", bar, srcpath, dstpath)
 	result := icmd.RunCmd(cmd)
 	result.Assert(t, icmd.Success)
diff --git a/e2e/pipe_test.go b/e2e/pipe_test.go
index 6252763..25db595 100644
--- a/e2e/pipe_test.go
+++ b/e2e/pipe_test.go
@@ -3,11 +3,14 @@ package e2e
 import (
 	"bytes"
 	"fmt"
+	"net/http"
 	"runtime"
 	"testing"
+	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"gotest.tools/v3/assert"
+	"gotest.tools/v3/fs"
 	"gotest.tools/v3/icmd"
 )
 
@@ -461,44 +464,68 @@ func TestUploadStdinToS3WithStorageClassGlacier(t *testing.T) {
 }
 
 // pipe --content-disposition inline s3://bucket/object
-func TestUploadStdinToToS3WithContentDisposition(t *testing.T) {
+func TestUploadStdinToToS3WithAllMetadataFlags(t *testing.T) {
 	t.Parallel()
 
 	s3client, s5cmd := setup(t)
 
 	bucket := s3BucketFromTestName(t)
+
 	createBucket(t, s3client, bucket)
 
 	const (
-		// make sure that Put reads the file header and guess Content-Type correctly.
-		filename = "index.html"
-		content  = `
-<html lang="tr">
-	<head>
-	<meta charset="utf-8">
-	<body>
-		<header></header>
-		<main></main>
-		<footer></footer>
-	</body>
-</html>
-`
-		expectedContentType        = "text/html; charset=utf-8"
-		expectedContentDisposition = "inline"
+		filename           = "index"
+		content            = `testfilecontent`
+		cacheControl       = "public, max-age=3600"
+		expires            = "2025-01-01T00:00:00Z"
+		storageClass       = "STANDARD_IA"
+		ContentType        = "text/html; charset=utf-8"
+		ContentDisposition = "inline"
+		ContentEncoding    = "utf-8"
+		EncryptionMethod   = "aws:kms"
+		EncryptionKeyID    = "1234abcd-12ab-34cd-56ef-1234567890ab"
 	)
 
+	// expected expires flag is the parsed version of the date in RFC3339 format
+	parsedTime, err := time.Parse(time.RFC3339, expires)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expectedExpires := parsedTime.Format(http.TimeFormat)
+
+	workdir := fs.NewDir(t, bucket, fs.WithFile(filename, content))
+	defer workdir.Remove()
+
 	dstpath := fmt.Sprintf("s3://%v/%v", bucket, filename)
+
 	reader := bytes.NewBufferString(content)
 
-	cmd := s5cmd("pipe", "--content-disposition", "inline", dstpath)
+	cmd := s5cmd("pipe",
+		"--cache-control", cacheControl,
+		"--expires", expires,
+		"--storage-class", storageClass,
+		"--content-type", ContentType,
+		"--content-disposition", ContentDisposition,
+		"--content-encoding", ContentEncoding,
+		"--sse", EncryptionMethod,
+		"--sse-kms-key-id", EncryptionKeyID,
+		dstpath,
+	)
+
 	result := icmd.RunCmd(cmd, icmd.WithStdin(reader))
 
 	result.Assert(t, icmd.Success)
 
-	assertLines(t, result.Stdout(), map[int]compareFunc{
-		0: suffix(`pipe %v`, dstpath),
-	})
-
 	// assert S3
-	assert.Assert(t, ensureS3Object(s3client, bucket, filename, content, ensureContentType(expectedContentType), ensureContentDisposition(expectedContentDisposition)))
+	assert.Assert(t, ensureS3Object(s3client, bucket, filename, content,
+		ensureExpires(expectedExpires),
+		ensureCacheControl(cacheControl),
+		ensureStorageClass(storageClass),
+		ensureContentType(ContentType),
+		ensureContentDisposition(ContentDisposition),
+		ensureContentEncoding(ContentEncoding),
+		ensureEncryptionMethod(EncryptionMethod),
+		ensureEncryptionKeyID(EncryptionKeyID),
+	))
 }
diff --git a/e2e/util_test.go b/e2e/util_test.go
index f88fe42..d41bea9 100644
--- a/e2e/util_test.go
+++ b/e2e/util_test.go
@@ -433,6 +433,7 @@ func createBucket(t *testing.T, client *s3.S3, bucket string) {
 
 	input := &s3.CreateBucketInput{
 		Bucket: aws.String(bucket),
+		ACL:    aws.String(s3.BucketCannedACLPublicRead),
 	}
 
 	_, err := client.CreateBucket(input)
@@ -571,14 +572,37 @@ func setBucketVersioning(t *testing.T, s3client *s3.S3, bucket string, versionin
 var errS3NoSuchKey = fmt.Errorf("s3: no such key")
 
 type ensureOpts struct {
+	cacheControl       *string
+	expires            *string
+	storageClass       *string
 	contentType        *string
 	contentDisposition *string
+	contentEncoding    *string
+	encryptionMethod   *string
+	encryptionKeyID    *string
 	metadata           map[string]*string
-	storageClass       *string
 }
 
 type ensureOption func(*ensureOpts)
 
+func ensureCacheControl(cacheControl string) ensureOption {
+	return func(opts *ensureOpts) {
+		opts.cacheControl = &cacheControl
+	}
+}
+
+func ensureExpires(expires string) ensureOption {
+	return func(opts *ensureOpts) {
+		opts.expires = &expires
+	}
+}
+
+func ensureStorageClass(expected string) ensureOption {
+	return func(opts *ensureOpts) {
+		opts.storageClass = &expected
+	}
+}
+
 func ensureContentType(contentType string) ensureOption {
 	return func(opts *ensureOpts) {
 		opts.contentType = &contentType
@@ -591,12 +615,22 @@ func ensureContentDisposition(contentDisposition string) ensureOption {
 	}
 }
 
-func ensureStorageClass(expected string) ensureOption {
+func ensureContentEncoding(contentEncoding string) ensureOption {
 	return func(opts *ensureOpts) {
-		opts.storageClass = &expected
+		opts.contentEncoding = &contentEncoding
+	}
+}
+func ensureEncryptionMethod(encryptionMethod string) ensureOption {
+	return func(opts *ensureOpts) {
+		opts.encryptionMethod = &encryptionMethod
 	}
 }
 
+func ensureEncryptionKeyID(encryptionKeyID string) ensureOption {
+	return func(opts *ensureOpts) {
+		opts.encryptionKeyID = &encryptionKeyID
+	}
+}
 func ensureArbitraryMetadata(metadata map[string]*string) ensureOption {
 	return func(opts *ensureOpts) {
 		opts.metadata = metadata
@@ -641,6 +675,24 @@ func ensureS3Object(
 		return fmt.Errorf("s3 %v/%v: (-want +got):\n%v", bucket, key, diff)
 	}
 
+	if opts.cacheControl != nil {
+		if diff := cmp.Diff(opts.cacheControl, output.CacheControl); diff != "" {
+			return fmt.Errorf("cache-control of %v/%v: (-want +got):\n%v", bucket, key, diff)
+		}
+	}
+
+	if opts.expires != nil {
+		if diff := cmp.Diff(opts.expires, output.Expires); diff != "" {
+			return fmt.Errorf("expires of %v/%v: (-want +got):\n%v", bucket, key, diff)
+		}
+	}
+
+	if opts.contentEncoding != nil {
+		if diff := cmp.Diff(opts.contentEncoding, output.ContentEncoding); diff != "" {
+			return fmt.Errorf("content-encoding of %v/%v: (-want +got):\n%v", bucket, key, diff)
+		}
+	}
+
 	if opts.contentType != nil {
 		if diff := cmp.Diff(opts.contentType, output.ContentType); diff != "" {
 			return fmt.Errorf("content-type of %v/%v: (-want +got):\n%v", bucket, key, diff)
@@ -660,6 +712,18 @@ func ensureS3Object(
 		}
 	}
 
+	if opts.encryptionMethod != nil {
+		if diff := cmp.Diff(opts.encryptionMethod, output.ServerSideEncryption); diff != "" {
+			return fmt.Errorf("encryption-method of %v/%v: (-want +got):\n%v", bucket, key, diff)
+		}
+	}
+
+	if opts.encryptionKeyID != nil {
+		if diff := cmp.Diff(opts.encryptionKeyID, output.SSEKMSKeyId); diff != "" {
+			return fmt.Errorf("encryption-key-id of %v/%v: (-want +got):\n%v", bucket, key, diff)
+		}
+	}
+
 	if opts.metadata != nil {
 		for mkey := range opts.metadata {
 			if opts.metadata[mkey] == nil || output.Metadata[mkey] == nil {
@@ -673,28 +737,27 @@ func ensureS3Object(
 	return nil
 }
 
-func putFile(t *testing.T, client *s3.S3, bucket string, filename string, content string) {
-	t.Helper()
+type putOption func(*s3.PutObjectInput)
+
+func putArbitraryMetadata(metadata map[string]*string) putOption {
+	return func(opts *s3.PutObjectInput) {
+		opts.Metadata = metadata
+	}
+}
 
-	_, err := client.PutObject(&s3.PutObjectInput{
+func putFile(t *testing.T, client *s3.S3, bucket string, filename string, content string, opts ...putOption) {
+	t.Helper()
+	input := &s3.PutObjectInput{
 		Body:   strings.NewReader(content),
 		Bucket: aws.String(bucket),
 		Key:    aws.String(filename),
-	})
-	if err != nil {
-		t.Fatal(err)
 	}
-}
 
-func putFileWithMetadata(t *testing.T, client *s3.S3, bucket string, filename string, content string, metadata map[string]*string) {
-	t.Helper()
+	for _, opt := range opts {
+		opt(input)
+	}
 
-	_, err := client.PutObject(&s3.PutObjectInput{
-		Body:     strings.NewReader(content),
-		Bucket:   aws.String(bucket),
-		Key:      aws.String(filename),
-		Metadata: metadata,
-	})
+	_, err := client.PutObject(input)
 	if err != nil {
 		t.Fatal(err)
 	}