Repository: peak/s5cmd
Pull request #437 (2022-04-14 09:45:30)

Add nfpm configuration to .goreleaser.yml
Attempt to address #380, although I have never used goreleaser


Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 282524f..d931a37 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -54,10 +54,24 @@ brews:
     folder: Formula
     name: s5cmd
     description: Parallel S3 and local filesystem execution tool
-    homepage:  https://github.com/peak/s5cmd
+    homepage: https://github.com/peak/s5cmd
     license: MIT
     test: |
       system "#{bin}/s5cmd version"
 
+nfpms:
+  -
+    vendor: Peak
+    homepage: https://github.com/peak/s5cmd
+    license: MIT
+    maintainer: Peak <packaging@peak.com>
+    description: |-
+      Very fast S3 and local filesystem execution tool. It comes with support
+      for a multitude of operations including tab completion and wildcard
+      support for files, which can be very handy for your object storage
+      workflow while working with large number of files.
+    formats:
+      - deb
+
 release:
   draft: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1406549..a3569b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
 
 ## not released yet
 
+#### Features
+- Added `--content-type` and `--content-encoding` flags to `cp` command. ([#264](https://github.com/peak/s5cmd/issues/264))
+- Added `--profile` flag to allow users to specify a [named profile](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html). ([#353](https://github.com/peak/s5cmd/issues/353))
+- Added `--credentials-file` flag to allow users to specify path for the AWS credentials file instead of using the [default location](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-where). 
+
+#### Improvements
+- Disable AWS SDK logger if log level is not "trace"
+
+#### Bugfixes
+- Fixed a bug where (`--stat`) prints unnecessarily when used with help and version commands ([#452](https://github.com/peak/s5cmd/issues/452))
+- Changed cp error message to be more precise. "given object not found" error message now will also include absolute path of the file. ([#463](https://github.com/peak/s5cmd/pull/463))
+- Fixed a bug where proxy is not being used when `--no-verify-ssl` flag is used. ([#445](https://github.com/peak/s5cmd/issues/445))
+
+
+## v2.0.0 - 4 Jul 2022
+
 #### Breaking changes
 - Dropped inline comment feature for `run` command. Previously s5cmd supported a command with an inline comment like `ls s3://bucket/object.gz  # inline comment`. ([#309](https://github.com/peak/s5cmd/issues/309))
 - Changed homebrew installation command on macOS. Users can install s5cmd via `brew install peak/tap/s5cmd`. ([#356](https://github.com/peak/s5cmd/issues/356))
@@ -15,15 +31,20 @@
 - Added `--ignore-glacier-warnings` flag to `cp`, `mv` and `select` commands. ([#346](https://github.com/peak/s5cmd/issues/346))
 - Added `--request-payer` flag to include `x-amz-request-payer` in header while sending GET, POST and HEAD requests. ([#297](https://github.com/peak/s5cmd/issues/297)) [@Kirill888](https://github.com/Kirill888)
 - Added `--use-list-objects-v1` flag to force using S3 ListObjects API instead of ListObjectsV2 API. ([#405](https://github.com/peak/s5cmd/issues/405)) [@greenpau](https://github.com/greenpau)
+- Added trace log level(`--log=trace`) which enables SDK debug logs.([#363](https://github.com/peak/s5cmd/issues/363))
 
 #### Improvements
 - Upgraded minimum required Go version to 1.16.
+- Debian packages are provided on [releases page](https://github.com/peak/s5cmd/releases) ([#380](https://github.com/peak/s5cmd/issues/380))
 
 #### Bugfixes
 - Fixed a bug about precedence of region detection, which auto region detection would always override region defined in environment or profile. ([#325](https://github.com/peak/s5cmd/issues/325))
 - Fixed a bug where errors did not result a non-zero exit code. ([#304](https://github.com/peak/s5cmd/issues/304))
 - Print error if the commands file of `run` command is not accessible. ([#410](https://github.com/peak/s5cmd/pull/410))
 - Updated region detection call to use current session's address resolving method ([#314](https://github.com/peak/s5cmd/issues/314))
+- Fixed a bug where lines with large tokens fail in `run` command. `sync` was failing when it finds multiple files to remove. ([#435](https://github.com/peak/s5cmd/issues/435), [#436](https://github.com/peak/s5cmd/issues/436))
+- Print usage error if given log level(`--log`) is not valid. ([#430](https://github.com/peak/s5cmd/pull/430))
+- Fixed a bug where (`--stat`) is ignored when log level is error. ([#359](https://github.com/peak/s5cmd/issues/359))
 
 ## v1.4.0 - 21 Sep 2021
 
diff --git a/README.md b/README.md
index f393342..f0f2a35 100644
--- a/README.md
+++ b/README.md
@@ -48,25 +48,45 @@ storage services and local filesystems.
 
 ## Installation
 
-### Binaries
+### Official Releases
+
+#### Binaries
 
 The [Releases](https://github.com/peak/s5cmd/releases) page provides pre-built
 binaries for Linux, macOS and Windows.
 
-### Homebrew
+#### Homebrew
 
 For macOS, a [homebrew](https://brew.sh) tap is provided:
 
     brew install peak/tap/s5cmd
 
-### MacPorts
+### Unofficial Releases (by Community)
+> **Warning**  
+> These releases are maintained by the community. They might be out of date compared to the official releases.
 
+#### MacPorts
 You can also install `s5cmd` from [MacPorts](https://ports.macports.org/port/s5cmd/summary) on macOS:
 
     sudo port selfupdate
     sudo port install s5cmd
 
-NOTE: MacPorts is not officially supported. The versions might be out of date compared to Homebrew.
+#### Conda
+`s5cmd` is [included](https://anaconda.org/conda-forge/s5cmd ) in the [conda-forge]( https://conda-forge.org ) channel, and it can be downloaded through the [Conda](https://docs.conda.io/).
+
+> Installing `s5cmd` from the `conda-forge` channel can be achieved by adding `conda-forge` to your channels with:
+> ```
+> conda config --add channels conda-forge
+> conda config --set channel_priority strict
+> ```
+> 
+> Once the `conda-forge` channel has been enabled, `s5cmd` can be installed with `conda`:
+> 
+> ```
+> conda install s5cmd
+> ```
+ps.  Quoted from [s5cmd feedstock](https://github.com/conda-forge/s5cmd-feedstock). You can also find further instructions on its [README](https://github.com/conda-forge/s5cmd-feedstock/blob/main/README.md).
+
 
 ### Build from source
 
@@ -393,7 +413,7 @@ s5cmd --use-list-objects-v1 ls s3://bucket/
 
 `s5cmd` uses official AWS SDK to access S3. SDK requires credentials to sign
 requests to AWS. Credentials can be provided in a variety of ways:
-
+- Command line options `--profile` to use a [named profile](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html), `--credentials-file` flag to use the specified credentials file, and `--no-sign-request` to send requests anonymously
 - Environment variables
 - AWS credentials file, including profile selection via `AWS_PROFILE` environment
   variable
@@ -403,6 +423,7 @@ requests to AWS. Credentials can be provided in a variety of ways:
 The SDK detects and uses the built-in providers automatically, without requiring
 manual configurations.
 
+
 ### Region detection
 
 While executing the commands, `s5cmd` detects the region according to the following order of priority:
@@ -443,6 +464,8 @@ or an alternative with environment variable
 
 all variants will return your GCS buckets.
 
+`s5cmd` reads `.aws/credentials` to access Google Cloud Storage. Populate the `aws_access_key_id` and `aws_secret_access_key` fields in `.aws/credentials` with an HMAC key created using this [procedure](https://cloud.google.com/storage/docs/authentication/managing-hmackeys#create).
+
 `s5cmd` will use virtual-host style bucket resolving for S3, S3 transfer
 acceleration and GCS. If a custom endpoint is provided, it'll fallback to
 path-style.
@@ -506,6 +529,33 @@ ERROR "cp s3://somebucket/file.txt file.txt": object already exists
     "error": "'cp s3://somebucket/file.txt file.txt': object already exists"
 }
 ```
+
+## Configuring Concurrency
+
+### numworkers
+
+`numworkers` is a global option that sets the size of the global worker pool. Default value of `numworkers` is [256](https://github.com/peak/s5cmd/blob/master/command/app.go#L18).
+Commands such as `cp`, `select` and `run`, which can benefit from parallelism use this worker pool to execute tasks. A task can be an upload, a download or anything in a [`run` file](https://github.com/peak/s5cmd/blob/master/command/app.go#L18).
+
+For example, if you are uploading 100 files to an S3 bucket and the `--numworkers` is set to 10, then `s5cmd` will limit the number of files concurrently uploaded to 10.
+
+```
+s5cmd --numworkers 10 cp '/Users/foo/bar/*' s3://mybucket/foo/bar/
+```
+
+### concurrency
+
+`concurrency` is a `cp` command option. It sets the number of parts that will be uploaded or downloaded in parallel for a single file.
+This parameter is used by the AWS Go SDK. Default value of `concurrency` is `5`.
+
+`numworkers` and `concurrency` options can be used together:
+
+```
+s5cmd --numworkers 10 cp --concurrency 10 '/Users/foo/bar/*' s3://mybucket/foo/bar/
+```
+
+If you have a few, large files to download, setting `--numworkers` to a very high value will not affect download speed. In this scenario setting `--concurrency` to a higher value may have a better impact on the download speed.
+
 ## Benchmarks
 Some benchmarks regarding the performance of `s5cmd` are introduced below. For more
 details refer to this [post](https://medium.com/@joshua_robinson/s5cmd-for-high-performance-object-storage-7071352cc09d)
diff --git a/command/app.go b/command/app.go
index f10a759..23f4588 100644
--- a/command/app.go
+++ b/command/app.go
@@ -49,10 +49,13 @@ var app = &cli.App{
 			Name:  "no-verify-ssl",
 			Usage: "disable SSL certificate verification",
 		},
-		&cli.StringFlag{
-			Name:  "log",
-			Value: "info",
-			Usage: "log level: (debug, info, error)",
+		&cli.GenericFlag{
+			Name: "log",
+			Value: &EnumValue{
+				Enum:    []string{"trace", "debug", "info", "error"},
+				Default: "info",
+			},
+			Usage: "log level: (trace, debug, info, error)",
 		},
 		&cli.BoolFlag{
 			Name:  "install-completion",
@@ -78,6 +81,14 @@ var app = &cli.App{
 			Name:  "request-payer",
 			Usage: "who pays for request (access requester pays buckets)",
 		},
+		&cli.StringFlag{
+			Name:  "profile",
+			Usage: "use the specified profile from the credentials file",
+		},
+		&cli.StringFlag{
+			Name:  "credentials-file",
+			Usage: "use the specified credentials file instead of the default credentials file",
+		},
 	},
 	Before: func(c *cli.Context) error {
 		retryCount := c.Int("retry-count")
@@ -94,6 +105,16 @@ var app = &cli.App{
 			printError(commandFromContext(c), c.Command.Name, err)
 			return err
 		}
+		if c.Bool("no-sign-request") && c.String("profile") != "" {
+			err := fmt.Errorf(`"no-sign-request" and "profile" flags cannot be used together`)
+			printError(commandFromContext(c), c.Command.Name, err)
+			return err
+		}
+		if c.Bool("no-sign-request") && c.String("credentials-file") != "" {
+			err := fmt.Errorf(`"no-sign-request" and "credentials-file" flags cannot be used together`)
+			printError(commandFromContext(c), c.Command.Name, err)
+			return err
+		}
 
 		if isStat {
 			stat.InitStat()
@@ -139,8 +160,8 @@ var app = &cli.App{
 		return cli.ShowAppHelp(c)
 	},
 	After: func(c *cli.Context) error {
-		if c.Bool("stat") {
-			log.Info(stat.Statistics())
+		if c.Bool("stat") && len(stat.Statistics()) > 0 {
+			log.Stat(stat.Statistics())
 		}
 
 		parallel.Close()
@@ -152,13 +173,16 @@ var app = &cli.App{
 // NewStorageOpts creates storage.Options object from the given context.
 func NewStorageOpts(c *cli.Context) storage.Options {
 	return storage.Options{
-		MaxRetries:       c.Int("retry-count"),
-		Endpoint:         c.String("endpoint-url"),
-		NoVerifySSL:      c.Bool("no-verify-ssl"),
 		DryRun:           c.Bool("dry-run"),
+		Endpoint:         c.String("endpoint-url"),
+		MaxRetries:       c.Int("retry-count"),
 		NoSignRequest:    c.Bool("no-sign-request"),
-		UseListObjectsV1: c.Bool("use-list-objects-v1"),
+		NoVerifySSL:      c.Bool("no-verify-ssl"),
 		RequestPayer:     c.String("request-payer"),
+		UseListObjectsV1: c.Bool("use-list-objects-v1"),
+		Profile:          c.String("profile"),
+		CredentialFile:   c.String("credentials-file"),
+		LogLevel:         log.LevelFromString(c.String("log")),
 	}
 }
 
diff --git a/command/cp.go b/command/cp.go
index aa78e41..cba3c42 100644
--- a/command/cp.go
+++ b/command/cp.go
@@ -2,6 +2,7 @@ package command
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -97,6 +98,9 @@ Examples:
 
 	20. Download an S3 object from a requester pays bucket
 		 > s5cmd --request-payer=requester {{.HelpName}} s3://bucket/prefix/object.gz .
+
+	21. Upload a file to S3 with a content-type and content-encoding header
+		 > s5cmd --content-type "text/css" --content-encoding "br" myfile.css.br s3://bucket/
 `
 
 func NewSharedFlags() []cli.Flag {
@@ -165,6 +169,14 @@ func NewSharedFlags() []cli.Flag {
 			Name:  "raw",
 			Usage: "disable the wildcard operations, useful with filenames that contains glob characters",
 		},
+		&cli.StringFlag{
+			Name:  "content-type",
+			Usage: "set content type for target: defines content type header for object, e.g. --content-type text/plain",
+		},
+		&cli.StringFlag{
+			Name:  "content-encoding",
+			Usage: "set content encoding for target: defines content encoding header for object, e.g. --content-encoding gzip",
+		},
 	}
 }
 
@@ -243,6 +255,8 @@ type Copy struct {
 	raw                   bool
 	cacheControl          string
 	expires               string
+	contentType           string
+	contentEncoding       string
 
 	// region settings
 	srcRegion string
@@ -280,6 +294,8 @@ func NewCopy(c *cli.Context, deleteSource bool) Copy {
 		raw:                   c.Bool("raw"),
 		cacheControl:          c.String("cache-control"),
 		expires:               c.String("expires"),
+		contentType:           c.String("content-type"),
+		contentEncoding:       c.String("content-encoding"),
 		// region settings
 		srcRegion: c.String("source-region"),
 		dstRegion: c.String("destination-region"),
@@ -549,7 +565,6 @@ func (c Copy) doUpload(ctx context.Context, srcurl *url.URL, dsturl *url.URL) er
 	}
 
 	metadata := storage.NewMetadata().
-		SetContentType(guessContentType(file)).
 		SetStorageClass(string(c.storageClass)).
 		SetSSE(c.encryptionMethod).
 		SetSSEKeyID(c.encryptionKeyID).
@@ -557,6 +572,16 @@ func (c Copy) doUpload(ctx context.Context, srcurl *url.URL, dsturl *url.URL) er
 		SetCacheControl(c.cacheControl).
 		SetExpires(c.expires)
 
+	if c.contentType != "" {
+		metadata.SetContentType(c.contentType)
+	} else {
+		metadata.SetContentType(guessContentType(file))
+	}
+
+	if c.contentEncoding != "" {
+		metadata.SetContentEncoding(c.contentEncoding)
+	}
+
 	err = dstClient.Put(ctx, file, dsturl, metadata, c.concurrency, c.partSize)
 	if err != nil {
 		return err
@@ -605,6 +630,13 @@ func (c Copy) doCopy(ctx context.Context, srcurl, dsturl *url.URL) error {
 		SetCacheControl(c.cacheControl).
 		SetExpires(c.expires)
 
+	if c.contentType != "" {
+		metadata.SetContentType(c.contentType)
+	}
+	if c.contentEncoding != "" {
+		metadata.SetContentEncoding(c.contentEncoding)
+	}
+
 	err = c.shouldOverride(ctx, srcurl, dsturl)
 	if err != nil {
 		if errorpkg.IsWarning(err) {
@@ -749,8 +781,12 @@ func prepareLocalDestination(
 	}
 
 	obj, err := client.Stat(ctx, dsturl)
-	if err != nil && err != storage.ErrGivenObjectNotFound {
-		return nil, err
+
+	if err != nil {
+		var objNotFound *storage.ErrGivenObjectNotFound
+		if !errors.As(err, &objNotFound) {
+			return nil, err
+		}
 	}
 
 	if isBatch && !flatten {
@@ -760,8 +796,8 @@ func prepareLocalDestination(
 			return nil, err
 		}
 	}
-
-	if err == storage.ErrGivenObjectNotFound {
+	var objNotFound *storage.ErrGivenObjectNotFound
+	if errors.As(err, &objNotFound) {
 		err := client.MkdirAll(dsturl.Dir())
 		if err != nil {
 			return nil, err
@@ -782,7 +818,8 @@ func prepareLocalDestination(
 // found, error and returning object would be nil.
 func getObject(ctx context.Context, url *url.URL, client storage.Storage) (*storage.Object, error) {
 	obj, err := client.Stat(ctx, url)
-	if err == storage.ErrGivenObjectNotFound {
+	var objNotFound *storage.ErrGivenObjectNotFound
+	if errors.As(err, &objNotFound) {
 		return nil, nil
 	}
 
diff --git a/command/expand.go b/command/expand.go
index 838ca35..f58c295 100644
--- a/command/expand.go
+++ b/command/expand.go
@@ -2,6 +2,7 @@ package command
 
 import (
 	"context"
+	"errors"
 	"sync"
 
 	"github.com/peak/s5cmd/atomic"
@@ -68,7 +69,8 @@ func expandSources(
 
 				objch, err := expandSource(ctx, client, followSymlinks, origSrc)
 				if err != nil {
-					if err != storage.ErrGivenObjectNotFound {
+					var objNotFound *storage.ErrGivenObjectNotFound
+					if !errors.As(err, &objNotFound) {
 						ch <- &storage.Object{Err: err}
 					}
 					return
diff --git a/command/flag.go b/command/flag.go
new file mode 100644
index 0000000..b0b9e26
--- /dev/null
+++ b/command/flag.go
@@ -0,0 +1,30 @@
+package command
+
+import (
+	"fmt"
+	"strings"
+)
+
+type EnumValue struct {
+	Enum     []string
+	Default  string
+	selected string
+}
+
+func (e *EnumValue) Set(value string) error {
+	for _, enum := range e.Enum {
+		if enum == value {
+			e.selected = value
+			return nil
+		}
+	}
+
+	return fmt.Errorf("allowed values: [%s]", strings.Join(e.Enum, ", "))
+}
+
+func (e EnumValue) String() string {
+	if e.selected == "" {
+		return e.Default
+	}
+	return e.selected
+}
diff --git a/command/run.go b/command/run.go
index 37e8cb9..0b17748 100644
--- a/command/run.go
+++ b/command/run.go
@@ -81,7 +81,6 @@ func NewRun(c *cli.Context, r io.Reader) Run {
 }
 
 func (r Run) Run(ctx context.Context) error {
-	reader := r.reader
 	pm := parallel.New(r.numWorkers)
 	defer pm.Close()
 
@@ -96,10 +95,10 @@ func (r Run) Run(ctx context.Context) error {
 		}
 	}()
 
-	scanner := NewScanner(ctx, reader)
+	reader := NewReader(ctx, r.reader)
 
 	lineno := -1
-	for line := range scanner.Scan() {
+	for line := range reader.Read() {
 		lineno++
 
 		line = strings.TrimSpace(line)
@@ -153,60 +152,67 @@ func (r Run) Run(ctx context.Context) error {
 	waiter.Wait()
 	<-errDoneCh
 
-	return multierror.Append(merrorWaiter, scanner.Err()).ErrorOrNil()
+	if reader.Err() != nil {
+		printError(commandFromContext(r.c), r.c.Command.Name, reader.Err())
+	}
+
+	return multierror.Append(merrorWaiter, reader.Err()).ErrorOrNil()
 }
 
-// Scanner is a cancelable scanner.
-type Scanner struct {
-	*bufio.Scanner
+// Reader is a cancelable reader.
+type Reader struct {
+	*bufio.Reader
 	err    error
 	linech chan string
 	ctx    context.Context
 }
 
-// NewScanner creates a new scanner with cancellation.
-func NewScanner(ctx context.Context, r io.Reader) *Scanner {
-	scanner := &Scanner{
-		ctx:     ctx,
-		Scanner: bufio.NewScanner(r),
-		linech:  make(chan string),
+// NewReader creates a new reader with cancellation.
+func NewReader(ctx context.Context, r io.Reader) *Reader {
+	reader := &Reader{
+		ctx:    ctx,
+		Reader: bufio.NewReader(r),
+		linech: make(chan string),
 	}
 
-	go scanner.scan()
-	return scanner
+	go reader.read()
+	return reader
 }
 
-// scan read the underlying reader.
-func (s *Scanner) scan() {
-	defer close(s.linech)
+// read reads lines from the underlying reader.
+func (r *Reader) read() {
+	defer close(r.linech)
 
 	for {
 		select {
-		case <-s.ctx.Done():
-			s.err = s.ctx.Err()
+		case <-r.ctx.Done():
+			r.err = r.ctx.Err()
 			return
 		default:
-			if !s.Scanner.Scan() {
-				return
+			// If ReadString encounters an error before finding a delimiter,
+			// it returns the data read before the error and the error itself (often io.EOF).
+			line, err := r.ReadString('\n')
+			if line != "" {
+				r.linech <- line
+			}
+			if err != nil {
+				if err == io.EOF {
+					return
+				}
+				r.err = multierror.Append(r.err, err)
 			}
-
-			s.linech <- s.Scanner.Text()
 		}
 	}
 }
 
-// Scan returns read-only channel to consume lines.
-func (s *Scanner) Scan() <-chan string {
-	return s.linech
+// Read returns read-only channel to consume lines.
+func (r *Reader) Read() <-chan string {
+	return r.linech
 }
 
 // Err returns encountered errors, if any.
-func (s *Scanner) Err() error {
-	if s.err != nil {
-		return s.err
-	}
-
-	return s.Scanner.Err()
+func (r *Reader) Err() error {
+	return r.err
 }
 
 func validateRunCommand(c *cli.Context) error {
diff --git a/command/select.go b/command/select.go
index 83ed871..1db02ef 100644
--- a/command/select.go
+++ b/command/select.go
@@ -47,10 +47,13 @@ func NewSelectCommand() *cli.Command {
 				Usage: "input compression format",
 				Value: "NONE",
 			},
-			&cli.StringFlag{
+			&cli.GenericFlag{
 				Name:  "format",
 				Usage: "input data format (only JSON supported for the moment)",
-				Value: "JSON",
+				Value: &EnumValue{
+					Enum:    []string{"JSON"},
+					Default: "JSON",
+				},
 			},
 			&cli.StringSliceFlag{
 				Name:  "exclude",
diff --git a/log/log.go b/log/log.go
index 3452be9..3d03763 100644
--- a/log/log.go
+++ b/log/log.go
@@ -22,19 +22,30 @@ func Init(level string, json bool) {
 	global = New(level, json)
 }
 
+// Trace prints message in trace mode.
+func Trace(msg Message) {
+	global.printf(LevelTrace, msg, os.Stdout)
+}
+
 // Debug prints message in debug mode.
 func Debug(msg Message) {
-	global.printf(levelDebug, msg, os.Stdout)
+	global.printf(LevelDebug, msg, os.Stdout)
 }
 
 // Info prints message in info mode.
 func Info(msg Message) {
-	global.printf(levelInfo, msg, os.Stdout)
+	global.printf(LevelInfo, msg, os.Stdout)
+}
+
+// Stat prints stat message regardless of the log level with info print formatting.
+// It uses printfHelper instead of printf to ignore the log level condition.
+func Stat(msg Message) {
+	global.printfHelper(LevelInfo, msg, os.Stdout)
 }
 
 // Error prints message in error mode.
 func Error(msg Message) {
-	global.printf(levelError, msg, os.Stderr)
+	global.printf(LevelError, msg, os.Stderr)
 }
 
 // Close closes logger and its channel.
@@ -47,12 +58,12 @@ func Close() {
 type Logger struct {
 	donech chan struct{}
 	json   bool
-	level  logLevel
+	level  LogLevel
 }
 
 // New creates new logger.
 func New(level string, json bool) *Logger {
-	logLevel := levelFromString(level)
+	logLevel := LevelFromString(level)
 	logger := &Logger{
 		donech: make(chan struct{}),
 		json:   json,
@@ -63,11 +74,14 @@ func New(level string, json bool) *Logger {
 }
 
 // printf prints message according to the given level, message and std mode.
-func (l *Logger) printf(level logLevel, message Message, std *os.File) {
+func (l *Logger) printf(level LogLevel, message Message, std *os.File) {
 	if level < l.level {
 		return
 	}
+	l.printfHelper(level, message, std)
+}
 
+func (l *Logger) printfHelper(level LogLevel, message Message, std *os.File) {
 	if l.json {
 		outputCh <- output{
 			message: message.JSON(),
@@ -90,40 +104,49 @@ func (l *Logger) out() {
 	}
 }
 
-// logLevel is the level of Logger.
-type logLevel int
+// LogLevel is the level of Logger.
+type LogLevel int
 
 const (
-	levelDebug logLevel = iota
-	levelInfo
-	levelError
+	LevelTrace LogLevel = iota
+	LevelDebug
+	LevelInfo
+	LevelError
 )
 
 // String returns the string representation of logLevel.
-func (l logLevel) String() string {
+func (l LogLevel) String() string {
 	switch l {
-	case levelInfo:
+	case LevelInfo:
 		return ""
-	case levelError:
+	case LevelError:
 		return "ERROR "
-	case levelDebug:
+	case LevelDebug:
 		return "DEBUG "
+	case LevelTrace:
+		// levelTrace is used for printing aws sdk logs and
+		// aws-sdk-go already adds "DEBUG" prefix to logs.
+		// So do not add another prefix to log which makes it
+		// look weird.
+		return ""
 	default:
 		return "UNKNOWN "
 	}
 }
 
-// levelFromString returns logLevel for given string. It
+// LevelFromString returns logLevel for given string. It
 // return `levelInfo` as a default.
-func levelFromString(s string) logLevel {
+func LevelFromString(s string) LogLevel {
 	switch s {
 	case "debug":
-		return levelDebug
+		return LevelDebug
 	case "info":
-		return levelInfo
+		return LevelInfo
 	case "error":
-		return levelError
+		return LevelError
+	case "trace":
+		return LevelTrace
 	default:
-		return levelInfo
+		return LevelInfo
 	}
 }
diff --git a/log/message.go b/log/message.go
index df2d0ab..0938ff0 100644
--- a/log/message.go
+++ b/log/message.go
@@ -63,6 +63,18 @@ type DebugMessage struct {
 	Err       string `json:"error"`
 }
 
+type TraceMessage struct {
+	Message string `json:"message"`
+}
+
+func (t TraceMessage) String() string {
+	return t.Message
+}
+
+func (t TraceMessage) JSON() string {
+	return strutil.JSON(t)
+}
+
 // String is the string representation of ErrorMessage.
 func (d DebugMessage) String() string {
 	if d.Command == "" {
diff --git a/storage/fs.go b/storage/fs.go
index 37ce48b..628244a 100644
--- a/storage/fs.go
+++ b/storage/fs.go
@@ -22,7 +22,7 @@ func (f *Filesystem) Stat(ctx context.Context, url *url.URL) (*Object, error) {
 	st, err := os.Stat(url.Absolute())
 	if err != nil {
 		if os.IsNotExist(err) {
-			return nil, ErrGivenObjectNotFound
+			return nil, &ErrGivenObjectNotFound{ObjectAbsPath: url.Absolute()}
 		}
 		return nil, err
 	}
diff --git a/storage/s3.go b/storage/s3.go
index 24373e6..7f9fbdd 100644
--- a/storage/s3.go
+++ b/storage/s3.go
@@ -118,7 +118,7 @@ func (s *S3) Stat(ctx context.Context, url *url.URL) (*Object, error) {
 	})
 	if err != nil {
 		if errHasCode(err, "NotFound") {
-			return nil, ErrGivenObjectNotFound
+			return nil, &ErrGivenObjectNotFound{ObjectAbsPath: url.Absolute()}
 		}
 		return nil, err
 	}
@@ -549,6 +549,11 @@ func (s *S3) Put(
 		}
 	}
 
+	contentEncoding := metadata.ContentEncoding()
+	if contentEncoding != "" {
+		input.ContentEncoding = aws.String(contentEncoding)
+	}
+
 	_, err := s.uploader.UploadWithContext(ctx, input, func(u *s3manager.Uploader) {
 		u.PartSize = partSize
 		u.Concurrency = concurrency
@@ -740,6 +745,15 @@ func (s *S3) RemoveBucket(ctx context.Context, name string) error {
 	return err
 }
 
+type sdkLogger struct{}
+
+func (l sdkLogger) Log(args ...interface{}) {
+	msg := log.TraceMessage{
+		Message: fmt.Sprint(args...),
+	}
+	log.Trace(msg)
+}
+
 // SessionCache holds session.Session according to s3Opts and it synchronizes
 // access/modification.
 type SessionCache struct {
@@ -761,7 +775,11 @@ func (sc *SessionCache) newSession(ctx context.Context, opts Options) (*session.
 
 	if opts.NoSignRequest {
 		// do not sign requests when making service API calls
-		awsCfg.Credentials = credentials.AnonymousCredentials
+		awsCfg = awsCfg.WithCredentials(credentials.AnonymousCredentials)
+	} else if opts.CredentialFile != "" || opts.Profile != "" {
+		awsCfg = awsCfg.WithCredentials(
+			credentials.NewSharedCredentials(opts.CredentialFile, opts.Profile),
+		)
 	}
 
 	endpointURL, err := parseEndpoint(opts.Endpoint)
@@ -792,6 +810,11 @@ func (sc *SessionCache) newSession(ctx context.Context, opts Options) (*session.
 		WithS3UseAccelerate(useAccelerate).
 		WithHTTPClient(httpClient)
 
+	if opts.LogLevel == log.LevelTrace {
+		awsCfg = awsCfg.WithLogLevel(aws.LogDebug).
+			WithLogger(sdkLogger{})
+	}
+
 	awsCfg.Retryer = newCustomRetryer(opts.MaxRetries)
 
 	useSharedConfig := session.SharedConfigEnable
@@ -916,6 +939,7 @@ func (c *customRetryer) ShouldRetry(req *request.Request) bool {
 var insecureHTTPClient = &http.Client{
 	Transport: &http.Transport{
 		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		Proxy:           http.ProxyFromEnvironment,
 	},
 }
 
diff --git a/storage/storage.go b/storage/storage.go
index 836cc68..83a7391 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -8,18 +8,26 @@ import (
 	"os"
 	"time"
 
+	"github.com/peak/s5cmd/log"
 	"github.com/peak/s5cmd/storage/url"
 	"github.com/peak/s5cmd/strutil"
 )
 
 var (
-	// ErrGivenObjectNotFound indicates a specified object is not found.
-	ErrGivenObjectNotFound = fmt.Errorf("given object not found")
 
 	// ErrNoObjectFound indicates there are no objects found from a given directory.
 	ErrNoObjectFound = fmt.Errorf("no object found")
 )
 
+// ErrGivenObjectNotFound indicates a specified object is not found.
+type ErrGivenObjectNotFound struct {
+	ObjectAbsPath string
+}
+
+func (e *ErrGivenObjectNotFound) Error() string {
+	return fmt.Sprintf("given object %v not found", e.ObjectAbsPath)
+}
+
 // Storage is an interface for storage operations that is common
 // to local filesystem and remote object storage.
 type Storage interface {
@@ -55,6 +63,9 @@ func NewRemoteClient(ctx context.Context, url *url.URL, opts Options) (*S3, erro
 		NoSignRequest:    opts.NoSignRequest,
 		UseListObjectsV1: opts.UseListObjectsV1,
 		RequestPayer:     opts.RequestPayer,
+		Profile:          opts.Profile,
+		CredentialFile:   opts.CredentialFile,
+		LogLevel:         opts.LogLevel,
 		bucket:           url.Bucket,
 		region:           opts.region,
 	}
@@ -76,7 +87,10 @@ type Options struct {
 	DryRun           bool
 	NoSignRequest    bool
 	UseListObjectsV1 bool
+	LogLevel         log.LogLevel
 	RequestPayer     string
+	Profile          string
+	CredentialFile   string
 	bucket           string
 	region           string
 }
@@ -265,3 +279,12 @@ func (m Metadata) SetSSEKeyID(kid string) Metadata {
 	m["EncryptionKeyID"] = kid
 	return m
 }
+
+func (m Metadata) ContentEncoding() string {
+	return m["ContentEncoding"]
+}
+
+func (m Metadata) SetContentEncoding(contentEncoding string) Metadata {
+	m["ContentEncoding"] = contentEncoding
+	return m
+}


Test patch:
diff --git a/e2e/app_test.go b/e2e/app_test.go
index 2b45a0c..a4eb32d 100644
--- a/e2e/app_test.go
+++ b/e2e/app_test.go
@@ -2,6 +2,7 @@ package e2e
 
 import (
 	"fmt"
+	"os"
 	"strconv"
 	"strings"
 	"testing"
@@ -69,21 +70,111 @@ func TestAppRetryCount(t *testing.T) {
 	}
 }
 
+// Checks if the stats are written in necessary conditions.
+// 1. Print with every log level when there is an operation
+// 2. Do not print when used with help & version commands.
 func TestAppDashStat(t *testing.T) {
 	t.Parallel()
 
-	_, s5cmd, cleanup := setup(t)
-	defer cleanup()
+	const (
+		bucket                  = "bucket"
+		fileContent             = "this is a file content"
+		src                     = "file1.txt"
+		expectedOutputIfPrinted = "Operation\tTotal\tError\tSuccess\t"
+	)
 
-	cmd := s5cmd("--stat", "ls")
-	result := icmd.RunCmd(cmd)
+	var testcases = []struct {
+		command         string
+		isPrintExpected bool
+	}{
+		{
+			command:         fmt.Sprintf("--stat --log %v cp s3://bucket/%v .", "trace", src),
+			isPrintExpected: true,
+		},
+		{
+			command:         fmt.Sprintf("--stat --log %v cp s3://bucket/%v .", "debug", src),
+			isPrintExpected: true,
+		},
+		{
+			command:         fmt.Sprintf("--stat --log %v cp s3://bucket/%v .", "info", src),
+			isPrintExpected: true,
+		},
+		{
+			command:         fmt.Sprintf("--stat --log %v cp s3://bucket/%v .", "error", src),
+			isPrintExpected: true,
+		},
+		// if level is an empty string, it ignores log levels and uses default.
+		{
+			command:         "--stat help",
+			isPrintExpected: false,
+		},
+		{
+			command:         "--stat version",
+			isPrintExpected: false,
+		},
+	}
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.command, func(t *testing.T) {
+			t.Parallel()
+			s3client, s5cmd, cleanup := setup(t)
+			defer cleanup()
 
-	result.Assert(t, icmd.Success)
+			createBucket(t, s3client, bucket)
+			putFile(t, s3client, bucket, src, fileContent)
+			cmd := s5cmd(strings.Fields(tc.command)...)
 
-	out := result.Stdout()
+			result := icmd.RunCmd(cmd)
 
-	tsv := fmt.Sprintf("%s\t%s\t%s\t%s\t", "Operation", "Total", "Error", "Success")
-	assert.Assert(t, strings.Contains(out, tsv))
+			result.Assert(t, icmd.Success)
+			out := result.Stdout()
+			assert.Assert(t, tc.isPrintExpected == strings.Contains(out, expectedOutputIfPrinted))
+		})
+	}
+}
+
+func TestAppProxy(t *testing.T) {
+
+	testcases := []struct {
+		name string
+		flag string
+	}{
+		{
+			name: "without no-verify-ssl flag",
+			flag: "",
+		},
+		{
+			name: "with no-verify-ssl flag",
+			flag: "--no-verify-ssl",
+		},
+	}
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			const expectedReqs = 1
+
+			proxy := httpProxy{}
+			pxyUrl, cleanup := setupProxy(&proxy)
+			defer cleanup()
+
+			os.Setenv("http_proxy", pxyUrl)
+
+			_, s5cmd, cleanup := setup(t, withProxy())
+			defer cleanup()
+
+			var cmd icmd.Cmd
+			if tc.flag != "" {
+				cmd = s5cmd(tc.flag, "ls")
+			} else {
+				cmd = s5cmd("ls")
+			}
+
+			result := icmd.RunCmd(cmd)
+
+			result.Assert(t, icmd.Success)
+			assert.Assert(t, proxy.isSuccessful(expectedReqs))
+		})
+	}
 }
 
 func TestAppUnknownCommand(t *testing.T) {
@@ -119,3 +210,20 @@ func TestUsageError(t *testing.T) {
 		1: equals("See 's5cmd --help' for usage"),
 	})
 }
+
+func TestInvalidLoglevel(t *testing.T) {
+	t.Parallel()
+
+	_, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	cmd := s5cmd("--log", "notexist", "ls")
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Expected{ExitCode: 1})
+
+	assertLines(t, result.Stderr(), map[int]compareFunc{
+		0: equals(`Incorrect Usage: invalid value "notexist" for flag -log: allowed values: [trace, debug, info, error]`),
+		1: equals("See 's5cmd --help' for usage"),
+	})
+}
diff --git a/e2e/cp_test.go b/e2e/cp_test.go
index fe3a60a..5d1540f 100644
--- a/e2e/cp_test.go
+++ b/e2e/cp_test.go
@@ -2802,6 +2802,37 @@ func TestCopyWithFollowSymlink(t *testing.T) {
 	assert.Assert(t, ensureS3Object(s3client, bucket, "prefix/c/link2", fileContent))
 }
 
+func TestCopyErrorWhenGivenObjectIsNotFoundUsingWildcard(t *testing.T) {
+	t.Parallel()
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	const bucket = "bucket"
+	createBucket(t, s3client, bucket)
+
+	folderLayout := []fs.PathOp{
+		// we intentionally did not create a/f1.txt to
+		// trigger given object not found error.
+		fs.WithDir("b"),
+		fs.WithSymlink("b/link1", "a/f1.txt"),
+	}
+
+	workdir := fs.NewDir(t, t.Name(), folderLayout...)
+	defer workdir.Remove()
+
+	dst := fmt.Sprintf("s3://%v/prefix/", bucket)
+
+	cmd := s5cmd("cp", "*", dst)
+	result := icmd.RunCmd(cmd, withWorkingDir(workdir))
+
+	result.Assert(t, icmd.Expected{ExitCode: 1})
+
+	assertLines(t, result.Stderr(), map[int]compareFunc{
+		0: equals(`ERROR "cp * %v": given object b/link1 not found`, dst),
+	}, sortInput(true))
+}
+
 // cp --no-follow-symlinks * s3://bucket/prefix/
 func TestCopyWithNoFollowSymlink(t *testing.T) {
 	t.Parallel()
diff --git a/e2e/proxy.go b/e2e/proxy.go
new file mode 100644
index 0000000..7fce994
--- /dev/null
+++ b/e2e/proxy.go
@@ -0,0 +1,85 @@
+package e2e
+
+import (
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"sync/atomic"
+)
+
+// Hop-by-hop headers. These are removed when sent to the backend.
+// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
+var hopHeaders = []string{
+	"Connection",
+	"Keep-Alive",
+	"Proxy-Authenticate",
+	"Proxy-Authorization",
+	"Te", // canonicalized version of "TE"
+	"Trailers",
+	"Transfer-Encoding",
+	"Upgrade",
+}
+
+func copyHeader(dst, src http.Header) {
+	for k, vv := range src {
+		for _, v := range vv {
+			dst.Add(k, v)
+		}
+	}
+}
+
+func delHopHeaders(header http.Header) {
+	for _, h := range hopHeaders {
+		header.Del(h)
+	}
+}
+
+type httpProxy struct {
+	successReqs int64
+	errorReqs   int64
+}
+
+func (p *httpProxy) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
+	if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
+		msg := "unsupported protocol scheme " + req.URL.Scheme
+		http.Error(wr, msg, http.StatusBadRequest)
+		return
+	}
+	// We need to explicitly set New Transport with no Proxy to make
+	// sure ServeHttp only receives the requests once. Otherwise, it
+	// causes httpProxy to call itself infinitely over and over again.
+	client := &http.Client{
+		Transport: &http.Transport{Proxy: nil},
+	}
+	// http: Request.RequestURI can't be set in client requests.
+	// http://golang.org/src/pkg/net/http/client.go
+	req.RequestURI = ""
+
+	delHopHeaders(req.Header)
+
+	resp, err := client.Do(req)
+	if err != nil {
+		http.Error(wr, "Server Error", http.StatusInternalServerError)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode == http.StatusOK {
+		atomic.AddInt64(&p.successReqs, 1)
+	} else {
+		atomic.AddInt64(&p.errorReqs, 1)
+	}
+	delHopHeaders(resp.Header)
+
+	copyHeader(wr.Header(), resp.Header)
+	wr.WriteHeader(resp.StatusCode)
+	io.Copy(wr, resp.Body)
+}
+
+func (p *httpProxy) isSuccessful(totalReqs int64) bool {
+	return totalReqs == atomic.LoadInt64(&p.successReqs) && atomic.LoadInt64(&p.errorReqs) == 0
+}
+
+func setupProxy(p *httpProxy) (string, func()) {
+	proxysrv := httptest.NewServer(p)
+	return proxysrv.URL, proxysrv.Close
+}
diff --git a/e2e/run_test.go b/e2e/run_test.go
index b695222..e78ba2a 100644
--- a/e2e/run_test.go
+++ b/e2e/run_test.go
@@ -98,7 +98,7 @@ func TestRunFromStdinWithErrors(t *testing.T) {
 
 	assertLines(t, result.Stderr(), map[int]compareFunc{
 		0: contains(`ERROR "cp s3://%v/nonexistentobject nonexistentobject": NoSuchKey: status code: 404`, bucket),
-		1: equals(`ERROR "ls s3/": given object not found`),
+		1: equals(`ERROR "ls s3/": given object s3/ not found`),
 	}, sortInput(true))
 }
 
diff --git a/e2e/s3_fake.go b/e2e/s3_fake.go
index 466cd48..639d10c 100644
--- a/e2e/s3_fake.go
+++ b/e2e/s3_fake.go
@@ -2,6 +2,7 @@ package e2e
 
 import (
 	"net/http/httptest"
+	"net/url"
 	"strings"
 	"testing"
 
@@ -11,7 +12,7 @@ import (
 	"gotest.tools/v3/fs"
 )
 
-func s3ServerEndpoint(t *testing.T, testdir *fs.Dir, loglvl, backend string, timeSource gofakes3.TimeSource) (string, func()) {
+func s3ServerEndpoint(t *testing.T, testdir *fs.Dir, loglvl, backend string, timeSource gofakes3.TimeSource, enableProxy bool) (string, func()) {
 	var s3backend gofakes3.Backend
 	switch backend {
 	case "mem":
@@ -58,5 +59,13 @@ func s3ServerEndpoint(t *testing.T, testdir *fs.Dir, loglvl, backend string, tim
 		// after each test.
 	}
 
+	if enableProxy {
+		parsedUrl, err := url.Parse(s3srv.URL)
+		if err != nil {
+			t.Fatal(err)
+		}
+		proxyEnabledURL := "http://localhost.:" + parsedUrl.Port()
+		return proxyEnabledURL, cleanup
+	}
 	return s3srv.URL, cleanup
 }
diff --git a/e2e/sync_test.go b/e2e/sync_test.go
index ca44891..cc15548 100644
--- a/e2e/sync_test.go
+++ b/e2e/sync_test.go
@@ -1621,3 +1621,59 @@ func TestSyncLocalDirectoryToS3WithExcludeFilter(t *testing.T) {
 		})
 	}
 }
+
+// sync --delete somedir s3://bucket/ (removes 10k objects)
+func TestIssue435(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t, withS3Backend("mem"))
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	// empty folder
+	folderLayout := []fs.PathOp{}
+
+	workdir := fs.NewDir(t, "somedir", folderLayout...)
+	defer workdir.Remove()
+
+	const filecount = 10_000
+
+	filenameFunc := func(i int) string { return fmt.Sprintf("file_%06d", i) }
+	contentFunc := func(i int) string { return fmt.Sprintf("file body %06d", i) }
+
+	for i := 0; i < filecount; i++ {
+		filename := filenameFunc(i)
+		content := contentFunc(i)
+		putFile(t, s3client, bucket, filename, content)
+	}
+
+	src := fmt.Sprintf("%v/", workdir.Path())
+	src = filepath.ToSlash(src)
+	dst := fmt.Sprintf("s3://%v/", bucket)
+
+	cmd := s5cmd("--log", "debug", "sync", "--delete", src, dst)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	assertLines(t, result.Stderr(), map[int]compareFunc{})
+
+	expected := make(map[int]compareFunc)
+	for i := 0; i < filecount; i++ {
+		expected[i] = contains("rm s3://%v/file_%06d", bucket, i)
+	}
+
+	assertLines(t, result.Stdout(), expected, sortInput(true))
+
+	// assert s3 objects
+	for i := 0; i < filecount; i++ {
+		filename := filenameFunc(i)
+		content := contentFunc(i)
+
+		err := ensureS3Object(s3client, bucket, filename, content)
+		assertError(t, err, errS3NoSuchKey)
+	}
+}
diff --git a/e2e/util_test.go b/e2e/util_test.go
index 38efbc8..cfede70 100644
--- a/e2e/util_test.go
+++ b/e2e/util_test.go
@@ -60,6 +60,7 @@ type setupOpts struct {
 	s3backend   string
 	endpointURL string
 	timeSource  gofakes3.TimeSource
+	enableProxy bool
 }
 
 type option func(*setupOpts)
@@ -82,6 +83,12 @@ func withTimeSource(timeSource gofakes3.TimeSource) option {
 	}
 }
 
+func withProxy() option {
+	return func(opts *setupOpts) {
+		opts.enableProxy = true
+	}
+}
+
 func setup(t *testing.T, options ...option) (*s3.S3, func(...string) icmd.Cmd, func()) {
 	t.Helper()
 
@@ -124,7 +131,7 @@ func server(t *testing.T, opts *setupOpts) (string, string, func()) {
 		s3LogLevel = "info" // aws has no level other than 'debug'
 	}
 
-	endpoint, dbcleanup := s3ServerEndpoint(t, testdir, s3LogLevel, opts.s3backend, opts.timeSource)
+	endpoint, dbcleanup := s3ServerEndpoint(t, testdir, s3LogLevel, opts.s3backend, opts.timeSource, opts.enableProxy)
 	if opts.endpointURL != "" {
 		endpoint = opts.endpointURL
 	}
diff --git a/storage/s3_test.go b/storage/s3_test.go
index b4a049c..a31f2f0 100644
--- a/storage/s3_test.go
+++ b/storage/s3_test.go
@@ -128,6 +128,88 @@ func TestNewSessionWithNoSignRequest(t *testing.T) {
 	}
 }
 
+func TestNewSessionWithProfileFromFile(t *testing.T) {
+	// create a temporary credentials file
+	file, err := os.CreateTemp("", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(file.Name())
+
+	profiles := `[default]
+aws_access_key_id = default_profile_key_id
+aws_secret_access_key = default_profile_access_key
+
+[p1]
+aws_access_key_id = p1_profile_key_id
+aws_secret_access_key = p1_profile_access_key
+
+[p2]
+aws_access_key_id = p2_profile_key_id
+aws_secret_access_key = p2_profile_access_key`
+
+	_, err = file.Write([]byte(profiles))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	testcases := []struct {
+		name               string
+		fileName           string
+		profileName        string
+		expAccessKeyId     string
+		expSecretAccessKey string
+	}{
+		{
+			name:               "use default profile",
+			fileName:           file.Name(),
+			profileName:        "",
+			expAccessKeyId:     "default_profile_key_id",
+			expSecretAccessKey: "default_profile_access_key",
+		},
+		{
+			name:               "use a non-default profile",
+			fileName:           file.Name(),
+			profileName:        "p1",
+			expAccessKeyId:     "p1_profile_key_id",
+			expSecretAccessKey: "p1_profile_access_key",
+		},
+		{
+
+			name:               "use a non-existent profile",
+			fileName:           file.Name(),
+			profileName:        "non-existent-profile",
+			expAccessKeyId:     "",
+			expSecretAccessKey: "",
+		},
+	}
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			globalSessionCache.clear()
+			sess, err := globalSessionCache.newSession(context.Background(), Options{
+				Profile:        tc.profileName,
+				CredentialFile: tc.fileName,
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			got, err := sess.Config.Credentials.Get()
+			if err != nil {
+				// if there should be such a profile but received an error fail,
+				// ignore the error otherwise.
+				if tc.expAccessKeyId != "" || tc.expSecretAccessKey != "" {
+					t.Fatal(err)
+				}
+			}
+
+			if got.AccessKeyID != tc.expAccessKeyId || got.SecretAccessKey != tc.expSecretAccessKey {
+				t.Errorf("Expected credentials does not match the credential we got!\nExpected: Access Key ID: %v, Secret Access Key: %v\nGot    : Access Key ID: %v, Secret Access Key: %v\n", tc.expAccessKeyId, tc.expSecretAccessKey, got.AccessKeyID, got.SecretAccessKey)
+			}
+		})
+	}
+}
+
 func TestS3ListURL(t *testing.T) {
 	url, err := url.New("s3://bucket/key")
 	if err != nil {
@@ -1036,6 +1118,52 @@ func TestS3ListObjectsAPIVersions(t *testing.T) {
 	})
 }
 
+func TestAWSLogLevel(t *testing.T) {
+	testcases := []struct {
+		name     string
+		level    string
+		expected aws.LogLevelType
+	}{
+		{
+			name:     "Trace: log level must be aws.LogDebug",
+			level:    "trace",
+			expected: aws.LogDebug,
+		},
+		{
+			name:     "Debug: log level must be aws.LogOff",
+			level:    "debug",
+			expected: aws.LogOff,
+		},
+		{
+			name:     "Info: log level must be aws.LogOff",
+			level:    "info",
+			expected: aws.LogOff,
+		},
+		{
+			name:     "Error: log level must be aws.LogOff",
+			level:    "error",
+			expected: aws.LogOff,
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			globalSessionCache.clear()
+			sess, err := globalSessionCache.newSession(context.Background(), Options{
+				LogLevel: log.LevelFromString(tc.level),
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			cfgLogLevel := *sess.Config.LogLevel
+			if diff := cmp.Diff(cfgLogLevel, tc.expected); diff != "" {
+				t.Errorf("%s: (-want +got):\n%v", tc.name, diff)
+			}
+		})
+	}
+}
+
 func valueAtPath(i interface{}, s string) interface{} {
 	v, err := awsutil.ValuesAtPath(i, s)
 	if err != nil || len(v) == 0 {