cloudflare-r2-cli 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -2
- data/README.md +109 -36
- data/docs/ARCHITECTURE.md +177 -82
- data/docs/FEATURES.md +121 -5
- data/docs/SECURITY.md +4 -0
- data/lib/r2/cli.rb +234 -13
- data/lib/r2/content_type.rb +70 -0
- data/lib/r2/errors.rb +13 -0
- data/lib/r2/logging.rb +34 -0
- data/lib/r2/retry.rb +105 -0
- data/lib/r2/storage.rb +183 -24
- data/lib/r2/version.rb +1 -1
- data/lib/r2.rb +3 -0
- metadata +6 -11
- data/docs/DECISIONS.md +0 -30
- data/docs/DEVELOPMENT.md +0 -65
- data/docs/ROADMAP.md +0 -69
- data/docs/architecture/cli.md +0 -24
- data/docs/architecture/configuration.md +0 -24
- data/docs/architecture/errors.md +0 -25
- data/docs/architecture/storage.md +0 -46
- data/docs/architecture/testing.md +0 -48
- /data/{LICENCE → LICENSE} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3195ca7161952b3ef9a5e7cc6e6d1afb4af82c631364a6786f4b1d4aeaace272
|
|
4
|
+
data.tar.gz: ca9017f232ee7a6a8a15048dfc6dd895e490f9ac18eb442dfb7e518deb89830d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7166d020132b6cabf2835a41a8cca93531eb58e4b65102ca8f9d8b9a16b9055eb460578f346a4b38cf15e757d3f5581e14da22eaf2b3b893497deabfb4de53d8
|
|
7
|
+
data.tar.gz: 331b10be069ca8262aceb8ebffebddaedc99c4cf806a4ff35eae5679eeed7304ef7b64079f7d4e66d4c4370767d6859f8e9cce72b776cf1f745bc38c6c88ff78
|
data/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,89 @@
|
|
|
3
3
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
4
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
-
## [
|
|
6
|
+
## [1.2.0] - 2026-09-21
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
* `r2 exists` — checks whether an object exists in the configured bucket,
|
|
11
|
+
exiting with status code `1` when it does not.
|
|
12
|
+
* `r2 list --prefix` — lists only the objects whose keys start with the given
|
|
13
|
+
prefix.
|
|
14
|
+
* `r2 delete --force` — deletes an object without asking for confirmation.
|
|
15
|
+
* Automatic `Content-Type` definition on upload, based on the object key.
|
|
16
|
+
* Automatic retries with exponential backoff for transient network failures
|
|
17
|
+
in every storage operation.
|
|
18
|
+
* `R2::ContentType` — maps the object key extension to a content type.
|
|
19
|
+
* `R2::Retry` — retry policy with exponential backoff shared by the storage
|
|
20
|
+
operations.
|
|
21
|
+
* `R2::Errors::ConfirmationRequiredError` and `R2::Errors::AbortedError`.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
* `r2 delete` now asks for confirmation before deleting. In non-interactive
|
|
26
|
+
executions (scripts and pipelines) the deletion requires `--force`.
|
|
27
|
+
* `r2 list` now follows the pagination of the bucket, listing every object
|
|
28
|
+
instead of only the first page.
|
|
29
|
+
* `Storage#list` accepts a `prefix` and lists every page of the bucket.
|
|
30
|
+
* `Storage#upload` accepts an explicit `content_type` and rewinds the streamed
|
|
31
|
+
content before every attempt.
|
|
32
|
+
* `Storage#exists?` — checks the existence of an object using its metadata.
|
|
33
|
+
* Restructured the documentation to cover the new commands, options and
|
|
34
|
+
reliability behavior.
|
|
35
|
+
|
|
36
|
+
## [1.2.0] - 2026-09-21
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
* `r2 exists` — checks whether an object exists in the configured bucket,
|
|
41
|
+
exiting with status code `1` when it does not.
|
|
42
|
+
* `r2 list --prefix` — lists only the objects whose keys start with the given
|
|
43
|
+
prefix.
|
|
44
|
+
* `r2 delete --force` — deletes an object without asking for confirmation.
|
|
45
|
+
* Automatic `Content-Type` definition on upload, based on the object key.
|
|
46
|
+
* Automatic retries with exponential backoff for transient network failures
|
|
47
|
+
in every storage operation.
|
|
48
|
+
* `R2::ContentType` — maps the object key extension to a content type.
|
|
49
|
+
* `R2::Retry` — retry policy with exponential backoff shared by the storage
|
|
50
|
+
operations.
|
|
51
|
+
* `R2::Errors::ConfirmationRequiredError` and `R2::Errors::AbortedError`.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
* `r2 delete` now asks for confirmation before deleting. In non-interactive
|
|
56
|
+
executions (scripts and pipelines) the deletion requires `--force`.
|
|
57
|
+
* `r2 list` now follows the pagination of the bucket, listing every object
|
|
58
|
+
instead of only the first page.
|
|
59
|
+
* `Storage#list` accepts a `prefix` and lists every page of the bucket.
|
|
60
|
+
* `Storage#upload` accepts an explicit `content_type` and rewinds the streamed
|
|
61
|
+
content before every attempt.
|
|
62
|
+
* `Storage#exists?` — checks the existence of an object using its metadata.
|
|
63
|
+
* Restructured the documentation to cover the new commands, options and
|
|
64
|
+
reliability behavior.
|
|
65
|
+
|
|
66
|
+
## [1.1.0] - 2026-09-09
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
* `r2 download` — downloads an object from the configured bucket, with
|
|
71
|
+
`--output` to choose a custom destination path.
|
|
72
|
+
* `r2 upload --key` — stores the uploaded file under a custom object key.
|
|
73
|
+
* Global `--verbose` flag — writes detailed diagnostic information to the
|
|
74
|
+
error output during execution.
|
|
75
|
+
* Internal logging support in the CLI and storage layers.
|
|
76
|
+
* `R2::Errors::ObjectNotFoundError` for missing objects.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
* Standardized success messages across commands:
|
|
81
|
+
* `Uploaded successfully: <key>`
|
|
82
|
+
* `Downloaded successfully: <destination>`
|
|
83
|
+
* `Deleted successfully: <key>`
|
|
84
|
+
* Restructured the `README` with a table of contents and documentation for
|
|
85
|
+
the new command, options and global flag.
|
|
86
|
+
* Integrated the documentation into the repository: architecture, features
|
|
87
|
+
and security guides now live in `docs/`, replacing the dedicated docs
|
|
88
|
+
branch.
|
|
7
89
|
|
|
8
90
|
## [1.0.0] - 2026-08-30
|
|
9
91
|
|
|
@@ -21,5 +103,7 @@ First stable release of `cloudflare-r2-cli`.
|
|
|
21
103
|
* Unit, integration and E2E test suites.
|
|
22
104
|
* Continuous integration via GitHub Actions.
|
|
23
105
|
|
|
24
|
-
[Unreleased]: https://github.com/rpzerosixcode/cloudflare-r2-cli/compare/v1.
|
|
106
|
+
[Unreleased]: https://github.com/rpzerosixcode/cloudflare-r2-cli/compare/v1.2.0...HEAD
|
|
107
|
+
[1.2.0]: https://github.com/rpzerosixcode/cloudflare-r2-cli/compare/v1.1.0...v1.2.0
|
|
108
|
+
[1.1.0]: https://github.com/rpzerosixcode/cloudflare-r2-cli/compare/v1.0.0...v1.1.0
|
|
25
109
|
[1.0.0]: https://github.com/rpzerosixcode/cloudflare-r2-cli/releases/tag/v1.0.0
|
data/README.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Ruby CLI to manage objects on Cloudflare R2 from the terminal.
|
|
4
4
|
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
* [Requirements](#requirements)
|
|
8
|
+
* [Installation](#installation)
|
|
9
|
+
* [Configuration](#configuration)
|
|
10
|
+
* [Usage](#usage)
|
|
11
|
+
* [Upload](#upload)
|
|
12
|
+
* [Download](#download)
|
|
13
|
+
* [Delete](#delete)
|
|
14
|
+
* [List](#list)
|
|
15
|
+
* [Exists](#exists)
|
|
16
|
+
* [Global Options](#global-options)
|
|
17
|
+
* [Reliability](#reliability)
|
|
18
|
+
* [Testing](#testing)
|
|
19
|
+
* [Changelog](#changelog)
|
|
20
|
+
* [License](#license)
|
|
21
|
+
* [Documentation](#documentation)
|
|
22
|
+
|
|
5
23
|
## Requirements
|
|
6
24
|
|
|
7
25
|
* Ruby **3.3** or higher.
|
|
@@ -21,40 +39,49 @@ $ git clone https://github.com/rpzerosixcode/cloudflare-r2-cli.git
|
|
|
21
39
|
$ cd cloudflare-r2-cli
|
|
22
40
|
$ bundle install
|
|
23
41
|
$ bundle exec rake build
|
|
24
|
-
$ gem install pkg/cloudflare-r2-cli-1.
|
|
42
|
+
$ gem install pkg/cloudflare-r2-cli-1.2.0.gem
|
|
25
43
|
```
|
|
26
44
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
### Configuration
|
|
45
|
+
## Configuration
|
|
30
46
|
|
|
31
47
|
Before using the CLI, define the required environment variables:
|
|
32
48
|
|
|
33
|
-
| Variable | Description
|
|
34
|
-
| ---------------------- |
|
|
35
|
-
| `R2_ACCESS_KEY_ID` | Cloudflare R2 S3 access key ID.
|
|
36
|
-
| `R2_SECRET_ACCESS_KEY` | Cloudflare R2 S3 secret access key.
|
|
37
|
-
| `R2_ENDPOINT` | Cloudflare R2 S3-compatible endpoint.
|
|
49
|
+
| Variable | Description |
|
|
50
|
+
| ---------------------- | ------------------------------------------------------------------ |
|
|
51
|
+
| `R2_ACCESS_KEY_ID` | Cloudflare R2 S3 access key ID. |
|
|
52
|
+
| `R2_SECRET_ACCESS_KEY` | Cloudflare R2 S3 secret access key. |
|
|
53
|
+
| `R2_ENDPOINT` | Cloudflare R2 S3-compatible endpoint. |
|
|
38
54
|
| `R2_REGION` | Region of the S3-compatible endpoint. *(optional, default `auto`)* |
|
|
39
|
-
| `R2_BUCKET` | Default bucket used by the CLI.
|
|
55
|
+
| `R2_BUCKET` | Default bucket used by the CLI. |
|
|
40
56
|
|
|
41
|
-
A fillable template is available in
|
|
57
|
+
A fillable template is available in `.env.example`.
|
|
42
58
|
|
|
43
|
-
Credentials are read only from environment variables and must never be
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
Credentials are read only from environment variables and must never be inserted into code or versioned files. If an access key is accidentally exposed, revoke it immediately through the Cloudflare dashboard and generate a new one.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
46
62
|
|
|
47
63
|
### Upload
|
|
48
64
|
|
|
49
|
-
Uploads
|
|
65
|
+
Uploads a file to the configured bucket:
|
|
50
66
|
|
|
51
67
|
```console
|
|
52
68
|
$ r2 upload image.jpg
|
|
53
69
|
$ r2 upload ./images/photo.png
|
|
70
|
+
$ r2 upload ./images/photo.png --key uploads/photo.png
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
By default, the object key in the bucket is the name of the given file. Use `--key` to store the object under a custom key. The `Content-Type` of the object is defined automatically from its key extension (`application/octet-stream` when the extension is unknown). On success, a confirmation message is displayed.
|
|
74
|
+
|
|
75
|
+
### Download
|
|
76
|
+
|
|
77
|
+
Downloads a file stored in the configured bucket:
|
|
78
|
+
|
|
79
|
+
```console
|
|
80
|
+
$ r2 download image.jpg
|
|
81
|
+
$ r2 download image.jpg --output ./images/photo.png
|
|
54
82
|
```
|
|
55
83
|
|
|
56
|
-
|
|
57
|
-
a confirmation message is displayed.
|
|
84
|
+
By default, the content is written to a file with the object key base name in the current directory. Use `--output` to choose a custom destination path. On success, a confirmation message is displayed.
|
|
58
85
|
|
|
59
86
|
### Delete
|
|
60
87
|
|
|
@@ -62,9 +89,17 @@ Deletes a file stored in the configured bucket:
|
|
|
62
89
|
|
|
63
90
|
```console
|
|
64
91
|
$ r2 delete image.jpg
|
|
92
|
+
$ r2 delete image.jpg --force
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The deletion is confirmed before the request is sent:
|
|
96
|
+
|
|
97
|
+
```console
|
|
98
|
+
$ r2 delete image.jpg
|
|
99
|
+
Delete "image.jpg" from the bucket? [y/N]
|
|
65
100
|
```
|
|
66
101
|
|
|
67
|
-
The operation is
|
|
102
|
+
The operation only proceeds with an affirmative answer (`y` or `yes`). Use `--force` to delete without confirmation, which is required in non-interactive executions, such as scripts and pipelines, where the confirmation cannot be requested. On success, a confirmation message is displayed.
|
|
68
103
|
|
|
69
104
|
### List
|
|
70
105
|
|
|
@@ -72,17 +107,55 @@ Lists the files stored in the configured bucket:
|
|
|
72
107
|
|
|
73
108
|
```console
|
|
74
109
|
$ r2 list
|
|
110
|
+
$ r2 list --prefix uploads/
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Every object is listed, following the pagination of the bucket. Use `--prefix` to list only the objects whose keys start with the given prefix.
|
|
114
|
+
|
|
115
|
+
### Exists
|
|
116
|
+
|
|
117
|
+
Checks whether an object exists in the configured bucket:
|
|
118
|
+
|
|
119
|
+
```console
|
|
120
|
+
$ r2 exists image.jpg
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The exit status is `0` when the object exists and `1` when it does not, which allows the command to be used in scripts:
|
|
124
|
+
|
|
125
|
+
```console
|
|
126
|
+
$ r2 exists image.jpg
|
|
127
|
+
Object exists: image.jpg
|
|
128
|
+
$ r2 exists missing.jpg
|
|
129
|
+
Object not found: missing.jpg
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Global Options
|
|
133
|
+
|
|
134
|
+
Every command accepts the global `--verbose` flag, which writes detailed diagnostic information to the error output without changing the standard output:
|
|
135
|
+
|
|
136
|
+
```console
|
|
137
|
+
$ r2 list --verbose
|
|
138
|
+
$ r2 upload image.jpg --verbose
|
|
139
|
+
$ r2 download image.jpg --verbose
|
|
140
|
+
$ r2 delete image.jpg --force --verbose
|
|
141
|
+
$ r2 exists image.jpg --verbose
|
|
75
142
|
```
|
|
76
143
|
|
|
77
|
-
On any error, the CLI displays the corresponding message on the error output
|
|
78
|
-
|
|
144
|
+
On any error, the CLI displays the corresponding message on the error output and exits with status code `1`.
|
|
145
|
+
|
|
146
|
+
## Reliability
|
|
147
|
+
|
|
148
|
+
### Automatic retries
|
|
149
|
+
|
|
150
|
+
Requests that fail due to transient network failures are retried automatically with exponential backoff: 3 attempts in total, waiting 0.5s and then 1s between them, capped at 5s. Failures that are not transient, such as missing credentials or an invalid bucket, are reported immediately.
|
|
151
|
+
|
|
152
|
+
The retries are applied to every storage operation and restart from the beginning of the data: uploaded content is rewound and downloaded content is written from the start again, so a retried request never uploads an empty body or leaves a partially written file.
|
|
79
153
|
|
|
80
|
-
|
|
154
|
+
### Pagination
|
|
81
155
|
|
|
82
|
-
|
|
83
|
-
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
|
|
156
|
+
`r2 list` follows the pagination of the bucket, requesting every page until the last one, so all objects are listed regardless of the amount of keys in the bucket.
|
|
84
157
|
|
|
85
|
-
|
|
158
|
+
## Testing
|
|
86
159
|
|
|
87
160
|
Run the full suite (unit, integration and E2E):
|
|
88
161
|
|
|
@@ -98,20 +171,20 @@ $ bundle exec rake integration
|
|
|
98
171
|
$ bundle exec rake e2e
|
|
99
172
|
```
|
|
100
173
|
|
|
101
|
-
The E2E tests require real Cloudflare R2 credentials, provided by the `.env`
|
|
102
|
-
file or by the environment. Without them, the scenarios are marked as
|
|
103
|
-
pending and do not fail.
|
|
174
|
+
The E2E tests require real Cloudflare R2 credentials, provided by the `.env` file or by the environment. Without them, the scenarios are marked as pending and do not fail.
|
|
104
175
|
|
|
105
|
-
##
|
|
176
|
+
## Changelog
|
|
106
177
|
|
|
107
|
-
|
|
108
|
-
* [Changelog](CHANGELOG.md) — Version history of the project.
|
|
109
|
-
* [Decisions](docs/DECISIONS.md) — Architecture and project decisions.
|
|
110
|
-
* [Features](docs/FEATURES.md) — Planned and implemented features.
|
|
111
|
-
* [Roadmap](docs/ROADMAP.md) — Planned evolution of the project.
|
|
112
|
-
* [Security](docs/SECURITY.md) — General security guidelines of the project.
|
|
113
|
-
* [Development](docs/DEVELOPMENT.md) — Development guidelines of the project.
|
|
178
|
+
See [`CHANGELOG.md`](CHANGELOG.md) for the version history of the project.
|
|
114
179
|
|
|
115
180
|
## License
|
|
116
181
|
|
|
117
|
-
[MIT License](./
|
|
182
|
+
[MIT License](./LICENSE) — Terms of use and distribution of the project.
|
|
183
|
+
|
|
184
|
+
## Documentation
|
|
185
|
+
|
|
186
|
+
Additional guides are versioned with the code in the `docs/` directory:
|
|
187
|
+
|
|
188
|
+
* [Features](docs/FEATURES.md) — commands, options and behaviors.
|
|
189
|
+
* [Architecture](docs/ARCHITECTURE.md) — layers, data flow, testing and design decisions.
|
|
190
|
+
* [Security](docs/SECURITY.md) — security practices adopted by the project.
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,92 +1,187 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Layers
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### CLI
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`lib/r2/cli.rb`
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Built with `thor`. Acts as a minimal orchestrator:
|
|
10
10
|
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* [Testing](architecture/testing.md) — Test strategy and organization.
|
|
11
|
+
* Parses command-line input.
|
|
12
|
+
* Delegates operations to `Storage` and `Configuration`.
|
|
13
|
+
* Prints results to the user.
|
|
14
|
+
* Handles domain errors.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
The CLI contains no business rules and does not access files or `ENV` directly.
|
|
17
|
+
|
|
18
|
+
Commands that delete data ask for confirmation before delegating the operation. The confirmation is requested only when the standard input is a terminal, and `--force` skips it, so non-interactive executions must opt out explicitly.
|
|
19
|
+
|
|
20
|
+
### Configuration
|
|
21
|
+
|
|
22
|
+
`lib/r2/configuration.rb`
|
|
23
|
+
|
|
24
|
+
Responsible exclusively for application configuration:
|
|
25
|
+
|
|
26
|
+
* Reads settings from environment variables.
|
|
27
|
+
* Requires `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_ENDPOINT`, and `R2_BUCKET`.
|
|
28
|
+
* Defaults `R2_REGION` to `auto`.
|
|
29
|
+
* Raises `Errors::ConfigurationError` when a required variable is missing.
|
|
30
|
+
|
|
31
|
+
No other component reads `ENV` directly.
|
|
32
|
+
|
|
33
|
+
### Storage
|
|
34
|
+
|
|
35
|
+
`lib/r2/storage.rb`
|
|
36
|
+
|
|
37
|
+
Wraps `aws-sdk-s3` to communicate with the S3-compatible Cloudflare R2 endpoint.
|
|
38
|
+
|
|
39
|
+
Responsible only for:
|
|
40
|
+
|
|
41
|
+
* Uploading objects, defining the content type from the object key.
|
|
42
|
+
* Downloading objects.
|
|
43
|
+
* Deleting objects.
|
|
44
|
+
* Listing objects, following the pagination of the bucket and filtering by prefix.
|
|
45
|
+
* Checking whether an object exists.
|
|
46
|
+
|
|
47
|
+
`Storage` does not:
|
|
48
|
+
|
|
49
|
+
* Read local files.
|
|
50
|
+
* Transform content.
|
|
51
|
+
* Determine object keys.
|
|
52
|
+
* Read configuration directly from `ENV`.
|
|
53
|
+
|
|
54
|
+
The bucket is provided by `Configuration` rather than passed to individual operations.
|
|
55
|
+
|
|
56
|
+
Every request is wrapped in the retry support (see `Retry`), so transient network failures are retried before the failure reaches the caller.
|
|
57
|
+
|
|
58
|
+
### Content Type
|
|
59
|
+
|
|
60
|
+
`lib/r2/content_type.rb`
|
|
61
|
+
|
|
62
|
+
Defines the content type of an object from the extension of its key:
|
|
63
|
+
|
|
64
|
+
* `ContentType.for(key)` returns the mapped type, or `application/octet-stream` when the extension is not mapped.
|
|
65
|
+
* The comparison is case insensitive, and keys inside directories are supported.
|
|
66
|
+
|
|
67
|
+
`Storage#upload` uses this value automatically and accepts an explicit `content_type` when the caller needs to override it.
|
|
68
|
+
|
|
69
|
+
### Retry
|
|
70
|
+
|
|
71
|
+
`lib/r2/retry.rb`
|
|
72
|
+
|
|
73
|
+
Concentrates the automatic retries of the application:
|
|
74
|
+
|
|
75
|
+
* `Policy` holds the retry settings: total attempts, base delay and maximum delay.
|
|
76
|
+
* `Retry.call` runs the block and retries only the error classes informed as transient, waiting between the attempts with exponential backoff.
|
|
77
|
+
* The waiting strategy and the logger are injected, keeping the behavior testable without real waiting.
|
|
78
|
+
|
|
79
|
+
`Storage` uses a shared policy for every operation and passes `Seahorse::Client::NetworkingError` as the transient error, which is also the error mapped to `NetworkError`.
|
|
80
|
+
|
|
81
|
+
### Errors
|
|
82
|
+
|
|
83
|
+
`lib/r2/errors.rb`
|
|
84
|
+
|
|
85
|
+
Defines the application's domain-level errors.
|
|
86
|
+
|
|
87
|
+
All errors inherit from `R2::Errors::Error`. Exceptions raised by `aws-sdk-s3` are mapped to specific application errors:
|
|
88
|
+
|
|
89
|
+
* `ConfigurationError`
|
|
90
|
+
* `BucketNotFoundError`
|
|
91
|
+
* `ObjectNotFoundError`
|
|
92
|
+
* `NetworkError`
|
|
93
|
+
* `StorageError`
|
|
94
|
+
|
|
95
|
+
Errors of the CLI are also part of the domain:
|
|
96
|
+
|
|
97
|
+
* `ConfirmationRequiredError` when a destructive operation cannot be confirmed in a non-interactive execution.
|
|
98
|
+
* `AbortedError` when the user does not confirm a destructive operation.
|
|
99
|
+
|
|
100
|
+
This prevents callers from depending on AWS SDK-specific exceptions.
|
|
101
|
+
|
|
102
|
+
The CLI rescues the base `Error`, prints the message to `stderr`, and exits with a non-zero status.
|
|
103
|
+
|
|
104
|
+
### Logging
|
|
105
|
+
|
|
106
|
+
`lib/r2/logging.rb`
|
|
107
|
+
|
|
108
|
+
`R2::Logging` builds the loggers used for diagnostics:
|
|
109
|
+
|
|
110
|
+
* `R2::Logging.build` returns a standard `Logger` writing to the error output when the `--verbose` flag is present, and a `NullLogger` otherwise.
|
|
111
|
+
* `NullLogger` silently discards debug messages, keeping collaborators free from nil checks.
|
|
112
|
+
|
|
113
|
+
The CLI builds the logger from the `--verbose` flag and shares it with `Storage` so diagnostics follow the requested verbosity.
|
|
114
|
+
|
|
115
|
+
## Data Flow
|
|
18
116
|
|
|
19
117
|
```text
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| release.yml
|
|
38
|
-
|
|
|
39
|
-
+---bin
|
|
40
|
-
| r2
|
|
41
|
-
|
|
|
42
|
-
+---docs
|
|
43
|
-
| | ARCHITECTURE.md
|
|
44
|
-
| | DECISIONS.md
|
|
45
|
-
| | DEVELOPMENT.md
|
|
46
|
-
| | FEATURES.md
|
|
47
|
-
| | ROADMAP.md
|
|
48
|
-
| | SECURITY.md
|
|
49
|
-
| |
|
|
50
|
-
| \---architecture
|
|
51
|
-
| cli.md
|
|
52
|
-
| configuration.md
|
|
53
|
-
| errors.md
|
|
54
|
-
| storage.md
|
|
55
|
-
| testing.md
|
|
56
|
-
|
|
|
57
|
-
+---lib
|
|
58
|
-
| | r2.rb
|
|
59
|
-
| |
|
|
60
|
-
| \---r2
|
|
61
|
-
| cli.rb
|
|
62
|
-
| configuration.rb
|
|
63
|
-
| errors.rb
|
|
64
|
-
| storage.rb
|
|
65
|
-
| version.rb
|
|
66
|
-
|
|
|
67
|
-
\---spec
|
|
68
|
-
| spec_helper.rb
|
|
69
|
-
|
|
|
70
|
-
+---e2e
|
|
71
|
-
| e2e_spec.rb
|
|
72
|
-
|
|
|
73
|
-
+---integration
|
|
74
|
-
| cli_storage_spec.rb
|
|
75
|
-
|
|
|
76
|
-
+---support
|
|
77
|
-
| cleanup.rb
|
|
78
|
-
| cli_expectations.rb
|
|
79
|
-
| cli_runner.rb
|
|
80
|
-
| e2e_helper.rb
|
|
81
|
-
| env_helper.rb
|
|
82
|
-
| fake_s3_client.rb
|
|
83
|
-
| output_capture.rb
|
|
84
|
-
| temp_file_helper.rb
|
|
85
|
-
|
|
|
86
|
-
\---unit
|
|
87
|
-
cli_spec.rb
|
|
88
|
-
configuration_spec.rb
|
|
89
|
-
errors_spec.rb
|
|
90
|
-
storage_spec.rb
|
|
91
|
-
version_spec.rb
|
|
118
|
+
CLI
|
|
119
|
+
│
|
|
120
|
+
├── Configuration ──→ ENV
|
|
121
|
+
│
|
|
122
|
+
├── Logging ←── --verbose
|
|
123
|
+
│
|
|
124
|
+
├── $stdin ←── confirmation of destructive commands
|
|
125
|
+
│
|
|
126
|
+
└── Storage
|
|
127
|
+
│
|
|
128
|
+
├── ContentType ──→ Content-Type of the object
|
|
129
|
+
│
|
|
130
|
+
└── Retry ──→ aws-sdk-s3 ──→ Cloudflare R2
|
|
131
|
+
│
|
|
132
|
+
└── failure → R2::Errors::*
|
|
133
|
+
│
|
|
134
|
+
└── CLI → stderr + exit 1
|
|
92
135
|
```
|
|
136
|
+
|
|
137
|
+
Transient network failures are retried by `Retry` before the error mapping, so a brief instability does not interrupt the operation.
|
|
138
|
+
|
|
139
|
+
## Testing
|
|
140
|
+
|
|
141
|
+
### Unit Tests
|
|
142
|
+
|
|
143
|
+
`spec/unit/`
|
|
144
|
+
|
|
145
|
+
Tests individual components in isolation: CLI, configuration, content type, errors, logging, retry and storage.
|
|
146
|
+
|
|
147
|
+
### Integration Tests
|
|
148
|
+
|
|
149
|
+
`spec/integration/`
|
|
150
|
+
|
|
151
|
+
Tests the interaction between the CLI and `Storage` using a fake S3 client, which reproduces the bucket behavior, including the pagination of the listing and the metadata requests.
|
|
152
|
+
|
|
153
|
+
The confirmation prompts are validated with a controlled standard input, so the suite never waits for a real user and the result does not depend on whether the runner has a terminal attached.
|
|
154
|
+
|
|
155
|
+
### End-to-End Tests
|
|
156
|
+
|
|
157
|
+
`spec/e2e/`
|
|
158
|
+
|
|
159
|
+
Tests the application against a real Cloudflare R2 bucket using `R2_TEST_BUCKET`.
|
|
160
|
+
|
|
161
|
+
These tests are skipped when the required credentials are unavailable.
|
|
162
|
+
|
|
163
|
+
## Design Decisions
|
|
164
|
+
|
|
165
|
+
### Direct Constructor Injection
|
|
166
|
+
|
|
167
|
+
No dependency-injection container is used. Dependencies are passed directly through constructors, keeping the architecture simple and explicit. `Storage`, for instance, receives its logger, retry policy and waiting strategy, which makes the retries testable without real waiting.
|
|
168
|
+
|
|
169
|
+
### Deletion Confirmation
|
|
170
|
+
|
|
171
|
+
Destructive commands confirm the operation before sending it, since a deletion cannot be undone. The confirmation depends on an interactive input, so scripts and pipelines must use `--force`: the alternative, deleting silently when no terminal is attached, would make accidental deletions possible in automated executions.
|
|
172
|
+
|
|
173
|
+
### Retries with Backoff
|
|
174
|
+
|
|
175
|
+
Transient network failures are retried with exponential backoff by the application, on top of the retries already performed by `aws-sdk-s3`. Keeping the behavior in the project makes it explicit, configurable and covered by the tests, instead of being an undocumented side effect of the SDK.
|
|
176
|
+
|
|
177
|
+
### Automatic Content Type
|
|
178
|
+
|
|
179
|
+
The content type is derived from the object key instead of the local file, because the key is what defines the object in the bucket and can differ from the name of the uploaded file (`--key`). Unmapped extensions fall back to `application/octet-stream`, the default of S3-compatible services.
|
|
180
|
+
|
|
181
|
+
### R2 Region
|
|
182
|
+
|
|
183
|
+
`R2_REGION` defaults to `auto`, which is the recommended region value for Cloudflare R2.
|
|
184
|
+
|
|
185
|
+
### Runtime XML Dependency
|
|
186
|
+
|
|
187
|
+
`rexml` is included as a required runtime dependency because it is used for XML parsing by `aws-sdk-s3`.
|