Repository: peak/s5cmd
Pull request #471 (2022-07-22 11:26:24)

Add benchmark script to compare two different builds of s5cmd


This python script allow us to compare two different build (from either version tag, PR number or commit tag) performance under various scenarios. These scenarios include:

1. Upload, Download, Remove many small sized file
1. Upload, Download, Remove large file
1. Upload, Download, Remove very large file

> To change the scenarios, you should edit it inside the `bench.py` for now. In the future, this could be read from a file. From each scenario, user should not forget to change the file size and file count keeping in mind the restrictions of their system.

To run use the following syntax:
```
usage: bench.py [-h] [-s OLD NEW] [-w WARMUP] [-r RUNS] [-o OUTPUT_FILE_NAME] -b BUCKET [-l LOCAL_PATH] [-p PREFIX] [-hf HYPERFINE_EXTRA_FLAGS] [-sf S5CMD_EXTRA_FLAGS]

Compare performance of two different builds of s5cmd.

optional arguments:
  -h, --help            show this help message and exit
  -s OLD NEW, --s5cmd OLD NEW
                        Reference to old and new s5cmd.It can be a decimal indicating PR number,any of the version tags like v2.0.0 or any commit tag. Additionally it can be 'latest_release' or
                        'master'. (default: ('latest_release', 'master'))
  -w WARMUP, --warmup WARMUP
                        Number of program executions before the actual benchmark: (default: 2)
  -r RUNS, --runs RUNS  Number of runs to perform for each command (default: 10)
  -o OUTPUT_FILE_NAME, --output_file_name OUTPUT_FILE_NAME
                        Name of the output file (default: summary.md)
  -b BUCKET, --bucket BUCKET
                        Name of the bucket in remote (default: None)
  -l LOCAL_PATH, --local-path LOCAL_PATH
                        specify a local path for temporary files to be loaded. (default: None)
  -p PREFIX, --prefix PREFIX
                        Key prefix to be used while uploading to a specified bucket (default: s5cmd-benchmarks-)
  -hf HYPERFINE_EXTRA_FLAGS, --hyperfine-extra-flags HYPERFINE_EXTRA_FLAGS
                        hyperfine global extra flags. Write in between quotation marks and start with a space to avoid bugs. (default: None)
  -sf S5CMD_EXTRA_FLAGS, --s5cmd-extra-flags S5CMD_EXTRA_FLAGS
                        s5cmd global extra flags. Write in between quotation marks and start with a space to avoid bugs. (default: None)
```

### Examples
```
./bench.py --bucket tempbucket --s5cmd v2.0.0 456 --warmup 2 --runs 10 
```
Above command will compare v2.0.0 to PR:456 with 2 warmup runs and 10 benchmark runs. 

```
./bench.py --bucket tempbucket --s5cmd v2.0.0 456 --warmup 2 --runs 10 -sf " --log error" -hf " --show-output"
```
When using `-hf` and `-sf` flags, use quotes like above and start with an empty space. If not started with an empty space, it might give an error. This is a known issue with `argparse` and [this](https://stackoverflow.com/questions/72129874/processing-arguments-for-subprocesses-using-argparse-expected-one-argument) discussion can be useful to understand the problem deeper.

boraberke: Results will be both printed out to console and saved to `bench_results.md` file, which looks like below:

### Benchmark summary:
Large File Size:  100M
Number of Small Files: 	100

|Scenario| Summary | 
|:---|:---|
|Upload Large|  'PR:468' ran    1.03 times faster than 'version:v2.0.0'|
|Upload Small|  'version:v2.0.0' ran    1.22 times faster than 'PR:468'|
|Download Large|  'PR:468' ran    1.04 times faster than 'version:v2.0.0'|
|Download Small|  'PR:468' ran    1.08 times faster than 'version:v2.0.0'|
|Remove Large|  'version:v2.0.0' ran    1.85 times faster than 'PR:468'|
|Remove Small|  'version:v2.0.0' ran    1.88 times faster than 'PR:468'|

### Detailed summary:
|Scenario| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|:---|---:|---:|---:|---:|
|Upload Large| `PR:468` | 1.377 | 1.377 | 1.377 | 1.00 |
|Upload Large| `version:v2.0.0` | 1.424 | 1.424 | 1.424 | 1.03 |
|Upload Small| `PR:468` | 4.852 | 4.852 | 4.852 | 1.22 |
|Upload Small| `version:v2.0.0` | 3.969 | 3.969 | 3.969 | 1.00 |
|Download Large| `PR:468` | 85.6 | 85.6 | 85.6 | 1.00 |
|Download Large| `version:v2.0.0` | 89.0 | 89.0 | 89.0 | 1.04 |
|Download Small| `PR:468` | 73.1 | 73.1 | 73.1 | 1.00 |
|Download Small| `version:v2.0.0` | 79.1 | 79.1 | 79.1 | 1.08 |
|Remove Large| `PR:468` | 452.5 | 452.5 | 452.5 | 1.85 |
|Remove Large| `version:v2.0.0` | 245.1 | 245.1 | 245.1 | 1.00 |
|Remove Small| `PR:468` | 276.4 | 276.4 | 276.4 | 1.88 |
|Remove Small| `version:v2.0.0` | 147.2 | 147.2 | 147.2 | 1.00 |








Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3b9e633..631c2f9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,9 +5,9 @@ jobs:
     strategy:
       matrix:
         go-version:
+          - 1.19.x
           - 1.18.x
           - 1.17.x
-          - 1.16.x
         os:
           - macos
           - ubuntu
@@ -27,9 +27,9 @@ jobs:
     strategy:
       matrix:
         go-version:
+          - 1.19.x
           - 1.18.x
           - 1.17.x
-          - 1.16.x
         os:
           - macos
           - ubuntu
@@ -49,7 +49,7 @@ jobs:
     strategy:
       matrix:
         go-version:
-          - 1.18.x
+          - 1.19.x
         os:
           - ubuntu
 
diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
index ce6bcd5..5a9ee9a 100644
--- a/.github/workflows/goreleaser.yml
+++ b/.github/workflows/goreleaser.yml
@@ -16,7 +16,7 @@ jobs:
         name: Set up Go
         uses: actions/setup-go@v2
         with:
-          go-version: 1.18
+          go-version: 1.19
       -
         name: Run GoReleaser
         uses: goreleaser/goreleaser-action@v2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ae646c..671c36b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,20 +8,23 @@
 #### 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). 
+- 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).
+- Added `bench.py` script under new `benchmark` folder to compare performances of two different builds of s5cmd. ([#471](https://github.com/peak/s5cmd/pull/471))
 
 #### Improvements
-- Disable AWS SDK logger if log level is not "trace"
+- Disable AWS SDK logger if log level is not `trace`. ([##460](https://github.com/peak/s5cmd/pull/460))
+- Allow adjacent slashes to be used as keys when uploading to remote. ([#459](https://github.com/peak/s5cmd/pull/459))
+- Debian packages are provided on [releases page](https://github.com/peak/s5cmd/releases) ([#380](https://github.com/peak/s5cmd/issues/380))
+- Upgraded minimum required Go version to 1.17.
+
 
 #### 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 some part of the destination path is removed by `cp` and `sync` subcommands ([#360](https://github.com/peak/s5cmd/issues/360))
 - Fixed a bug where proxy is not being used when `--no-verify-ssl` flag is used. ([#445](https://github.com/peak/s5cmd/issues/445))
 - Fixed `unknown url format` error when object key also includes `s3://` e.g. `s5cmd ls s3://foo/bar/s3://baz` ([#449](https://github.com/peak/s5cmd/issues/449))
-
-#### Improvements
-- Disable AWS SDK logger if log level is not "trace"
-- Allow adjacent slashes to be used as keys when uploading to remote. ([#459](https://github.com/peak/s5cmd/pull/459))
+- Fixed a bug where the local file created for the download operation was not deleted if the download fails in Windows. ([#348](https://github.com/peak/s5cmd/issues/348))
 
 ## v2.0.0 - 4 Jul 2022
 
@@ -42,7 +45,6 @@
 
 #### 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))
diff --git a/Dockerfile b/Dockerfile
index e5512ab..dc2c885 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.18-alpine as build
+FROM golang:1.19-alpine as build
 COPY . /s5cmd/
 RUN apk add --no-cache git make && \
     cd /s5cmd/ && \
diff --git a/Makefile b/Makefile
index 7c46af6..2eefbb9 100644
--- a/Makefile
+++ b/Makefile
@@ -11,14 +11,21 @@ LDFLAGS=-ldflags "-X=github.com/peak/s5cmd/version.Version=$(VERSION) -X=github.
 build:
 	@go build ${GCFLAGS} ${LDFLAGS} -mod=vendor .
 
-RACE_FLAG := -race
+TEST_TYPE:=test_with_race
 ifeq ($(OS),Windows_NT)
-	RACE_FLAG =
+	TEST_TYPE=test_without_race
 endif
 
 .PHONY: test
-test:
-	@go test -mod=vendor -count=1 ${RACE_FLAG} ./...
+test: $(TEST_TYPE)
+
+.PHONY: test_with_race
+test_with_race:
+	@S5CMD_BUILD_BINARY_WITHOUT_RACE_FLAG=0 go test -mod=vendor -count=1 -race ./...
+
+.PHONY: test_without_race
+test_without_race:
+	@S5CMD_BUILD_BINARY_WITHOUT_RACE_FLAG=1 go test -mod=vendor -count=1 ./...
 
 .PHONY: check
 check: vet staticcheck unparam check-fmt
diff --git a/README.md b/README.md
index f0f2a35..2a78a84 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,7 @@ For macOS, a [homebrew](https://brew.sh) tap is provided:
     brew install peak/tap/s5cmd
 
 ### Unofficial Releases (by Community)
+[![Packaging status](https://repology.org/badge/tiny-repos/s5cmd.svg)](https://repology.org/project/s5cmd/versions)
 > **Warning**  
 > These releases are maintained by the community. They might be out of date compared to the official releases.
 
@@ -90,7 +91,7 @@ ps.  Quoted from [s5cmd feedstock](https://github.com/conda-forge/s5cmd-feedstoc
 
 ### Build from source
 
-You can build `s5cmd` from source if you have [Go](https://golang.org/dl/) 1.16+
+You can build `s5cmd` from source if you have [Go](https://golang.org/dl/) 1.17+
 installed.
 
     go get github.com/peak/s5cmd
@@ -289,9 +290,6 @@ rm s3://bucket/2020/03/19/file2.gz
 
 # rename an S3 object
 mv s3://bucket/2020/03/18/file1.gz s3://bucket/2020/03/18/original/file.gz
-
-# list all buckets
-ls # inline comments are OK too
 ```
 
 #### Sync
@@ -581,6 +579,10 @@ of concurrent systems easy and make full utilization of multi-core processors.
 - *Parallelization.* `s5cmd` starts out with concurrent worker pools and parallelizes
 workloads as much as possible while trying to achieve maximum throughput.
 
+## performance regression tests
+
+[`bench.py`](benchmark/bench.py) script can be used to compare performance of two different s5cmd builds. Refer to this [readme](benchmark/README.md) file for further details.
+
 # Advanced Usage
 
 Some of the advanced usage patterns provided below are inspired by the following [article](https://medium.com/@joshua_robinson/s5cmd-hits-v1-0-and-intro-to-advanced-usage-37ad02f7e895) (thank you! [@joshuarobinson](https://github.com/joshuarobinson))
@@ -645,7 +647,6 @@ the latter sends single delete request per thousand objects, whereas using the f
 sends a separate delete request for each subcommand provided to `run.` Thus, there can be a
 significant runtime difference between those two approaches.
 
-
 # LICENSE
 
 MIT. See [LICENSE](https://github.com/peak/s5cmd/blob/master/LICENSE).
diff --git a/benchmark/README.md b/benchmark/README.md
new file mode 100644
index 0000000..2dc0f84
--- /dev/null
+++ b/benchmark/README.md
@@ -0,0 +1,110 @@
+
+## s5cmd performance regression tests
+
+`bench.py` allow us to compare two different build (from either version tag, PR number or commit tag) performance under various scenarios. These scenarios include:
+
+1. Upload, Download, Remove many small sized file
+1. Upload, Download, Remove large file
+1. Upload, Download, Remove very large file
+
+> To change the scenarios, you should edit it inside the `bench.py` for now. In the future, this could be read from a file. From each scenario, user should not forget to change the file size and file count keeping in mind the restrictions of their system.
+
+
+### required tools
+This script is dependent on the following tools. Make sure you install them to your system before running `bench.py`.
+- git
+- go 
+- hyperfine
+- truncate
+
+
+
+To run use the following syntax:
+```
+usage: bench.py [-h] [-s OLD NEW] [-w WARMUP] [-r RUNS] [-o OUTPUT_FILE_NAME] -b BUCKET [-l LOCAL_PATH] [-p PREFIX] [-hf HYPERFINE_EXTRA_FLAGS] [-sf S5CMD_EXTRA_FLAGS]
+
+Compare performance of two different builds of s5cmd.
+
+optional arguments:
+  -h, --help            show this help message and exit
+  -s OLD NEW, --s5cmd OLD NEW
+                        Reference to old and new s5cmd.It can be a decimal indicating PR number,any of the version tags like v2.0.0 or any commit tag. Additionally it can be 'latest_release' or
+                        'master'. (default: ('latest_release', 'master'))
+  -w WARMUP, --warmup WARMUP
+                        Number of program executions before the actual benchmark: (default: 2)
+  -r RUNS, --runs RUNS  Number of runs to perform for each command (default: 10)
+  -o OUTPUT_FILE_NAME, --output_file_name OUTPUT_FILE_NAME
+                        Name of the output file (default: summary.md)
+  -b BUCKET, --bucket BUCKET
+                        Name of the bucket in remote (default: None)
+  -l LOCAL_PATH, --local-path LOCAL_PATH
+                        specify a local path for temporary files to be loaded. (default: None)
+  -p PREFIX, --prefix PREFIX
+                        Key prefix to be used while uploading to a specified bucket (default: s5cmd-benchmarks-)
+  -hf HYPERFINE_EXTRA_FLAGS, --hyperfine-extra-flags HYPERFINE_EXTRA_FLAGS
+                        hyperfine global extra flags. Write in between quotation marks and start with a space to avoid bugs. (default: None)
+  -sf S5CMD_EXTRA_FLAGS, --s5cmd-extra-flags S5CMD_EXTRA_FLAGS
+                        s5cmd global extra flags. Write in between quotation marks and start with a space to avoid bugs. (default: None)
+```
+
+### Examples
+```
+./bench.py --bucket tempbucket  
+```
+Above command will compare `latest-release` to `master` with 2 warmup runs and 10 benchmark runs.  
+
+```
+./bench.py --bucket tempbucket --s5cmd v2.0.0 456 --warmup 2 --runs 10 
+```
+Above command will compare v2.0.0 to PR:456 with 2 warmup runs and 10 benchmark runs. 
+
+```
+./bench.py --bucket tempbucket --s5cmd v2.0.0 456 --warmup 2 --runs 10 -sf " --log error" -hf " --show-output"
+```
+When using `-hf` and `-sf` flags, use quotes like above and start with an empty space. If not started with an empty space, it might give an error. This is a known issue with `argparse` and [this](https://stackoverflow.com/questions/72129874/processing-arguments-for-subprocesses-using-argparse-expected-one-argument) discussion can be useful to understand the problem deeper.
+
+### Example Output
+```
+./bench.py --bucket tempbucket --s5cmd master 478 --warmup 2 --runs 15
+```
+
+### Benchmark summary: 
+|Scenarios | File Size | File Count |
+|:---|:---|:---|
+| small files | 1M | 10000 |
+| large file | 10G | 1 |
+| very large file | 300G | 1 |
+
+|Scenario| Summary |
+|:---|:---|
+| upload small files | 'PR:478' ran 1.01 ± 0.02 times faster than 'master' |
+| download small files | 'PR:478' ran 1.00 ± 0.01 times faster than 'master' |
+| remove small files | 'master' ran 1.05 ± 0.41 times faster than 'PR:478' |
+| upload large file | 'PR:478' ran 1.18 ± 0.23 times faster than 'master' |
+| download large file | 'master' ran 1.05 ± 0.08 times faster than 'PR:478' |
+| remove large file | 'master' ran 1.21 ± 0.39 times faster than 'PR:478' |
+| upload very large file | 'PR:478' ran 1.01 times faster than 'master' |
+| download very large file | 'master' ran 1.02 times faster than 'PR:478' |
+| remove very large file | 'PR:478' ran 1.13 times faster than 'master' |
+
+ ### Detailed summary: 
+ |Scenario| Command | Mean [s] | Min [s] | Max [s] | Relative |
+ |:---|:---|---:|---:|---:|---:|
+| upload small files | `PR:478` | 9.117 ± 0.155 | 8.848 | 9.337 | 1.00 |
+| upload small files | `master` | 9.252 ± 0.160 | 9.084 | 9.483 | 1.01 ± 0.02 |
+| download small files | `PR:478` | 79.992 ± 0.091 | 79.879 | 80.177 | 1.00 |
+| download small files | `master` | 79.993 ± 0.462 | 79.096 | 81.028 | 1.00 ± 0.01 |
+ | remove small files | `PR:478` | 2.603 ± 0.435 | 2.308 | 3.245 | 1.05 ± 0.41 |
+ | remove small files | `master` | 2.470 ± 0.878 | 2.012 | 3.787 | 1.00 |
+ | upload large file | `PR:478` | 10.093 ± 1.491 | 9.043 | 14.222 | 1.00 |
+ | upload large file | `master` | 11.876 ± 1.486 | 10.730 | 15.787 | 1.18 ± 0.23 |
+ | download large file | `PR:478` | 27.689 ± 1.378 | 25.979 | 30.803 | 1.05 ± 0.08 |
+ | download large file | `master` | 26.452 ± 1.667 | 24.891 | 29.375 | 1.00 |
+ | remove large file | `PR:478` | 0.157 ± 0.029 | 0.122 | 0.210 | 1.21 ± 0.39 |
+ | remove large file | `master` | 0.130 ± 0.034 | 0.090 | 0.220 | 1.00 |
+ | upload very large file | `PR:478` | 270.462 | 270.462 | 270.462 | 1.00 |
+ | upload very large file | `master` | 272.473 | 272.473 | 272.473 | 1.01 |
+ | download very large file | `PR:478` | 2538.727 | 2538.727 | 2538.727 | 1.02 |
+ | download very large file | `master` | 2501.010 | 2501.010 | 2501.010 | 1.00 |
+ | remove very large file | `PR:478` | 1.011 | 1.011 | 1.011 | 1.00 |
+ | remove very large file | `master` | 1.145 | 1.145 | 1.145 | 1.13 |
diff --git a/vendor/github.com/karrick/godirwalk/go.sum b/benchmark/__init__.py
similarity index 100%
rename from vendor/github.com/karrick/godirwalk/go.sum
rename to benchmark/__init__.py
diff --git a/benchmark/bench.py b/benchmark/bench.py
new file mode 100755
index 0000000..fda459d
--- /dev/null
+++ b/benchmark/bench.py
@@ -0,0 +1,597 @@
+#!/usr/bin/env python
+import argparse
+import datetime
+import os
+import re
+import shutil
+import subprocess
+from tempfile import mkdtemp
+
+
+def main(argv=None):
+    parser = argparse.ArgumentParser(
+        description="Compare performance of two different builds of s5cmd.",
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+    )
+
+    parser.add_argument(
+        "-s",
+        "--s5cmd",
+        nargs=2,
+        metavar=("OLD", "NEW"),
+        default=("latest_release", "master"),
+        help="Reference to old and new s5cmd."
+        "It can be a decimal indicating PR number,any of the version tags like v2.0.0 or any commit tag. Additionally "
+        "it can be 'latest_release' or 'master'.",
+    )
+    parser.add_argument(
+        "-w",
+        "--warmup",
+        default="2",
+        help="Number of program executions before the actual benchmark:",
+    )
+    parser.add_argument(
+        "-r", "--runs", default="10", help="Number of runs to perform for each command"
+    )
+    parser.add_argument(
+        "-o", "--output_file_name", default="summary.md", help="Name of the output file"
+    )
+    parser.add_argument(
+        "-b", "--bucket", required=True, help="Name of the bucket in remote"
+    )
+    parser.add_argument(
+        "-l",
+        "--local-path",
+        help="specify a local path for temporary files to be loaded.",
+    )
+    parser.add_argument(
+        "-p",
+        "--prefix",
+        default="s5cmd-benchmarks-",
+        help="Key prefix to be used while uploading to a specified bucket",
+    )
+    parser.add_argument(
+        "-hf",
+        "--hyperfine-extra-flags",
+        help="hyperfine global extra flags. "
+        "Write in between quotation marks "
+        "and start with a space to avoid bugs.",
+    )
+    parser.add_argument(
+        "-sf",
+        "--s5cmd-extra-flags",
+        help="s5cmd global extra flags. "
+        "Write in between quotation marks "
+        "and start with a space to avoid bugs.",
+    )
+
+    ok, err_msg = check_dependencies()
+    if not ok:
+        print(err_msg)
+        raise SystemExit(1)
+
+    args = parser.parse_args(argv)
+
+    cwd = os.getcwd()
+
+    local_dir, dst_path = create_bench_dir(args.bucket, args.prefix, args.local_path)
+    print("The created local&remote files will be deleted at the end of tests.")
+    print(
+        f"Hyperfine will execute s5cmd uploads {args.warmup} times to warmup, and {args.runs} times for measurements."
+    )
+    old_s5cmd, new_s5cmd = build_s5cmd_exec(args.s5cmd[0], args.s5cmd[1], local_dir)
+
+    scenarios = [
+        Scenario(
+            name="small files",
+            cwd=cwd,
+            dst_path=dst_path,
+            local_dir=local_dir,
+            file_size="1M",
+            file_count="10000",
+            s5cmd_args=args.s5cmd_extra_flags,
+            hyperfine_args={
+                "runs": args.runs,
+                "warmup": args.warmup,
+                "extra_flags": args.hyperfine_extra_flags,
+            },
+        ),
+        Scenario(
+            name="large file",
+            cwd=cwd,
+            dst_path=dst_path,
+            local_dir=local_dir,
+            file_size="10G",
+            file_count="1",
+            s5cmd_args=args.s5cmd_extra_flags,
+            hyperfine_args={
+                "runs": args.runs,
+                "warmup": args.warmup,
+                "extra_flags": args.hyperfine_extra_flags,
+            },
+        ),
+        Scenario(
+            name="very large file",
+            cwd=cwd,
+            dst_path=dst_path,
+            local_dir=local_dir,
+            file_size="300G",
+            file_count="1",
+            s5cmd_args=args.s5cmd_extra_flags,
+            hyperfine_args={
+                "runs": "1",
+                "warmup": "0",
+                "extra_flags": args.hyperfine_extra_flags,
+            },
+        ),
+    ]
+    scenario_details = []
+    for s in scenarios:
+        scenario_details.append(f"| {s.name} | {s.file_size} | {s.file_count} |")
+
+    for idx, scenario in enumerate(scenarios):
+        if idx == 0:
+            scenario.setup(
+                args.output_file_name,
+                initialize_bench=True,
+                all_scenario_details=scenario_details,
+            )
+        else:
+            scenario.setup(args.output_file_name)
+        scenario.run(old_s5cmd, new_s5cmd)
+        scenario.teardown()
+
+    # append detailed_summary to output_file_name
+    with open(os.path.join(cwd, "detailed_summary.md"), "r+") as f:
+        detailed_summary = join_with_spaces(f.readlines())
+    with open(os.path.join(cwd, args.output_file_name), "a") as f:
+        f.write(detailed_summary)
+
+    cleanup(local_dir, cwd)
+
+
+class S5cmd:
+    def __init__(self, folder_path, clone_path, name, tag):
+        self.clone_path = clone_path
+        self.name = name
+        self.tag = tag
+        self.descriptive_name = ""
+        self.path = os.path.join(folder_path, self.name)
+        self.build()
+
+    def build(self):
+
+        if self.tag == "master":
+            self.tag = self._get_master_commit_tag().strip()
+            self._checkout_commit()
+            self.descriptive_name = "master"
+
+        elif self.tag == "latest_release":
+            releases = self._get_all_releases()
+            self.tag = releases.strip().split("\n")[-1]
+            self.tag = self.tag.removeprefix("'refs/tags/").removesuffix("'")
+            self._checkout_version()
+            self.descriptive_name = "latest_release:" + self.tag
+
+        elif is_pr(self.tag):
+            self._checkout_pr()
+            self.descriptive_name = "PR:" + self.tag
+
+        elif is_version_tag(self.tag):
+            self._checkout_version()
+            self.descriptive_name = "version:" + self.tag
+
+        else:
+            self._checkout_commit()
+            self.descriptive_name = "commit" + self.tag
+
+        os.chdir(self.clone_path)
+        run_cmd(["go", "build", "-o", self.path])
+
+    def _checkout_commit(self):
+        cmd = ["git", "-C", self.clone_path, "checkout", self.tag, "-q"]
+
+        return run_cmd(cmd)
+
+    def _get_master_commit_tag(self):
+        cmd = ["git", "-C", self.clone_path, "rev-parse", "--short", "origin/master"]
+
+        return run_cmd(cmd, verbose=False)
+
+    def _get_all_releases(self):
+        cmd = [
+            "git",
+            "-C",
+            self.clone_path,
+            "for-each-ref",
+            "--sort=creatordate",
+            "--format",
+            "'%(refname)'",
+            "refs/tags",
+        ]
+        return run_cmd(cmd, verbose=False)
+
+    def _checkout_pr(self):
+        fetch_cmd = [
+            "git",
+            "-C",
+            self.clone_path,
+            "fetch",
+            "origin",
+            f"pull/{self.tag}/head",
+            "-q",
+        ]
+        checkout_cmd = ["git", "-C", self.clone_path, "checkout", "FETCH_HEAD", "-q"]
+        stdout = [run_cmd(fetch_cmd), run_cmd(checkout_cmd)]
+
+        return stdout
+
+    def _checkout_version(self):
+        cmd = ["git", "-C", self.clone_path, "checkout", f"tags/{self.tag}", "-q"]
+
+        return run_cmd(cmd)
+
+
+class Scenario:
+    def __init__(
+        self,
+        name,
+        cwd,
+        file_size,
+        file_count,
+        s5cmd_args,
+        hyperfine_args,
+        local_dir,
+        dst_path,
+    ):
+        self.all_scenario_details = None
+        self.initialize_bench = False
+        self.run_name = None
+        self.name = name
+        self.cwd = cwd
+        self.file_size = file_size
+        self.file_count = file_count
+        self.s5cmd_args = ""
+        if s5cmd_args is not None:
+            self.s5cmd_args = s5cmd_args
+        self.hyperfine_args = hyperfine_args
+        self.local_dir = local_dir
+        self.folder_dir = ""
+        self.output_file_name = ""
+        self.run_types = ["upload", "download", "remove"]
+
+        self.dst_path = dst_path
+
+    def setup(
+        self, output_file_name, initialize_bench=False, all_scenario_details=None
+    ):
+
+        self.output_file_name = output_file_name
+        self.initialize_bench = initialize_bench
+        self.all_scenario_details = all_scenario_details
+
+        if self.file_count:
+            self.file_count = int(self.file_count)
+            self.create_files()
+
+        else:
+            self.folder_dir = f"{self.local_dir}/"
+
+    def create_files(self):
+        # create subdirectory under local_dir named with a scenario name
+        # create file_count files with each file_size size
+        self.folder_dir = f'{self.local_dir}/{self.name.replace(" ", "-")}'
+
+        os.mkdir(self.folder_dir)
+        os.chdir(self.folder_dir)
+
+        if self.file_count <= 0:
+            raise ValueError(f"{self.file_count} cannot be negative.")
+        else:
+            for i in range(self.file_count):
+                run_cmd(
+                    ["truncate", "-s", f"{int(to_bytes(self.file_size))}", f"tmp{i}"]
+                )
+
+    def teardown(self):
+        # remove local files at teardown
+        shutil.rmtree(self.folder_dir)
+
+    def run(self, old_s5cmd, new_s5cmd):
+
+        s5cmd_cmds = self.get_s5cmd_commands(old_s5cmd, new_s5cmd)
+        for run in self.run_types:
+            os.chdir(self.folder_dir)
+
+            cmd = [
+                "hyperfine",
+                "--export-markdown",
+                os.path.join(self.local_dir, "temp.md"),
+                "-u",
+                "second",
+                "--runs",
+                self.hyperfine_args["runs"],
+                "--warmup",
+                self.hyperfine_args["warmup"],
+                "-n",
+                old_s5cmd.descriptive_name,
+                "-n",
+                new_s5cmd.descriptive_name,
+            ]
+
+            self.run_name = f"{run} {self.name}"
+            print(f"Running: {self.run_name}:\n")
+
+            if run == "upload":
+                cmd.append(s5cmd_cmds["old_upload"])
+                cmd.append(s5cmd_cmds["new_upload"])
+
+            elif run == "download":
+                cmd.append(s5cmd_cmds["old_download"])
+                cmd.append(s5cmd_cmds["new_download"])
+
+            elif run == "remove":
+                cmd.append(s5cmd_cmds["old_remove"])
+                cmd.append(s5cmd_cmds["new_remove"])
+
+                # if there is only one run without warmups, then do not prepare.
+                if (
+                    int(self.hyperfine_args["runs"]) > 1
+                    or int(self.hyperfine_args["warmup"]) >= 1
+                ):
+                    cmd.append("--prepare")
+                    cmd.append(s5cmd_cmds["prepare_old_for_remove"])
+                    cmd.append("--prepare")
+                    cmd.append(s5cmd_cmds["prepare_new_for_remove"])
+
+            if self.hyperfine_args["extra_flags"]:
+                cmd.append(self.hyperfine_args["extra_flags"].strip())
+
+            output = run_cmd(cmd)
+            summary = self.parse_output(output)
+            if self.initialize_bench:
+                init_bench_results(
+                    self.cwd, self.output_file_name, self.all_scenario_details
+                )
+                self.initialize_bench = False
+
+            with open(os.path.join(self.cwd, self.output_file_name), "a") as f:
+                f.write(summary)
+
+            detailed_summary = ""
+            temp_dir = os.path.join(self.local_dir, "temp.md")
+
+            with open(temp_dir, "r+") as f:
+                lines = f.readlines()
+                # get hyperfine markdown output and add a new column in the front as scenario name
+                detailed_summary = (
+                    f"| {self.run_name} {lines[-1]}" f"| {self.run_name} {lines[-2]}"
+                )
+
+            with open(os.path.join(self.cwd, "detailed_summary.md"), "a") as f:
+                f.write(detailed_summary)
+
+    def get_s5cmd_commands(self, old_s5cmd, new_s5cmd):
+        result = {}
+
+        old_upload = join_with_spaces(
+            [self.s5cmd_args, "cp", f'"*"', f"{self.dst_path}/old/"]
+        )
+        old_upload = f"{old_s5cmd.path} {old_upload}"
+        result["old_upload"] = old_upload
+
+        prepare_old_for_remove = f"{old_upload} | sleep 10"
+        result["prepare_old_for_remove"] = prepare_old_for_remove
+
+        new_upload = join_with_spaces(
+            [self.s5cmd_args, "cp", '"*"', f"{self.dst_path}/new/"]
+        )
+        new_upload = f"{new_s5cmd.path} {new_upload}"
+        result["new_upload"] = new_upload
+
+        prepare_new_for_remove = f"{new_upload} | sleep 10"
+        result["prepare_new_for_remove"] = prepare_new_for_remove
+
+        old_download = join_with_spaces(
+            [self.s5cmd_args, "cp", f'"{self.dst_path}/old/*"', "old/"]
+        )
+        old_download = f"{old_s5cmd.path} {old_download}"
+        result["old_download"] = old_download
+
+        new_download = join_with_spaces(
+            [self.s5cmd_args, "cp", f'"{self.dst_path}/new/*"', "new/"]
+        )
+        new_download = f"{new_s5cmd.path} {new_download}"
+        result["new_download"] = new_download
+
+        new_remove = join_with_spaces(
+            [self.s5cmd_args, "rm", f'"{self.dst_path}/new/*"']
+        )
+
+        new_remove = f"{new_s5cmd.path} {new_remove}"
+        result["new_remove"] = new_remove
+
+        old_remove = join_with_spaces(
+            [self.s5cmd_args, "rm", f'"{self.dst_path}/old/*"']
+        )
+        old_remove = f"{old_s5cmd.path} {old_remove}"
+        result["old_remove"] = old_remove
+
+        return result
+
+    def parse_output(self, output):
+        lines = output.split("\n")
+        summary = ""
+        for i, line in enumerate(lines):
+            # get the next two lines after summary and format it as markdown table.
+            if "Summary" in line:
+                line1 = lines[i + 1].replace("\n", "").strip()
+                line2 = lines[i + 2].replace("\n", "").strip()
+                summary = f"| {self.run_name} | {line1} {line2} |\n"
+        return summary
+
+
+def init_bench_results(cwd, output_file_name, scenario_details):
+    header = (
+        "### Benchmark summary: "
+        "\n| Scenario | File Size | File Count |"
+        "\n|:---|:---|:---|"
+        "\n"
+    )
+
+    summary = (
+        f"{header}"
+        f"{join_with_newlines(scenario_details)}"
+        "\n\n|Scenario| Summary |"
+        "\n|:---|:---|"
+        "\n"
+    )
+
+    with open(os.path.join(cwd, output_file_name), "w") as file:
+        file.write(summary)
+
+    detailed_summary = (
+        "\n### Detailed summary: "
+        "\n|Scenario| Command | Mean [s] | Min [s] | Max [s] | Relative |"
+        "\n|:---|:---|---:|---:|---:|---:|"
+        "\n"
+    )
+
+    with open(os.path.join(cwd, "detailed_summary.md"), "w") as file:
+        file.write(detailed_summary)
+
+
+def join_with_spaces(lst):
+    return " ".join(lst)
+
+
+def join_with_newlines(lst):
+    return "\n".join(lst)
+
+
+def to_bytes(size):
+    if size.isdigit():
+        return int(size)
+    unit = size[-1]
+    if unit == "K":
+        return int(size[:-1]) * 1024
+    elif unit == "M":
+        return int(size[:-1]) * (1024**2)
+    elif unit == "G":
+        return int(size[:-1]) * (1024**3)
+    elif unit == "T":
+        return int(size[:-1]) * (1024**4)
+    elif unit == "P":
+        return int(size[:-1]) * (1024**5)
+    else:
+        raise ValueError("Given size is not correct.")
+
+
+def build_s5cmd_exec(old, new, local_dir):
+    run_cmd(
+        [
+            "git",
+            "-C",
+            local_dir,
+            "clone",
+            "https://github.com/peak/s5cmd.git",
+            "-q",
+        ]
+    )
+
+    clone_dir = os.path.join(local_dir, "s5cmd")
+
+    old = S5cmd(local_dir, clone_dir, "old", old)
+    new = S5cmd(local_dir, clone_dir, "new", new)
+
+    return old, new
+
+
+def is_pr(tag):
+    if tag is None or tag == "" or tag == "0":
+        return False
+
+    return re.match(r"^\d+$", tag) is not None
+
+
+def is_version_tag(tag):
+    if tag is None or tag == "":
+        return False
+
+    return re.match(r"^v(\d+.){2}(\d)(-[a-z]*.?\d?)?$", tag) is not None
+
+
+def create_bench_dir(bucket, prefix, local_path):
+    """
+    Create a benchmark directory with a unique name to specified local_path.
+    If no path is specified, create temporary directory using mkdtemp.
+    In both of those cases, use the same unique name to create remote path name.
+
+    :param bucket: remote bucket to be used to create remote_path
+    :param prefix: prefix to be used after specified bucket for remote_path
+    :param local_path: specify a path for temporary files to be created in your local. If empty,
+    use default temporary folder path of your device.
+    :returns:
+        - local_dir - created local_dir path as local_path/bench-unique_suffix
+        - remote_path - created remote_path as s3://bucket/prefix/unique_suffix
+
+    """
+    if local_path:
+        basename = "bench"
+        suffix = datetime.datetime.now().strftime("%y%m%d_%H%M%S")
+        tmp_dir = "_".join([basename, suffix])
+        if os.path.isdir(local_path):
+            os.chdir(local_path)
+        else:
+            raise NotADirectoryError(local_path)
+        os.chdir(local_path)
+        os.mkdir(tmp_dir)
+        os.chdir(tmp_dir)
+
+        local_dir = os.getcwd()
+        remote_path = f"s3://{bucket}/{prefix}/{suffix}"
+    else:
+        local_dir = mkdtemp(prefix=prefix)
+        idx = local_dir.rfind(prefix[-1]) + 1
+        remote_path = f"s3://{bucket}/{prefix}/{local_dir[idx:]}"
+
+    print(f"All the local temporary files will be created at {local_dir}")
+    print(f"All the remote files will be uploaded to {remote_path}")
+
+    return local_dir, remote_path
+
+
+def run_cmd(cmd, verbose=True):
+    process = subprocess.run(cmd, capture_output=True, text=True)
+    if verbose:
+        print(process.stdout, end="")
+        print(process.stderr, end="")
+    return process.stdout
+
+
+def cleanup(tmp_dir, temp_result_file_dir):
+    temp_summary = os.path.join(temp_result_file_dir, "detailed_summary.md")
+    if os.path.isfile(temp_summary):
+        os.remove(temp_summary)
+
+    shutil.rmtree(tmp_dir)
+
+
+def check_dependencies():
+    """
+    Checks external binary dependencies and returns bool, str tuple.
+    Returns True, None if all dependencies are installed.
+    Returns False, Descriptive Message if any of the dependencies is not installed.
+    """
+    dependencies = ["git", "go", "hyperfine", "truncate"]
+    for d in dependencies:
+        if shutil.which(d) is None:
+            return (
+                False,
+                f"{d} is not found. Please install it to your system and run the script again.",
+            )
+    return True, None
+
+
+if __name__ == "__main__":
+    main()
diff --git a/command/app.go b/command/app.go
index 23f4588..6f324b8 100644
--- a/command/app.go
+++ b/command/app.go
@@ -173,16 +173,17 @@ var app = &cli.App{
 // NewStorageOpts creates storage.Options object from the given context.
 func NewStorageOpts(c *cli.Context) storage.Options {
 	return storage.Options{
-		DryRun:           c.Bool("dry-run"),
-		Endpoint:         c.String("endpoint-url"),
-		MaxRetries:       c.Int("retry-count"),
-		NoSignRequest:    c.Bool("no-sign-request"),
-		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")),
+		DryRun:                 c.Bool("dry-run"),
+		Endpoint:               c.String("endpoint-url"),
+		MaxRetries:             c.Int("retry-count"),
+		NoSignRequest:          c.Bool("no-sign-request"),
+		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")),
+		NoSuchUploadRetryCount: c.Int("no-such-upload-retry-count"),
 	}
 }
 
diff --git a/command/cp.go b/command/cp.go
index cba3c42..cdf1937 100644
--- a/command/cp.go
+++ b/command/cp.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"mime"
 	"net/http"
 	"os"
@@ -177,6 +176,12 @@ func NewSharedFlags() []cli.Flag {
 			Name:  "content-encoding",
 			Usage: "set content encoding for target: defines content encoding header for object, e.g. --content-encoding gzip",
 		},
+		&cli.IntFlag{
+			Name:        "no-such-upload-retry-count",
+			Usage:       "number of times that a request will be retried on NoSuchUpload error; you should not use this unless you really know what you're doing",
+			DefaultText: "0",
+			Hidden:      true,
+		},
 	}
 }
 
@@ -251,8 +256,8 @@ type Copy struct {
 	acl                   string
 	forceGlacierTransfer  bool
 	ignoreGlacierWarnings bool
-	exclude               []string
 	raw                   bool
+	exclude               []string
 	cacheControl          string
 	expires               string
 	contentType           string
@@ -512,13 +517,18 @@ func (c Copy) doDownload(ctx context.Context, srcurl *url.URL, dsturl *url.URL)
 	if err != nil {
 		return err
 	}
-	defer file.Close()
 
 	size, err := srcClient.Get(ctx, srcurl, file, c.concurrency, c.partSize)
 	if err != nil {
-		_ = dstClient.Delete(ctx, dsturl)
+		// file must be closed before deletion
+		file.Close()
+		dErr := dstClient.Delete(ctx, dsturl)
+		if dErr != nil {
+			printDebug(c.op, dErr, srcurl, dsturl)
+		}
 		return err
 	}
+	defer file.Close()
 
 	if c.deleteSource {
 		_ = srcClient.Delete(ctx, srcurl)
@@ -908,7 +918,7 @@ func guessContentType(file *os.File) string {
 		defer file.Seek(0, io.SeekStart)
 
 		const bufsize = 512
-		buf, err := ioutil.ReadAll(io.LimitReader(file, bufsize))
+		buf, err := io.ReadAll(io.LimitReader(file, bufsize))
 		if err != nil {
 			return ""
 		}
diff --git a/command/sync_strategy.go b/command/sync_strategy.go
index 4f84db9..cd6db2d 100644
--- a/command/sync_strategy.go
+++ b/command/sync_strategy.go
@@ -31,10 +31,11 @@ func (s *SizeOnlyStrategy) ShouldSync(srcObj, dstObj *storage.Object) error {
 
 // SizeAndModificationStrategy determines to sync based on objects' both sizes and modification times.
 // It treats source object as the source-of-truth;
-//     time: src > dst        size: src != dst    should sync: yes
-//     time: src > dst        size: src == dst    should sync: yes
-//     time: src <= dst       size: src != dst    should sync: yes
-//     time: src <= dst       size: src == dst    should sync: no
+//
+//	time: src > dst        size: src != dst    should sync: yes
+//	time: src > dst        size: src == dst    should sync: yes
+//	time: src <= dst       size: src != dst    should sync: yes
+//	time: src <= dst       size: src == dst    should sync: no
 type SizeAndModificationStrategy struct{}
 
 func (sm *SizeAndModificationStrategy) ShouldSync(srcObj, dstObj *storage.Object) error {
diff --git a/go.mod b/go.mod
index fbcc647..b06f77c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,21 +1,40 @@
 module github.com/peak/s5cmd
 
-go 1.13
+go 1.17
 
 require (
 	github.com/aws/aws-sdk-go v1.40.25
-	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/google/go-cmp v0.4.0
 	github.com/hashicorp/go-multierror v1.0.0
 	github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
-	github.com/igungor/gofakes3 v0.0.10
+	github.com/igungor/gofakes3 v0.0.11
 	github.com/karrick/godirwalk v1.15.3
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
-	github.com/kr/pretty v0.2.0 // indirect
 	github.com/posener/complete v1.2.3
 	github.com/stretchr/testify v1.4.0
 	github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae
 	github.com/urfave/cli/v2 v2.2.0
-	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gotest.tools/v3 v3.0.2
 )
+
+require (
+	github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/jmespath/go-jmespath v0.4.0 // indirect
+	github.com/kr/pretty v0.2.0 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/russross/blackfriday/v2 v2.0.1 // indirect
+	github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
+	github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63 // indirect
+	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
+	github.com/stretchr/objx v0.1.0 // indirect
+	go.etcd.io/bbolt v1.3.6 // indirect
+	golang.org/x/sys v0.0.0-20220405210540-1e041c57c461 // indirect
+	golang.org/x/tools v0.0.0-20190624222133-a101b041ded4 // indirect
+	golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
+	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
+	gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
+	gopkg.in/yaml.v2 v2.2.8 // indirect
+)
diff --git a/go.sum b/go.sum
index cd59ff8..1066238 100644
--- a/go.sum
+++ b/go.sum
@@ -16,8 +16,8 @@ github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uP
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 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.10 h1:/sGbnHRjqYyK3iRQ8jOFZQhsxTMOYLCk7+CVN3FPYF4=
-github.com/igungor/gofakes3 v0.0.10/go.mod h1:id+2y7yvTVoN1HW6VOtnfSqFt4EAfH5piVL4p9JsmQc=
+github.com/igungor/gofakes3 v0.0.11 h1:KqVnxdhlrc5LQYUBifexI/E0X7V5CUonTmvfujkmKao=
+github.com/igungor/gofakes3 v0.0.11/go.mod h1:id+2y7yvTVoN1HW6VOtnfSqFt4EAfH5piVL4p9JsmQc=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 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=
diff --git a/storage/fs.go b/storage/fs.go
index 628244a..3626414 100644
--- a/storage/fs.go
+++ b/storage/fs.go
@@ -86,7 +86,7 @@ func (f *Filesystem) expandGlob(ctx context.Context, src *url.URL, followSymlink
 			filename := filename
 
 			fileurl, _ := url.New(filename)
-			fileurl.SetRelative(src.Absolute())
+			fileurl.SetRelative(src)
 
 			obj, _ := f.Stat(ctx, fileurl)
 
@@ -120,7 +120,7 @@ func walkDir(ctx context.Context, fs *Filesystem, src *url.URL, followSymlinks b
 				return err
 			}
 
-			fileurl.SetRelative(src.Absolute())
+			fileurl.SetRelative(src)
 
 			//skip if symlink is pointing to a file and --no-follow-symlink
 			if !ShouldProcessUrl(fileurl, followSymlinks) {
diff --git a/storage/s3.go b/storage/s3.go
index 2c78070..82f10a4 100644
--- a/storage/s3.go
+++ b/storage/s3.go
@@ -2,11 +2,14 @@ package storage
 
 import (
 	"context"
+	"crypto/rand"
 	"crypto/tls"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
+	"math"
+	"math/big"
 	"net/http"
 	urlpkg "net/url"
 	"os"
@@ -43,6 +46,9 @@ const (
 
 	// Google Cloud Storage endpoint
 	gcsEndpoint = "storage.googleapis.com"
+
+	// the key of the object metadata which is used to handle retry decision on NoSuchUpload error
+	metadataKeyRetryID = "s5cmd-upload-retry-id"
 )
 
 // Re-used AWS sessions dramatically improve performance.
@@ -53,13 +59,14 @@ var globalSessionCache = &SessionCache{
 // S3 is a storage type which interacts with S3API, DownloaderAPI and
 // UploaderAPI.
 type S3 struct {
-	api              s3iface.S3API
-	downloader       s3manageriface.DownloaderAPI
-	uploader         s3manageriface.UploaderAPI
-	endpointURL      urlpkg.URL
-	dryRun           bool
-	useListObjectsV1 bool
-	requestPayer     string
+	api                    s3iface.S3API
+	downloader             s3manageriface.DownloaderAPI
+	uploader               s3manageriface.UploaderAPI
+	endpointURL            urlpkg.URL
+	dryRun                 bool
+	useListObjectsV1       bool
+	noSuchUploadRetryCount int
+	requestPayer           string
 }
 
 func (s *S3) RequestPayer() *string {
@@ -99,13 +106,14 @@ func newS3Storage(ctx context.Context, opts Options) (*S3, error) {
 	}
 
 	return &S3{
-		api:              s3.New(awsSession),
-		downloader:       s3manager.NewDownloader(awsSession),
-		uploader:         s3manager.NewUploader(awsSession),
-		endpointURL:      endpointURL,
-		dryRun:           opts.DryRun,
-		useListObjectsV1: opts.UseListObjectsV1,
-		requestPayer:     opts.RequestPayer,
+		api:                    s3.New(awsSession),
+		downloader:             s3manager.NewDownloader(awsSession),
+		uploader:               s3manager.NewUploader(awsSession),
+		endpointURL:            endpointURL,
+		dryRun:                 opts.DryRun,
+		useListObjectsV1:       opts.UseListObjectsV1,
+		requestPayer:           opts.RequestPayer,
+		noSuchUploadRetryCount: opts.NoSuchUploadRetryCount,
 	}, nil
 }
 
@@ -125,12 +133,21 @@ func (s *S3) Stat(ctx context.Context, url *url.URL) (*Object, error) {
 
 	etag := aws.StringValue(output.ETag)
 	mod := aws.TimeValue(output.LastModified)
-	return &Object{
+
+	obj := &Object{
 		URL:     url,
 		Etag:    strings.Trim(etag, `"`),
 		ModTime: &mod,
 		Size:    aws.Int64Value(output.ContentLength),
-	}, nil
+	}
+
+	if s.noSuchUploadRetryCount > 0 {
+		if retryID, ok := output.Metadata[metadataKeyRetryID]; ok {
+			obj.retryID = *retryID
+		}
+	}
+
+	return obj, nil
 }
 
 // List is a non-blocking S3 list operation which paginates and filters S3
@@ -514,6 +531,7 @@ func (s *S3) Put(
 		Key:          aws.String(to.Path),
 		Body:         reader,
 		ContentType:  aws.String(contentType),
+		Metadata:     make(map[string]*string),
 		RequestPayer: s.RequestPayer(),
 	}
 
@@ -554,11 +572,53 @@ func (s *S3) Put(
 		input.ContentEncoding = aws.String(contentEncoding)
 	}
 
-	_, err := s.uploader.UploadWithContext(ctx, input, func(u *s3manager.Uploader) {
+	// add retry ID to the object metadata
+	if s.noSuchUploadRetryCount > 0 {
+		input.Metadata[metadataKeyRetryID] = generateRetryID()
+	}
+
+	uploaderOptsFn := func(u *s3manager.Uploader) {
 		u.PartSize = partSize
 		u.Concurrency = concurrency
-	})
+	}
+	_, err := s.uploader.UploadWithContext(ctx, input, uploaderOptsFn)
+
+	if errHasCode(err, s3.ErrCodeNoSuchUpload) && s.noSuchUploadRetryCount > 0 {
+		return s.retryOnNoSuchUpload(ctx, to, input, err, uploaderOptsFn)
+	}
+
+	return err
+}
+
+func (s *S3) retryOnNoSuchUpload(ctx aws.Context, to *url.URL, input *s3manager.UploadInput,
+	err error, uploaderOpts ...func(*s3manager.Uploader)) error {
+
+	var expectedRetryID string
+	if ID, ok := input.Metadata[metadataKeyRetryID]; ok {
+		expectedRetryID = *ID
+	}
+
+	attempts := 0
+	for ; errHasCode(err, s3.ErrCodeNoSuchUpload) && attempts < s.noSuchUploadRetryCount; attempts++ {
+		// check if object exists and has the retry ID we provided, if it does
+		// then it means that one of previous uploads was succesfull despite the received error.
+		obj, sErr := s.Stat(ctx, to)
+		if sErr == nil && obj.retryID == expectedRetryID {
+			err = nil
+			break
+		}
 
+		msg := log.DebugMessage{Err: fmt.Sprintf("Retrying to upload %v upon error: %q", to, err.Error())}
+		log.Debug(msg)
+
+		_, err = s.uploader.UploadWithContext(ctx, input, uploaderOpts...)
+	}
+
+	if errHasCode(err, s3.ErrCodeNoSuchUpload) && s.noSuchUploadRetryCount > 0 {
+		err = awserr.New(s3.ErrCodeNoSuchUpload, fmt.Sprintf(
+			"RetryOnNoSuchUpload: %v attempts to retry resulted in %v", attempts,
+			s3.ErrCodeNoSuchUpload), err)
+	}
 	return err
 }
 
@@ -808,6 +868,10 @@ func (sc *SessionCache) newSession(ctx context.Context, opts Options) (*session.
 		WithS3ForcePathStyle(!isVirtualHostStyle).
 		WithS3UseAccelerate(useAccelerate).
 		WithHTTPClient(httpClient).
+		// TODO WithLowerCaseHeaderMaps and WithDisableRestProtocolURICleaning options
+		// are going to be unnecessary and unsupported in AWS-SDK version 2.
+		// They should be removed during migration.
+		WithLowerCaseHeaderMaps(true).
 		// Disable URI cleaning to allow adjacent slashes to be used in S3 object keys.
 		WithDisableRestProtocolURICleaning(true)
 
@@ -985,3 +1049,9 @@ func errHasCode(err error, code string) bool {
 func IsCancelationError(err error) bool {
 	return errHasCode(err, request.CanceledErrorCode)
 }
+
+// generate a retry ID for this upload attempt
+func generateRetryID() *string {
+	num, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
+	return aws.String(num.String())
+}
diff --git a/storage/storage.go b/storage/storage.go
index 83a7391..52f7f56 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -56,18 +56,19 @@ func NewLocalClient(opts Options) *Filesystem {
 
 func NewRemoteClient(ctx context.Context, url *url.URL, opts Options) (*S3, error) {
 	newOpts := Options{
-		MaxRetries:       opts.MaxRetries,
-		Endpoint:         opts.Endpoint,
-		NoVerifySSL:      opts.NoVerifySSL,
-		DryRun:           opts.DryRun,
-		NoSignRequest:    opts.NoSignRequest,
-		UseListObjectsV1: opts.UseListObjectsV1,
-		RequestPayer:     opts.RequestPayer,
-		Profile:          opts.Profile,
-		CredentialFile:   opts.CredentialFile,
-		LogLevel:         opts.LogLevel,
-		bucket:           url.Bucket,
-		region:           opts.region,
+		MaxRetries:             opts.MaxRetries,
+		Endpoint:               opts.Endpoint,
+		NoVerifySSL:            opts.NoVerifySSL,
+		DryRun:                 opts.DryRun,
+		NoSignRequest:          opts.NoSignRequest,
+		UseListObjectsV1:       opts.UseListObjectsV1,
+		RequestPayer:           opts.RequestPayer,
+		Profile:                opts.Profile,
+		CredentialFile:         opts.CredentialFile,
+		LogLevel:               opts.LogLevel,
+		bucket:                 url.Bucket,
+		region:                 opts.region,
+		NoSuchUploadRetryCount: opts.NoSuchUploadRetryCount,
 	}
 	return newS3Storage(ctx, newOpts)
 }
@@ -81,18 +82,19 @@ func NewClient(ctx context.Context, url *url.URL, opts Options) (Storage, error)
 
 // Options stores configuration for storage.
 type Options struct {
-	MaxRetries       int
-	Endpoint         string
-	NoVerifySSL      bool
-	DryRun           bool
-	NoSignRequest    bool
-	UseListObjectsV1 bool
-	LogLevel         log.LogLevel
-	RequestPayer     string
-	Profile          string
-	CredentialFile   string
-	bucket           string
-	region           string
+	MaxRetries             int
+	NoSuchUploadRetryCount int
+	Endpoint               string
+	NoVerifySSL            bool
+	DryRun                 bool
+	NoSignRequest          bool
+	UseListObjectsV1       bool
+	LogLevel               log.LogLevel
+	RequestPayer           string
+	Profile                string
+	CredentialFile         string
+	bucket                 string
+	region                 string
 }
 
 func (o *Options) SetRegion(region string) {
@@ -108,6 +110,7 @@ type Object struct {
 	Size         int64        `json:"size,omitempty"`
 	StorageClass StorageClass `json:"storage_class,omitempty"`
 	Err          error        `json:"error,omitempty"`
+	retryID      string
 }
 
 // String returns the string representation of Object.
@@ -199,17 +202,6 @@ func (s StorageClass) IsGlacier() bool {
 	return s == "GLACIER"
 }
 
-// notImplemented is a structure which is used on the unsupported operations.
-type notImplemented struct {
-	apiType string
-	method  string
-}
-
-// Error returns the string representation of Error for notImplemented.
-func (e notImplemented) Error() string {
-	return fmt.Sprintf("%q is not supported on %q storage", e.method, e.apiType)
-}
-
 type Metadata map[string]string
 
 // NewMetadata will return an empty metadata object.
diff --git a/storage/url/url.go b/storage/url/url.go
index daa48cf..22ab37d 100644
--- a/storage/url/url.go
+++ b/storage/url/url.go
@@ -226,23 +226,24 @@ func (u *URL) remoteURL() string {
 // prefix is the part that comes before the wildcard string.
 //
 // Example:
-//		key: a/b/test?/c/*.tsv
-//		prefix: a/b/test
-//		filter: ?/c/*
-//		regex: ^a/b/test./c/.*?\\.tsv$
-//		delimiter: ""
+//
+//	key: a/b/test?/c/*.tsv
+//	prefix: a/b/test
+//	filter: ?/c/*
+//	regex: ^a/b/test./c/.*?\\.tsv$
+//	delimiter: ""
 //
 // It prepares delimiter, prefix and regex for regular strings.
 // These are used in S3 listing operations.
 // See: https://docs.aws.amazon.com/AmazonS3/latest/dev/ListingKeysHierarchy.html
 //
 // Example:
-//		key: a/b/c
-//		prefix: a/b/c
-//		filter: ""
-//		regex: ^a/b/c.*$
-//		delimiter: "/"
 //
+//	key: a/b/c
+//	prefix: a/b/c
+//	filter: ""
+//	regex: ^a/b/c.*$
+//	delimiter: "/"
 func (u *URL) setPrefixAndFilter() error {
 	if u.raw {
 		return nil
@@ -290,9 +291,37 @@ func (u *URL) Clone() *URL {
 }
 
 // SetRelative explicitly sets the relative path of u against given base value.
-func (u *URL) SetRelative(base string) {
-	dir := filepath.Dir(base)
-	u.relativePath, _ = filepath.Rel(dir, u.Absolute())
+// If the base path contains `globCharacters` then, the relative path is
+// determined with respect to the parent directory of the so called wildcarded
+// object.
+func (u *URL) SetRelative(base *URL) {
+	basePath := base.Absolute()
+	if base.IsWildcard() {
+		// When the basePath includes a wildcard character (globCharacters)
+		// replace basePath with its substring up to the
+		// index of the first instance of a wildcard character.
+		//
+		// If we don't handle this, the filepath.Dir()
+		// will assume those wildcards as a part of the name.
+		// Consequently the filepath.Rel() will determine
+		// the relative path incorrectly since the wildcarded
+		// path string won't match with the actual name of the
+		// object.
+		// e.g. base.Absolute(): "/a/*/n"
+		//      u.Absolute()   : "/a/b/n"
+		//
+		// if we don't trim substring from globCharacters on
+		// filepath.Dir() will give: "/a/*"
+		// consequently the
+		// filepath.Rel() will give: "../b/c" rather than "b/c"
+		// since "b" and "*" are not the same.
+		loc := strings.IndexAny(basePath, globCharacters)
+		if loc >= 0 {
+			basePath = basePath[:loc]
+		}
+	}
+	baseDir := filepath.Dir(basePath)
+	u.relativePath, _ = filepath.Rel(baseDir, u.Absolute())
 }
 
 // Match reports whether if given key matches with the object.
@@ -333,10 +362,10 @@ func (u *URL) IsWildcard() bool {
 // wildcard part (filter)
 //
 // Example:
-//		key: a/b/test2/c/example_file.tsv
-//		prefix: a/b/
-//		output: test2/c/example_file.tsv
 //
+//	key: a/b/test2/c/example_file.tsv
+//	prefix: a/b/
+//	output: test2/c/example_file.tsv
 func parseBatch(prefix string, key string) string {
 	index := strings.LastIndex(prefix, s3Separator)
 	if index < 0 || !strings.HasPrefix(key, prefix) {
@@ -352,10 +381,10 @@ func parseBatch(prefix string, key string) string {
 // path.
 //
 // Example:
-//		key: a/b/c/d
-//		prefix: a/b
-//		output: c/
 //
+//	key: a/b/c/d
+//	prefix: a/b
+//	output: c/
 func parseNonBatch(prefix string, key string) string {
 	if key == prefix || !strings.HasPrefix(key, prefix) {
 		return key
diff --git a/vendor/github.com/hashicorp/errwrap/go.mod b/vendor/github.com/hashicorp/errwrap/go.mod
deleted file mode 100644
index c9b8402..0000000
--- a/vendor/github.com/hashicorp/errwrap/go.mod
+++ /dev/null
@@ -1,1 +0,0 @@
-module github.com/hashicorp/errwrap
diff --git a/vendor/github.com/hashicorp/go-multierror/go.mod b/vendor/github.com/hashicorp/go-multierror/go.mod
deleted file mode 100644
index 2534331..0000000
--- a/vendor/github.com/hashicorp/go-multierror/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module github.com/hashicorp/go-multierror
-
-require github.com/hashicorp/errwrap v1.0.0
diff --git a/vendor/github.com/hashicorp/go-multierror/go.sum b/vendor/github.com/hashicorp/go-multierror/go.sum
deleted file mode 100644
index 85b1f8f..0000000
--- a/vendor/github.com/hashicorp/go-multierror/go.sum
+++ /dev/null
@@ -1,4 +0,0 @@
-github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce h1:prjrVgOk2Yg6w+PflHoszQNLTUh4kaByUcEWM/9uin4=
-github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
diff --git a/vendor/github.com/igungor/gofakes3/go.mod b/vendor/github.com/igungor/gofakes3/go.mod
deleted file mode 100644
index 6bb1d87..0000000
--- a/vendor/github.com/igungor/gofakes3/go.mod
+++ /dev/null
@@ -1,23 +0,0 @@
-module github.com/igungor/gofakes3
-
-require (
-	github.com/aws/aws-sdk-go v1.17.4
-	github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46
-	github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63
-	github.com/spf13/afero v1.2.1
-	go.etcd.io/bbolt v1.3.6
-	golang.org/x/tools v0.0.0-20190308174544-00c44ba9c14f
-	gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
-)
-
-require (
-	github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
-	github.com/stretchr/testify v1.3.0 // indirect
-	golang.org/x/net v0.0.0-20190310074541-c10a0554eabf // indirect
-	golang.org/x/sys v0.0.0-20220405210540-1e041c57c461 // indirect
-	golang.org/x/text v0.3.0 // indirect
-	gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 // indirect
-	gopkg.in/yaml.v2 v2.2.2 // indirect
-)
-
-go 1.13
diff --git a/vendor/github.com/igungor/gofakes3/go.sum b/vendor/github.com/igungor/gofakes3/go.sum
deleted file mode 100644
index 63ec5e1..0000000
--- a/vendor/github.com/igungor/gofakes3/go.sum
+++ /dev/null
@@ -1,36 +0,0 @@
-github.com/aws/aws-sdk-go v1.17.4 h1:L2KFocQhg48kIzEAV98SnSz3nmIZ3UDFP+vU647KO3c=
-github.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
-github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
-github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63 h1:J6qvD6rbmOil46orKqJaRPG+zTpoGlBTUdyv8ki63L0=
-github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63/go.mod h1:n+VKSARF5y/tS9XFSP7vWDfS+GUC5vs/YT7M5XDTUEM=
-github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
-github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
-go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190310074541-c10a0554eabf h1:J7RqX9u0J9ZB37CGaFc2VC+QZZT6E6jnDbrboEFVo0U=
-golang.org/x/net v0.0.0-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20220405210540-1e041c57c461 h1:kHVeDEnfKn3T238CvrUcz6KeEsFHVaKh4kMTt6Wsysg=
-golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/tools v0.0.0-20190308174544-00c44ba9c14f h1:SUQ6L9W8e5xt2GFO9s+i18JGITAfem+a0AQuFU8Ls74=
-golang.org/x/tools v0.0.0-20190308174544-00c44ba9c14f/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
-gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/igungor/gofakes3/gofakes3.go b/vendor/github.com/igungor/gofakes3/gofakes3.go
index eedd925..be35b6a 100644
--- a/vendor/github.com/igungor/gofakes3/gofakes3.go
+++ b/vendor/github.com/igungor/gofakes3/gofakes3.go
@@ -23,6 +23,8 @@ import (
 //
 // Logic is delegated to other components, like Backend or uploader.
 type GoFakeS3 struct {
+	requestID uint64
+
 	storage   Backend
 	versioned VersionedBackend
 
@@ -33,7 +35,6 @@ type GoFakeS3 struct {
 	failOnUnimplementedPage bool
 	hostBucket              bool
 	uploader                *uploader
-	requestID               uint64
 	log                     Logger
 }
 
diff --git a/vendor/github.com/jmespath/go-jmespath/go.mod b/vendor/github.com/jmespath/go-jmespath/go.mod
deleted file mode 100644
index 4d448e8..0000000
--- a/vendor/github.com/jmespath/go-jmespath/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module github.com/jmespath/go-jmespath
-
-go 1.14
-
-require github.com/jmespath/go-jmespath/internal/testify v1.5.1
diff --git a/vendor/github.com/jmespath/go-jmespath/go.sum b/vendor/github.com/jmespath/go-jmespath/go.sum
deleted file mode 100644
index d2db411..0000000
--- a/vendor/github.com/jmespath/go-jmespath/go.sum
+++ /dev/null
@@ -1,11 +0,0 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/karrick/godirwalk/go.mod b/vendor/github.com/karrick/godirwalk/go.mod
deleted file mode 100644
index faf23fd..0000000
--- a/vendor/github.com/karrick/godirwalk/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module github.com/karrick/godirwalk
-
-go 1.13
diff --git a/vendor/github.com/posener/complete/go.mod b/vendor/github.com/posener/complete/go.mod
deleted file mode 100644
index 6d82a98..0000000
--- a/vendor/github.com/posener/complete/go.mod
+++ /dev/null
@@ -1,8 +0,0 @@
-module github.com/posener/complete
-
-require (
-	github.com/hashicorp/go-multierror v1.0.0
-	github.com/stretchr/testify v1.4.0
-)
-
-go 1.13
diff --git a/vendor/github.com/posener/complete/go.sum b/vendor/github.com/posener/complete/go.sum
deleted file mode 100644
index accaa27..0000000
--- a/vendor/github.com/posener/complete/go.sum
+++ /dev/null
@@ -1,15 +0,0 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/russross/blackfriday/v2/go.mod b/vendor/github.com/russross/blackfriday/v2/go.mod
deleted file mode 100644
index 620b74e..0000000
--- a/vendor/github.com/russross/blackfriday/v2/go.mod
+++ /dev/null
@@ -1,1 +0,0 @@
-module github.com/russross/blackfriday/v2
diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod b/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod
deleted file mode 100644
index 1e25534..0000000
--- a/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod
+++ /dev/null
@@ -1,1 +0,0 @@
-module github.com/shurcooL/sanitized_anchor_name
diff --git a/vendor/github.com/urfave/cli/v2/go.mod b/vendor/github.com/urfave/cli/v2/go.mod
deleted file mode 100644
index c38d41c..0000000
--- a/vendor/github.com/urfave/cli/v2/go.mod
+++ /dev/null
@@ -1,9 +0,0 @@
-module github.com/urfave/cli/v2
-
-go 1.11
-
-require (
-	github.com/BurntSushi/toml v0.3.1
-	github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d
-	gopkg.in/yaml.v2 v2.2.2
-)
diff --git a/vendor/github.com/urfave/cli/v2/go.sum b/vendor/github.com/urfave/cli/v2/go.sum
deleted file mode 100644
index ef121ff..0000000
--- a/vendor/github.com/urfave/cli/v2/go.sum
+++ /dev/null
@@ -1,14 +0,0 @@
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/go.etcd.io/bbolt/go.mod b/vendor/go.etcd.io/bbolt/go.mod
deleted file mode 100644
index 96355a6..0000000
--- a/vendor/go.etcd.io/bbolt/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module go.etcd.io/bbolt
-
-go 1.12
-
-require golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d
diff --git a/vendor/go.etcd.io/bbolt/go.sum b/vendor/go.etcd.io/bbolt/go.sum
deleted file mode 100644
index c13f8f4..0000000
--- a/vendor/go.etcd.io/bbolt/go.sum
+++ /dev/null
@@ -1,2 +0,0 @@
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/vendor/golang.org/x/xerrors/go.mod b/vendor/golang.org/x/xerrors/go.mod
deleted file mode 100644
index 870d4f6..0000000
--- a/vendor/golang.org/x/xerrors/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module golang.org/x/xerrors
-
-go 1.11
diff --git a/vendor/gopkg.in/yaml.v2/go.mod b/vendor/gopkg.in/yaml.v2/go.mod
deleted file mode 100644
index 1934e87..0000000
--- a/vendor/gopkg.in/yaml.v2/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module "gopkg.in/yaml.v2"
-
-require (
-	"gopkg.in/check.v1" v0.0.0-20161208181325-20d25e280405
-)
diff --git a/vendor/modules.txt b/vendor/modules.txt
index af8400e..375fbc7 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,4 +1,5 @@
 # github.com/aws/aws-sdk-go v1.40.25
+## explicit; go 1.11
 github.com/aws/aws-sdk-go/aws
 github.com/aws/aws-sdk-go/aws/arn
 github.com/aws/aws-sdk-go/aws/awserr
@@ -53,10 +54,13 @@ github.com/aws/aws-sdk-go/service/sso/ssoiface
 github.com/aws/aws-sdk-go/service/sts
 github.com/aws/aws-sdk-go/service/sts/stsiface
 # github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d
+## explicit; go 1.12
 github.com/cpuguy83/go-md2man/v2/md2man
 # github.com/davecgh/go-spew v1.1.1
+## explicit
 github.com/davecgh/go-spew/spew
 # github.com/google/go-cmp v0.4.0
+## explicit; go 1.8
 github.com/google/go-cmp/cmp
 github.com/google/go-cmp/cmp/cmpopts
 github.com/google/go-cmp/cmp/internal/diff
@@ -64,64 +68,93 @@ github.com/google/go-cmp/cmp/internal/flags
 github.com/google/go-cmp/cmp/internal/function
 github.com/google/go-cmp/cmp/internal/value
 # github.com/hashicorp/errwrap v1.0.0
+## explicit
 github.com/hashicorp/errwrap
 # github.com/hashicorp/go-multierror v1.0.0
+## explicit
 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.10
+# github.com/igungor/gofakes3 v0.0.11
+## explicit; go 1.13
 github.com/igungor/gofakes3
 github.com/igungor/gofakes3/backend/s3bolt
 github.com/igungor/gofakes3/backend/s3mem
 github.com/igungor/gofakes3/internal/goskipiter
 github.com/igungor/gofakes3/internal/s3io
 # github.com/jmespath/go-jmespath v0.4.0
+## explicit; go 1.14
 github.com/jmespath/go-jmespath
 # github.com/karrick/godirwalk v1.15.3
+## explicit; go 1.13
 github.com/karrick/godirwalk
 # github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
+## explicit
 github.com/kballard/go-shellquote
+# github.com/kr/pretty v0.2.0
+## explicit; go 1.12
 # github.com/pkg/errors v0.9.1
+## explicit
 github.com/pkg/errors
 # github.com/pmezard/go-difflib v1.0.0
+## explicit
 github.com/pmezard/go-difflib/difflib
 # github.com/posener/complete v1.2.3
+## explicit; go 1.13
 github.com/posener/complete
 github.com/posener/complete/cmd
 github.com/posener/complete/cmd/install
 # github.com/russross/blackfriday/v2 v2.0.1
+## explicit
 github.com/russross/blackfriday/v2
 # github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46
+## explicit
 github.com/ryszard/goskiplist/skiplist
 # github.com/shabbyrobe/gocovmerge v0.0.0-20180507124511-f6ea450bfb63
+## explicit
 github.com/shabbyrobe/gocovmerge
 # github.com/shurcooL/sanitized_anchor_name v1.0.0
+## explicit
 github.com/shurcooL/sanitized_anchor_name
 # github.com/stretchr/objx v0.1.0
+## explicit
 github.com/stretchr/objx
 # github.com/stretchr/testify v1.4.0
+## explicit
 github.com/stretchr/testify/assert
 github.com/stretchr/testify/mock
 # github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae
+## explicit
 github.com/termie/go-shutil
 # github.com/urfave/cli/v2 v2.2.0
+## explicit; go 1.11
 github.com/urfave/cli/v2
 # go.etcd.io/bbolt v1.3.6
+## explicit; go 1.12
 go.etcd.io/bbolt
 # golang.org/x/sys v0.0.0-20220405210540-1e041c57c461
+## explicit; go 1.17
 golang.org/x/sys/internal/unsafeheader
 golang.org/x/sys/unix
 # golang.org/x/tools v0.0.0-20190624222133-a101b041ded4
+## explicit; go 1.11
 golang.org/x/tools/cover
 # golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
+## explicit; go 1.11
 golang.org/x/xerrors
 golang.org/x/xerrors/internal
+# gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
+## explicit
 # gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
+## explicit
 gopkg.in/mgo.v2/bson
 gopkg.in/mgo.v2/internal/json
 # gopkg.in/yaml.v2 v2.2.8
+## explicit
 gopkg.in/yaml.v2
 # gotest.tools/v3 v3.0.2
+## explicit; go 1.11
 gotest.tools/v3/assert
 gotest.tools/v3/assert/cmp
 gotest.tools/v3/fs


Test patch:
diff --git a/benchmark/test_bench.py b/benchmark/test_bench.py
new file mode 100644
index 0000000..8030779
--- /dev/null
+++ b/benchmark/test_bench.py
@@ -0,0 +1,33 @@
+import unittest
+from collections import namedtuple
+from benchmark.bench import is_pr, is_version_tag
+
+
+class TestBench(unittest.TestCase):
+    Test = namedtuple("Test", ["input", "expected"])
+
+    def test_is_pr(self):
+        test_cases = [
+            self.Test("478", True),
+            self.Test("10", True),
+            self.Test("0", False),
+            self.Test(None, False),
+            self.Test("", False),
+        ]
+        for test in test_cases:
+            self.assertEqual(is_pr(test.input), test.expected)
+
+    def test_is_version_tag(self):
+        test_cases = [
+            self.Test("v1.0.0", True),
+            self.Test("v2.0.0", True),
+            self.Test("v1.4.0", True),
+            self.Test("v2.0.0-beta", True),
+            self.Test("v2.0.0-beta.2", True),
+            self.Test("v1.4.0-rc.1", True),
+            self.Test("v.1.4.0", False),
+            self.Test("", False),
+            self.Test(None, False),
+        ]
+        for test in test_cases:
+            self.assertEqual(is_version_tag(test.input), test.expected)
diff --git a/command/cp_test.go b/command/cp_test.go
index 5d1e0f8..9127d8d 100644
--- a/command/cp_test.go
+++ b/command/cp_test.go
@@ -2,7 +2,6 @@ package command
 
 import (
 	"io"
-	"io/ioutil"
 	"os"
 	"testing"
 
@@ -46,7 +45,7 @@ func TestGuessContentType(t *testing.T) {
 	for _, tc := range testcases {
 		tc := tc
 
-		f, err := ioutil.TempFile("", tc.filename)
+		f, err := os.CreateTemp("", tc.filename)
 		if err != nil {
 			t.Error(err)
 		}
diff --git a/e2e/cp_test.go b/e2e/cp_test.go
index 484c427..ab93231 100644
--- a/e2e/cp_test.go
+++ b/e2e/cp_test.go
@@ -1053,7 +1053,6 @@ func TestFlattenCopyDirToS3(t *testing.T) {
 
 // cp dir/* s3://bucket/
 func TestCopyMultipleFilesToS3Bucket(t *testing.T) {
-
 	t.Parallel()
 
 	bucket := s3BucketFromTestName(t)
@@ -1110,9 +1109,160 @@ func TestCopyMultipleFilesToS3Bucket(t *testing.T) {
 	}
 }
 
+// cp parent/*/name.txt s3://bucket/newfolder
+func TestCopyMultipleFilesWithWildcardedDirectoryToS3Bucket(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	folderLayout := []fs.PathOp{
+		fs.WithDir("parent", fs.WithDir(
+			"child1",
+			fs.WithFile("name.txt", "A file in parent/child1/"),
+		),
+			fs.WithDir(
+				"child2",
+				fs.WithFile("name.txt", "A file in parent/child2/"),
+			),
+		),
+	}
+
+	workdir := fs.NewDir(t, "somedir", folderLayout...)
+	dstpath := fmt.Sprintf("s3://%v/newfolder/", bucket)
+	srcpath := workdir.Path()
+	srcpath = filepath.ToSlash(srcpath)
+	defer workdir.Remove()
+
+	cmd := s5cmd("cp", srcpath+"/parent/*/name.txt", dstpath)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+	rs := result.Stdout()
+	assertLines(t, rs, map[int]compareFunc{
+		0: equals(`cp %v/parent/child1/name.txt %vchild1/name.txt`, srcpath, dstpath),
+		1: equals(`cp %v/parent/child2/name.txt %vchild2/name.txt`, srcpath, dstpath),
+	}, sortInput(true))
+
+	// assert local filesystem
+	expected := fs.Expected(t, folderLayout...)
+	assert.Assert(t, fs.Equal(workdir.Path(), expected))
+	expectedS3Content := map[string]string{
+		"newfolder/child1/name.txt": "A file in parent/child1/",
+		"newfolder/child2/name.txt": "A file in parent/child2/",
+	}
+
+	// assert s3
+	for filename, content := range expectedS3Content {
+		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+	}
+}
+
+// cp parent/c*/name.txt s3://bucket/newfolder
+func TestCopyMultipleFilesEndWildcardedToS3Bucket(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	folderLayout := []fs.PathOp{
+		fs.WithDir("parent", fs.WithDir(
+			"child1",
+			fs.WithFile("name.txt", "A file in parent/child1/"),
+		),
+			fs.WithDir(
+				"child2",
+				fs.WithFile("name.txt", "A file in parent/child2/"),
+			),
+		),
+	}
+
+	workdir := fs.NewDir(t, "somedir", folderLayout...)
+	dstpath := fmt.Sprintf("s3://%v/newfolder/", bucket)
+	srcpath := workdir.Path()
+	srcpath = filepath.ToSlash(srcpath)
+	defer workdir.Remove()
+
+	cmd := s5cmd("cp", srcpath+"/parent/c*/name.txt", dstpath)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+	rs := result.Stdout()
+	assertLines(t, rs, map[int]compareFunc{
+		0: equals(`cp %v/parent/child1/name.txt %vchild1/name.txt`, srcpath, dstpath),
+		1: equals(`cp %v/parent/child2/name.txt %vchild2/name.txt`, srcpath, dstpath),
+	}, sortInput(true))
+
+	// assert local filesystem
+	expected := fs.Expected(t, folderLayout...)
+	assert.Assert(t, fs.Equal(workdir.Path(), expected))
+	expectedS3Content := map[string]string{
+		"newfolder/child1/name.txt": "A file in parent/child1/",
+		"newfolder/child2/name.txt": "A file in parent/child2/",
+	}
+
+	// assert s3
+	for filename, content := range expectedS3Content {
+		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+	}
+}
+
+// cp parent/c*1/name.txt s3://bucket/newfolder
+func TestCopyMultipleFilesMiddleWildcardedDirectoryToS3Bucket(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	folderLayout := []fs.PathOp{
+		fs.WithDir("parent", fs.WithDir(
+			"child1",
+			fs.WithFile("name.txt", "A file in parent/child1/"),
+		)),
+	}
+
+	workdir := fs.NewDir(t, "somedir", folderLayout...)
+	dstpath := fmt.Sprintf("s3://%v/newfolder/", bucket)
+	srcpath := workdir.Path()
+	srcpath = filepath.ToSlash(srcpath)
+	defer workdir.Remove()
+
+	cmd := s5cmd("cp", srcpath+"/parent/c*1/name.txt", dstpath)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+	rs := result.Stdout()
+	assertLines(t, rs, map[int]compareFunc{
+		0: equals(`cp %v/parent/child1/name.txt %vchild1/name.txt`, srcpath, dstpath),
+	}, sortInput(true))
+
+	// assert local filesystem
+	expected := fs.Expected(t, folderLayout...)
+	assert.Assert(t, fs.Equal(workdir.Path(), expected))
+	expectedS3Content := map[string]string{
+		"newfolder/child1/name.txt": "A file in parent/child1/",
+	}
+
+	// assert s3
+	for filename, content := range expectedS3Content {
+		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+	}
+}
+
 // cp --flatten dir/* s3://bucket/
 func TestFlattenCopyMultipleFilesToS3Bucket(t *testing.T) {
-
 	t.Parallel()
 
 	bucket := s3BucketFromTestName(t)
@@ -3996,3 +4146,66 @@ func TestCopyExpectExitCode1OnUnreachableHost(t *testing.T) {
 
 	result.Assert(t, icmd.Expected{ExitCode: 1})
 }
+
+func TestCopySingleFileToS3WithNoSuchUploadRetryCount(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	const (
+		filename = "example.txt"
+		content  = "Some example text"
+	)
+
+	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", "--no-such-upload-retry-count", "5", srcpath, dstpath)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	assertLines(t, result.Stdout(), map[int]compareFunc{
+		0: suffix(`cp %v %v%v`, srcpath, dstpath, filename),
+	})
+
+	// assert local filesystem
+	expected := fs.Expected(t, fs.WithFile(filename, content))
+	assert.Assert(t, fs.Equal(workdir.Path(), expected))
+
+	// assert S3
+	assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+}
+
+// Before downloading a file from s3 a local target file is created. If download
+// fails the created file should be deleted.
+func TestDeleteFileWhenDownloadFailed(t *testing.T) {
+	t.Parallel()
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	bucket := s3BucketFromTestName(t)
+	filename := "testfile1.txt"
+	createBucket(t, s3client, bucket)
+
+	// It is going try downloading a nonexistent file from the s3 so it will fail.
+	// In this case we don't expect to have a local file with the name `filename`.
+	cmd := s5cmd("cp", "s3://"+bucket+"/"+filename, filename)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Expected{ExitCode: 1})
+
+	// assert local filesystem does not have any (such) file
+	expected := fs.Expected(t)
+	assert.Assert(t, fs.Equal(cmd.Dir, expected))
+}
diff --git a/e2e/main_test.go b/e2e/main_test.go
index a110bcd..e5e48d6 100644
--- a/e2e/main_test.go
+++ b/e2e/main_test.go
@@ -1,11 +1,14 @@
 package e2e
 
 import (
+	"flag"
 	"os"
 	"testing"
 )
 
 func TestMain(m *testing.M) {
+	flag.Parse()
+
 	cleanup := goBuildS5cmd()
 	code := m.Run()
 	cleanup()
diff --git a/e2e/sync_test.go b/e2e/sync_test.go
index 8626a53..50a6d65 100644
--- a/e2e/sync_test.go
+++ b/e2e/sync_test.go
@@ -246,6 +246,59 @@ func TestSyncLocalFolderToS3EmptyBucket(t *testing.T) {
 	}
 }
 
+// cp parent/*/name.txt s3://bucket/newfolder
+func TestSyncMultipleFilesWithWildcardedDirectoryToS3Bucket(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	folderLayout := []fs.PathOp{
+		fs.WithDir("parent", fs.WithDir(
+			"child1",
+			fs.WithFile("name.txt", "A file in parent/child1/"),
+		),
+			fs.WithDir(
+				"child2",
+				fs.WithFile("name.txt", "A file in parent/child2/"),
+			),
+		),
+	}
+
+	workdir := fs.NewDir(t, "somedir", folderLayout...)
+	dstpath := fmt.Sprintf("s3://%v/newfolder/", bucket)
+	srcpath := workdir.Path()
+	srcpath = filepath.ToSlash(srcpath)
+	defer workdir.Remove()
+
+	cmd := s5cmd("sync", srcpath+"/parent/*/name.txt", dstpath)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+	rs := result.Stdout()
+	assertLines(t, rs, map[int]compareFunc{
+		0: equals(`cp %v/parent/child1/name.txt %vchild1/name.txt`, srcpath, dstpath),
+		1: equals(`cp %v/parent/child2/name.txt %vchild2/name.txt`, srcpath, dstpath),
+	}, sortInput(true))
+
+	// assert local filesystem
+	expected := fs.Expected(t, folderLayout...)
+	assert.Assert(t, fs.Equal(workdir.Path(), expected))
+	expectedS3Content := map[string]string{
+		"newfolder/child1/name.txt": "A file in parent/child1/",
+		"newfolder/child2/name.txt": "A file in parent/child2/",
+	}
+
+	// assert s3
+	for filename, content := range expectedS3Content {
+		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+	}
+}
+
 // sync  s3://bucket/* folder/
 func TestSyncS3BucketToEmptyFolder(t *testing.T) {
 	t.Parallel()
diff --git a/e2e/util_test.go b/e2e/util_test.go
index a84dedc..b4877bf 100644
--- a/e2e/util_test.go
+++ b/e2e/util_test.go
@@ -9,7 +9,6 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"math/rand"
 	"os"
 	"os/exec"
@@ -38,6 +37,12 @@ import (
 	"gotest.tools/v3/icmd"
 )
 
+const (
+	// Don't use "race" flag in the build arguments.
+	testDisableRaceFlagKey = "S5CMD_BUILD_BINARY_WITHOUT_RACE_FLAG"
+	testDisableRaceFlagVal = "1"
+)
+
 var (
 	defaultAccessKeyID     = "s5cmd-test-access-key-id"
 	defaultSecretAccessKey = "s5cmd-test-secret-access-key"
@@ -189,7 +194,7 @@ func s5cmd(workdir, endpoint string) func(args ...string) icmd.Cmd {
 }
 
 func goBuildS5cmd() func() {
-	tmpdir, err := ioutil.TempDir("", "")
+	tmpdir, err := os.MkdirTemp("", "")
 	if err != nil {
 		panic(err)
 	}
@@ -210,14 +215,23 @@ func goBuildS5cmd() func() {
 	workdir = filepath.Dir(workdir)
 
 	var args []string
-	if runtime.GOOS == "windows" {
+
+	if os.Getenv(testDisableRaceFlagKey) == testDisableRaceFlagVal {
 		/*
-		 disable '-race' flag because CI fails with below error.
+		 1. disable '-race' flag because CI fails with below error.
 
 		 ==2688==ERROR: ThreadSanitizer failed to allocate 0x000001000000
 		 (16777216) bytes at 0x040140000000 (error code: 1455)
 
 		 Ref: https://github.com/golang/go/issues/22553
+
+		 2.  Some distributions default to buildmode pie which is incompatible with race flag.
+
+		 Ref: Alpine Linux: "All userland binaries are compiled as Position
+		 Independent Executables (PIE)..." https://www.alpinelinux.org/about/
+
+		 Ref 2: "-buildmode=pie not supported when -race is enabled"
+		 https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/work/init.go;l=245;drc=eaf21256545ae04a35fa070763faa6eb2098591d
 		*/
 		args = []string{"build", "-mod=vendor", "-o", s5cmdPath}
 	} else {
diff --git a/storage/s3_test.go b/storage/s3_test.go
index a31f2f0..75b823f 100644
--- a/storage/s3_test.go
+++ b/storage/s3_test.go
@@ -6,7 +6,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"math/rand"
 	"net/http"
 	"net/http/httptest"
@@ -14,6 +13,7 @@ import (
 	"os"
 	"reflect"
 	"strings"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -565,6 +565,83 @@ func TestS3Retry(t *testing.T) {
 	}
 }
 
+func TestS3RetryOnNoSuchUpload(t *testing.T) {
+	log.Init("debug", false)
+
+	noSuchUploadError := awserr.New(s3.ErrCodeNoSuchUpload, "The specified upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed. status code: 404, request id: PJXXXXX, host id: HOSTIDXX", nil)
+	testcases := []struct {
+		name       string
+		err        error
+		retryCount int32
+	}{
+		{
+			name:       "Don't retry",
+			err:        noSuchUploadError,
+			retryCount: 0,
+		}, {
+			name:       "Retry 5 times on NoSuchUpload error",
+			err:        noSuchUploadError,
+			retryCount: 5,
+		}, {
+			name:       "No error",
+			err:        nil,
+			retryCount: 0,
+		},
+	}
+
+	url, err := url.New("s3://bucket/key")
+	if err != nil {
+		t.Errorf("unexpected error: %v", err)
+	}
+
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			mockApi := s3.New(unit.Session)
+			mockS3 := &S3{
+				api: mockApi,
+				uploader: &s3manager.Uploader{
+					S3:                mockApi,
+					PartSize:          s3manager.DefaultUploadPartSize,
+					Concurrency:       s3manager.DefaultUploadConcurrency,
+					LeavePartsOnError: false,
+					MaxUploadParts:    s3manager.MaxUploadParts,
+				},
+				noSuchUploadRetryCount: int(tc.retryCount),
+			}
+
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			atomicCounter := new(int32)
+			atomic.StoreInt32(atomicCounter, 0)
+
+			mockApi.Handlers.Send.Clear()
+			mockApi.Handlers.Unmarshal.Clear()
+			mockApi.Handlers.UnmarshalMeta.Clear()
+			mockApi.Handlers.ValidateResponse.Clear()
+			mockApi.Handlers.Unmarshal.PushBack(func(r *request.Request) {
+				r.Error = tc.err
+				r.HTTPResponse = &http.Response{}
+			})
+			mockApi.Handlers.Unmarshal.PushBack(func(r *request.Request) {
+				atomic.AddInt32(atomicCounter, 1)
+			})
+
+			mockS3.Put(ctx, strings.NewReader(""), url, NewMetadata(), s3manager.DefaultUploadConcurrency, s3manager.DefaultUploadPartSize)
+
+			// +1 is for the original request
+			// *2 is to account for the "Stat" requests that are made to obtain
+			// retry code from object metada.
+			want := 2*tc.retryCount + 1
+			counter := atomic.LoadInt32(atomicCounter)
+			if counter != want {
+				t.Errorf("expected retry request count %d, got %d", want, counter)
+			}
+		})
+	}
+}
+
 func TestS3CopyEncryptionRequest(t *testing.T) {
 	testcases := []struct {
 		name     string
@@ -623,7 +700,7 @@ func TestS3CopyEncryptionRequest(t *testing.T) {
 
 				r.HTTPResponse = &http.Response{
 					StatusCode: http.StatusOK,
-					Body:       ioutil.NopCloser(strings.NewReader("")),
+					Body:       io.NopCloser(strings.NewReader("")),
 				}
 
 				params := r.Params
@@ -725,7 +802,7 @@ func TestS3PutEncryptionRequest(t *testing.T) {
 
 				r.HTTPResponse = &http.Response{
 					StatusCode: http.StatusOK,
-					Body:       ioutil.NopCloser(strings.NewReader("")),
+					Body:       io.NopCloser(strings.NewReader("")),
 				}
 
 				params := r.Params
@@ -818,7 +895,7 @@ func TestS3listObjectsV2(t *testing.T) {
 	mockApi.Handlers.Send.PushBack(func(r *request.Request) {
 		r.HTTPResponse = &http.Response{
 			StatusCode: http.StatusOK,
-			Body:       ioutil.NopCloser(strings.NewReader("")),
+			Body:       io.NopCloser(strings.NewReader("")),
 		}
 
 		r.Data = &s3.ListObjectsV2Output{
@@ -1050,7 +1127,7 @@ func TestSessionAutoRegion(t *testing.T) {
 				r.HTTPResponse = &http.Response{
 					StatusCode: tc.status,
 					Header:     header,
-					Body:       ioutil.NopCloser(strings.NewReader("")),
+					Body:       io.NopCloser(strings.NewReader("")),
 				}
 			})
 
@@ -1182,7 +1259,9 @@ func valueAtPath(i interface{}, s string) interface{} {
 
 // tempError is a wrapper error type that implements anonymous
 // interface getting checked in url.Error.Temporary;
-//    interface { Temporary() bool }
+//
+//	interface { Temporary() bool }
+//
 // see: https://github.com/golang/go/blob/2ebe77a2fda1ee9ff6fd9a3e08933ad1ebaea039/src/net/url/url.go#L38-L43
 //
 // AWS SDK checks if the underlying error in received url.Error implements it;
diff --git a/storage/url/url_test.go b/storage/url/url_test.go
index a1e04aa..4aef7ea 100644
--- a/storage/url/url_test.go
+++ b/storage/url/url_test.go
@@ -1,6 +1,7 @@
 package url
 
 import (
+	"path/filepath"
 	"reflect"
 	"regexp"
 	"testing"
@@ -525,3 +526,77 @@ func TestURLWithMode(t *testing.T) {
 		}
 	}
 }
+
+func TestURLSetRelative(t *testing.T) {
+	type testcase struct {
+		name   string
+		base   string
+		target string
+		expect string
+	}
+
+	sep := string(filepath.Separator)
+	tests := []testcase{
+		{
+			name:   "local_sibling_child_object",
+			base:   sep + "parent" + sep + "child" + sep + "object",
+			target: sep + "parent" + sep + "child2" + sep + "object",
+			expect: ".." + sep + "child2" + sep + "object",
+		},
+		{
+			name:   "local_same_directory_object",
+			base:   sep + "parent" + sep + "child" + sep + "object",
+			target: sep + "parent" + sep + "child" + sep + "object2",
+			expect: "object2",
+		},
+		{
+			name:   "s3_sibling_child_object",
+			base:   "s3://bucket/parent" + sep + "child" + sep + "object",
+			target: "s3://bucket/parent" + sep + "child2" + sep + "",
+			expect: ".." + sep + "child2",
+		},
+		{
+			name:   "local_child_directory_fully_wildcarded",
+			base:   sep + "parent" + sep + "*" + sep + "object",
+			target: sep + "parent" + sep + "child" + sep + "object",
+			expect: "child" + sep + "object",
+		},
+		{
+			name:   "local_child_directory_partially_wildcarded",
+			base:   sep + "parent" + sep + "c*d" + sep + "object",
+			target: sep + "parent" + sep + "child" + sep + "object",
+			expect: "child" + sep + "object",
+		},
+		{
+			name:   "local_child_directory_fully_wildcarded_with_question_mark",
+			base:   sep + "parent" + sep + "?" + sep + "object",
+			target: sep + "parent" + sep + "c" + sep + "object",
+			expect: "c" + sep + "object",
+		},
+		{
+			name:   "s3_child_directory_wildcarded",
+			base:   "s3://bucket/parent" + sep + "*" + sep + "object",
+			target: "s3://bucket/parent" + sep + "child2" + sep + "",
+			expect: "child2",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			baseURL, err := New(tt.base)
+			if err != nil {
+				t.Errorf("URL cannot be instantiated: \nPath: %v, Error: %v", tt.base, err)
+			}
+			targUrl, err := New(tt.target)
+			if err != nil {
+				t.Errorf("URL cannot be instantiated:\nPath: %v, Error: %v", tt.base, err)
+			}
+
+			targUrl.SetRelative(baseURL)
+
+			if diff := cmp.Diff(tt.expect, targUrl.Relative()); diff != "" {
+				t.Errorf("SetRelative() with %s did not produce expected path (-want +got):\n%s", tt.name, diff)
+			}
+		})
+	}
+}