cloudflare-r2-cli 1.0.0 → 1.1.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 +26 -1
- data/README.md +66 -36
- data/docs/ARCHITECTURE.md +122 -82
- data/docs/FEATURES.md +40 -2
- data/lib/r2/cli.rb +124 -12
- data/lib/r2/errors.rb +4 -0
- data/lib/r2/logging.rb +34 -0
- data/lib/r2/storage.rb +65 -12
- data/lib/r2/version.rb +1 -1
- data/lib/r2.rb +1 -0
- metadata +4 -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: 61fad913620eba6d2c642b7d1b8d82dca3598d429fbc96d0304b9b732647b139
|
|
4
|
+
data.tar.gz: 85a0718c8c066018a21f5603f279c30ff1572ed57d5d7e323b2e5692a3b00d63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4879d8be86b64e6a2dfd8230cca78d4f20844ad3dc1955369b01a4f61e87aedf6c22c75aa34746945aa8d23a3cf91dfc4dc903fec612c49f6446de9009f6b632
|
|
7
|
+
data.tar.gz: 4705537941cfe7918ee1885aef24707f9a23cbf8cd799925eb50cb91def26267d6dc77c9a49d8127c625eec5eb65f95fdcbe0a5070c1c2dec1a380d4c5db40d3
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-09-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* `r2 download` — downloads an object from the configured bucket, with
|
|
13
|
+
`--output` to choose a custom destination path.
|
|
14
|
+
* `r2 upload --key` — stores the uploaded file under a custom object key.
|
|
15
|
+
* Global `--verbose` flag — writes detailed diagnostic information to the
|
|
16
|
+
error output during execution.
|
|
17
|
+
* Internal logging support in the CLI and storage layers.
|
|
18
|
+
* `R2::Errors::ObjectNotFoundError` for missing objects.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
* Standardized success messages across commands:
|
|
23
|
+
* `Uploaded successfully: <key>`
|
|
24
|
+
* `Downloaded successfully: <destination>`
|
|
25
|
+
* `Deleted successfully: <key>`
|
|
26
|
+
* Restructured the `README` with a table of contents and documentation for
|
|
27
|
+
the new command, options and global flag.
|
|
28
|
+
* Integrated the documentation into the repository: architecture, features
|
|
29
|
+
and security guides now live in `docs/`, replacing the dedicated docs
|
|
30
|
+
branch.
|
|
31
|
+
|
|
8
32
|
## [1.0.0] - 2026-08-30
|
|
9
33
|
|
|
10
34
|
First stable release of `cloudflare-r2-cli`.
|
|
@@ -21,5 +45,6 @@ First stable release of `cloudflare-r2-cli`.
|
|
|
21
45
|
* Unit, integration and E2E test suites.
|
|
22
46
|
* Continuous integration via GitHub Actions.
|
|
23
47
|
|
|
24
|
-
[Unreleased]: https://github.com/rpzerosixcode/cloudflare-r2-cli/compare/v1.
|
|
48
|
+
[Unreleased]: https://github.com/rpzerosixcode/cloudflare-r2-cli/compare/v1.1.0...HEAD
|
|
49
|
+
[1.1.0]: https://github.com/rpzerosixcode/cloudflare-r2-cli/compare/v1.0.0...v1.1.0
|
|
25
50
|
[1.0.0]: https://github.com/rpzerosixcode/cloudflare-r2-cli/releases/tag/v1.0.0
|
data/README.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
* [Global Options](#global-options)
|
|
16
|
+
* [Testing](#testing)
|
|
17
|
+
* [Changelog](#changelog)
|
|
18
|
+
* [License](#license)
|
|
19
|
+
* [Documentation](#documentation)
|
|
20
|
+
|
|
5
21
|
## Requirements
|
|
6
22
|
|
|
7
23
|
* Ruby **3.3** or higher.
|
|
@@ -21,40 +37,49 @@ $ git clone https://github.com/rpzerosixcode/cloudflare-r2-cli.git
|
|
|
21
37
|
$ cd cloudflare-r2-cli
|
|
22
38
|
$ bundle install
|
|
23
39
|
$ bundle exec rake build
|
|
24
|
-
$ gem install pkg/cloudflare-r2-cli-1.
|
|
40
|
+
$ gem install pkg/cloudflare-r2-cli-1.1.0.gem
|
|
25
41
|
```
|
|
26
42
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
### Configuration
|
|
43
|
+
## Configuration
|
|
30
44
|
|
|
31
45
|
Before using the CLI, define the required environment variables:
|
|
32
46
|
|
|
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.
|
|
47
|
+
| Variable | Description |
|
|
48
|
+
| ---------------------- | ------------------------------------------------------------------ |
|
|
49
|
+
| `R2_ACCESS_KEY_ID` | Cloudflare R2 S3 access key ID. |
|
|
50
|
+
| `R2_SECRET_ACCESS_KEY` | Cloudflare R2 S3 secret access key. |
|
|
51
|
+
| `R2_ENDPOINT` | Cloudflare R2 S3-compatible endpoint. |
|
|
38
52
|
| `R2_REGION` | Region of the S3-compatible endpoint. *(optional, default `auto`)* |
|
|
39
|
-
| `R2_BUCKET` | Default bucket used by the CLI.
|
|
53
|
+
| `R2_BUCKET` | Default bucket used by the CLI. |
|
|
40
54
|
|
|
41
|
-
A fillable template is available in
|
|
55
|
+
A fillable template is available in `.env.example`.
|
|
42
56
|
|
|
43
|
-
Credentials are read only from environment variables and must never be
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
46
60
|
|
|
47
61
|
### Upload
|
|
48
62
|
|
|
49
|
-
Uploads
|
|
63
|
+
Uploads a file to the configured bucket:
|
|
50
64
|
|
|
51
65
|
```console
|
|
52
66
|
$ r2 upload image.jpg
|
|
53
67
|
$ r2 upload ./images/photo.png
|
|
68
|
+
$ r2 upload ./images/photo.png --key uploads/photo.png
|
|
54
69
|
```
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
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. On success, a confirmation message is displayed.
|
|
72
|
+
|
|
73
|
+
### Download
|
|
74
|
+
|
|
75
|
+
Downloads a file stored in the configured bucket:
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
$ r2 download image.jpg
|
|
79
|
+
$ r2 download image.jpg --output ./images/photo.png
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
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
83
|
|
|
59
84
|
### Delete
|
|
60
85
|
|
|
@@ -64,7 +89,7 @@ Deletes a file stored in the configured bucket:
|
|
|
64
89
|
$ r2 delete image.jpg
|
|
65
90
|
```
|
|
66
91
|
|
|
67
|
-
|
|
92
|
+
On success, a confirmation message is displayed.
|
|
68
93
|
|
|
69
94
|
### List
|
|
70
95
|
|
|
@@ -74,15 +99,20 @@ Lists the files stored in the configured bucket:
|
|
|
74
99
|
$ r2 list
|
|
75
100
|
```
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
and exits with status code `1`.
|
|
102
|
+
### Global Options
|
|
79
103
|
|
|
80
|
-
|
|
104
|
+
Every command accepts the global `--verbose` flag, which writes detailed diagnostic information to the error output without changing the standard output:
|
|
81
105
|
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
```console
|
|
107
|
+
$ r2 list --verbose
|
|
108
|
+
$ r2 upload image.jpg --verbose
|
|
109
|
+
$ r2 download image.jpg --verbose
|
|
110
|
+
$ r2 delete image.jpg --verbose
|
|
111
|
+
```
|
|
84
112
|
|
|
85
|
-
|
|
113
|
+
On any error, the CLI displays the corresponding message on the error output and exits with status code `1`.
|
|
114
|
+
|
|
115
|
+
## Testing
|
|
86
116
|
|
|
87
117
|
Run the full suite (unit, integration and E2E):
|
|
88
118
|
|
|
@@ -98,20 +128,20 @@ $ bundle exec rake integration
|
|
|
98
128
|
$ bundle exec rake e2e
|
|
99
129
|
```
|
|
100
130
|
|
|
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.
|
|
131
|
+
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
132
|
|
|
105
|
-
##
|
|
133
|
+
## Changelog
|
|
106
134
|
|
|
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.
|
|
135
|
+
See [`CHANGELOG.md`](CHANGELOG.md) for the version history of the project.
|
|
114
136
|
|
|
115
137
|
## License
|
|
116
138
|
|
|
117
|
-
[MIT License](./
|
|
139
|
+
[MIT License](./LICENSE) — Terms of use and distribution of the project.
|
|
140
|
+
|
|
141
|
+
## Documentation
|
|
142
|
+
|
|
143
|
+
Additional guides are versioned with the code in the `docs/` directory:
|
|
144
|
+
|
|
145
|
+
* [Features](docs/FEATURES.md) — commands, options and behaviors.
|
|
146
|
+
* [Architecture](docs/ARCHITECTURE.md) — layers, data flow, testing and design decisions.
|
|
147
|
+
* [Security](docs/SECURITY.md) — security practices adopted by the project.
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,92 +1,132 @@
|
|
|
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
|
+
### Configuration
|
|
19
|
+
|
|
20
|
+
`lib/r2/configuration.rb`
|
|
21
|
+
|
|
22
|
+
Responsible exclusively for application configuration:
|
|
23
|
+
|
|
24
|
+
* Reads settings from environment variables.
|
|
25
|
+
* Requires `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_ENDPOINT`, and `R2_BUCKET`.
|
|
26
|
+
* Defaults `R2_REGION` to `auto`.
|
|
27
|
+
* Raises `Errors::ConfigurationError` when a required variable is missing.
|
|
28
|
+
|
|
29
|
+
No other component reads `ENV` directly.
|
|
30
|
+
|
|
31
|
+
### Storage
|
|
32
|
+
|
|
33
|
+
`lib/r2/storage.rb`
|
|
34
|
+
|
|
35
|
+
Wraps `aws-sdk-s3` to communicate with the S3-compatible Cloudflare R2 endpoint.
|
|
36
|
+
|
|
37
|
+
Responsible only for:
|
|
38
|
+
|
|
39
|
+
* Uploading objects.
|
|
40
|
+
* Downloading objects.
|
|
41
|
+
* Deleting objects.
|
|
42
|
+
* Listing objects.
|
|
43
|
+
|
|
44
|
+
`Storage` does not:
|
|
45
|
+
|
|
46
|
+
* Read local files.
|
|
47
|
+
* Transform content.
|
|
48
|
+
* Determine object keys.
|
|
49
|
+
* Read configuration directly from `ENV`.
|
|
50
|
+
|
|
51
|
+
The bucket is provided by `Configuration` rather than passed to individual operations.
|
|
52
|
+
|
|
53
|
+
### Errors
|
|
54
|
+
|
|
55
|
+
`lib/r2/errors.rb`
|
|
56
|
+
|
|
57
|
+
Defines the application's domain-level errors.
|
|
58
|
+
|
|
59
|
+
All errors inherit from `R2::Errors::Error`. Exceptions raised by `aws-sdk-s3` are mapped to specific application errors:
|
|
60
|
+
|
|
61
|
+
* `ConfigurationError`
|
|
62
|
+
* `BucketNotFoundError`
|
|
63
|
+
* `ObjectNotFoundError`
|
|
64
|
+
* `NetworkError`
|
|
65
|
+
* `StorageError`
|
|
66
|
+
|
|
67
|
+
This prevents callers from depending on AWS SDK-specific exceptions.
|
|
68
|
+
|
|
69
|
+
The CLI rescues the base `Error`, prints the message to `stderr`, and exits with a non-zero status.
|
|
70
|
+
|
|
71
|
+
### Logging
|
|
72
|
+
|
|
73
|
+
`lib/r2/logging.rb`
|
|
74
|
+
|
|
75
|
+
`R2::Logging` builds the loggers used for diagnostics:
|
|
76
|
+
|
|
77
|
+
* `R2::Logging.build` returns a standard `Logger` writing to the error output when the `--verbose` flag is present, and a `NullLogger` otherwise.
|
|
78
|
+
* `NullLogger` silently discards debug messages, keeping collaborators free from nil checks.
|
|
79
|
+
|
|
80
|
+
The CLI builds the logger from the `--verbose` flag and shares it with `Storage` so diagnostics follow the requested verbosity.
|
|
81
|
+
|
|
82
|
+
## Data Flow
|
|
18
83
|
|
|
19
84
|
```text
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| .rspec
|
|
32
|
-
| .rubocop.yml
|
|
33
|
-
|
|
|
34
|
-
+---.github
|
|
35
|
-
| \---workflows
|
|
36
|
-
| ci.yml
|
|
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
|
|
85
|
+
CLI
|
|
86
|
+
│
|
|
87
|
+
├── Configuration ──→ ENV
|
|
88
|
+
│
|
|
89
|
+
├── Logging ←── --verbose
|
|
90
|
+
│
|
|
91
|
+
└── Storage ──→ aws-sdk-s3 ──→ Cloudflare R2
|
|
92
|
+
│
|
|
93
|
+
└── failure → R2::Errors::*
|
|
94
|
+
│
|
|
95
|
+
└── CLI → stderr + exit 1
|
|
92
96
|
```
|
|
97
|
+
|
|
98
|
+
## Testing
|
|
99
|
+
|
|
100
|
+
### Unit Tests
|
|
101
|
+
|
|
102
|
+
`spec/unit/`
|
|
103
|
+
|
|
104
|
+
Tests individual components in isolation.
|
|
105
|
+
|
|
106
|
+
### Integration Tests
|
|
107
|
+
|
|
108
|
+
`spec/integration/`
|
|
109
|
+
|
|
110
|
+
Tests the interaction between the CLI and `Storage` using a fake S3 client.
|
|
111
|
+
|
|
112
|
+
### End-to-End Tests
|
|
113
|
+
|
|
114
|
+
`spec/e2e/`
|
|
115
|
+
|
|
116
|
+
Tests the application against a real Cloudflare R2 bucket using `R2_TEST_BUCKET`.
|
|
117
|
+
|
|
118
|
+
These tests are skipped when the required credentials are unavailable.
|
|
119
|
+
|
|
120
|
+
## Design Decisions
|
|
121
|
+
|
|
122
|
+
### Direct Constructor Injection
|
|
123
|
+
|
|
124
|
+
No dependency-injection container is used. Dependencies are passed directly through constructors, keeping the architecture simple and explicit.
|
|
125
|
+
|
|
126
|
+
### R2 Region
|
|
127
|
+
|
|
128
|
+
`R2_REGION` defaults to `auto`, which is the recommended region value for Cloudflare R2.
|
|
129
|
+
|
|
130
|
+
### Runtime XML Dependency
|
|
131
|
+
|
|
132
|
+
`rexml` is included as a required runtime dependency because it is used for XML parsing by `aws-sdk-s3`.
|
data/docs/FEATURES.md
CHANGED
|
@@ -7,7 +7,7 @@ Uploads a file to the configured Cloudflare R2 bucket.
|
|
|
7
7
|
**Usage:**
|
|
8
8
|
|
|
9
9
|
```console
|
|
10
|
-
$ r2 upload <file>
|
|
10
|
+
$ r2 upload <file> [--key <key>]
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
**Examples:**
|
|
@@ -15,6 +15,7 @@ $ r2 upload <file>
|
|
|
15
15
|
```console
|
|
16
16
|
$ r2 upload image.jpg
|
|
17
17
|
$ r2 upload ./images/photo.png
|
|
18
|
+
$ r2 upload ./images/photo.png --key uploads/photo.png
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
**Behavior:**
|
|
@@ -23,11 +24,37 @@ $ r2 upload ./images/photo.png
|
|
|
23
24
|
* Validates that the path is not a directory.
|
|
24
25
|
* Opens the file in binary read mode.
|
|
25
26
|
* Uploads the content to the configured Cloudflare R2 bucket.
|
|
26
|
-
* Uses the file name as the object key in the bucket
|
|
27
|
+
* Uses the file name as the object key in the bucket, or the custom key given via `--key`.
|
|
27
28
|
* Displays a success message after the upload.
|
|
28
29
|
|
|
29
30
|
On error, the corresponding message is displayed and the CLI exits with a non-zero status code.
|
|
30
31
|
|
|
32
|
+
## Download
|
|
33
|
+
|
|
34
|
+
Downloads a file stored in the configured Cloudflare R2 bucket.
|
|
35
|
+
|
|
36
|
+
**Usage:**
|
|
37
|
+
|
|
38
|
+
```console
|
|
39
|
+
$ r2 download <key> [--output <path>]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Examples:**
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
$ r2 download image.jpg
|
|
46
|
+
$ r2 download image.jpg --output ./images/photo.png
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Behavior:**
|
|
50
|
+
|
|
51
|
+
* Receives the object key to download.
|
|
52
|
+
* Streams the content directly to the destination file, so large objects do not need to be fully loaded into memory.
|
|
53
|
+
* Writes the content to a file named after the object key in the current directory, unless `--output` chooses a custom destination path.
|
|
54
|
+
* Displays a success message after the download.
|
|
55
|
+
|
|
56
|
+
On error, the corresponding message is displayed and the CLI exits with a non-zero status code.
|
|
57
|
+
|
|
31
58
|
## Delete
|
|
32
59
|
|
|
33
60
|
Deletes a file stored in the configured Cloudflare R2 bucket.
|
|
@@ -73,3 +100,14 @@ $ r2 list
|
|
|
73
100
|
* Returns all objects without pagination or control over the amount of returned objects.
|
|
74
101
|
|
|
75
102
|
On error, the corresponding message is displayed and the CLI exits with a non-zero status code.
|
|
103
|
+
|
|
104
|
+
## Global Options
|
|
105
|
+
|
|
106
|
+
Every command accepts the global `--verbose` flag, which writes detailed diagnostic information to the error output without changing the standard output:
|
|
107
|
+
|
|
108
|
+
```console
|
|
109
|
+
$ r2 list --verbose
|
|
110
|
+
$ r2 upload image.jpg --verbose
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Without the flag, diagnostics are discarded by a null logger and only the standard output is produced.
|
data/lib/r2/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require_relative "errors"
|
|
5
|
+
require_relative "logging"
|
|
5
6
|
|
|
6
7
|
module R2
|
|
7
8
|
# Command line interface of the project.
|
|
@@ -9,6 +10,11 @@ module R2
|
|
|
9
10
|
# Acts as a minimal orchestrator: interprets the user's input, delegates
|
|
10
11
|
# the execution to the responsible components and presents the results.
|
|
11
12
|
class CLI < Thor
|
|
13
|
+
class_option :verbose,
|
|
14
|
+
type: :boolean,
|
|
15
|
+
default: false,
|
|
16
|
+
desc: "Displays detailed information during execution"
|
|
17
|
+
|
|
12
18
|
# Initializes the CLI with its dependencies.
|
|
13
19
|
#
|
|
14
20
|
# Dependencies are loaded lazily: they are only created when the first
|
|
@@ -17,14 +23,18 @@ module R2
|
|
|
17
23
|
#
|
|
18
24
|
# @param configuration [Configuration] application configuration
|
|
19
25
|
# @param storage [Storage] storage used in object operations
|
|
26
|
+
# @param logger [#debug, nil] logger used for diagnostics
|
|
20
27
|
def initialize(
|
|
21
28
|
*,
|
|
22
29
|
configuration: nil,
|
|
23
|
-
storage: nil
|
|
30
|
+
storage: nil,
|
|
31
|
+
logger: nil
|
|
24
32
|
)
|
|
25
33
|
super(*)
|
|
26
34
|
@configuration = configuration
|
|
27
35
|
@storage = storage
|
|
36
|
+
@injected_logger = logger
|
|
37
|
+
@logger = nil
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
# Ensures that Thor exits with a non-zero status code when an
|
|
@@ -46,28 +56,33 @@ module R2
|
|
|
46
56
|
exit 1
|
|
47
57
|
end
|
|
48
58
|
|
|
49
|
-
desc "upload FILE", "Uploads
|
|
59
|
+
desc "upload FILE", "Uploads a file to R2"
|
|
60
|
+
method_option :key,
|
|
61
|
+
type: :string,
|
|
62
|
+
desc: "Custom object key used in the bucket"
|
|
50
63
|
|
|
51
64
|
long_desc <<~LONGDESC
|
|
52
|
-
Uploads
|
|
53
|
-
|
|
65
|
+
Uploads a file to the configured Cloudflare R2 bucket.
|
|
66
|
+
By default, the object key is the name of the given file.
|
|
67
|
+
Use --key to store the object under a custom key.
|
|
54
68
|
|
|
55
69
|
Examples:
|
|
56
70
|
|
|
57
71
|
$ r2 upload image.jpg
|
|
58
72
|
|
|
59
|
-
$ r2 upload ./images/photo.png
|
|
73
|
+
$ r2 upload ./images/photo.png --key uploads/photo.png
|
|
60
74
|
LONGDESC
|
|
61
75
|
|
|
62
|
-
# Uploads
|
|
63
|
-
#
|
|
64
|
-
# The object key in the bucket will be the name of the given file.
|
|
76
|
+
# Uploads a file to the configured Cloudflare R2 bucket.
|
|
65
77
|
#
|
|
66
78
|
# @param file [String] path of the file to upload
|
|
67
79
|
def upload(file)
|
|
80
|
+
key = options[:key] || File.basename(file)
|
|
81
|
+
logger.debug("Starting upload: #{file.inspect} as #{key.inspect}.")
|
|
68
82
|
body = open_file(file)
|
|
69
|
-
storage.upload(key:
|
|
70
|
-
puts "
|
|
83
|
+
storage.upload(key: key, body: body)
|
|
84
|
+
puts "Uploaded successfully: #{key}"
|
|
85
|
+
logger.debug("Finished upload: #{key.inspect}.")
|
|
71
86
|
rescue Errors::Error => e
|
|
72
87
|
warn "Error: #{e.message}"
|
|
73
88
|
exit 1
|
|
@@ -75,6 +90,39 @@ module R2
|
|
|
75
90
|
body&.close
|
|
76
91
|
end
|
|
77
92
|
|
|
93
|
+
desc "download KEY", "Downloads a file from R2"
|
|
94
|
+
method_option :output,
|
|
95
|
+
type: :string,
|
|
96
|
+
desc: "Local path where the content is written"
|
|
97
|
+
|
|
98
|
+
long_desc <<~LONGDESC
|
|
99
|
+
Downloads a file from the configured Cloudflare R2 bucket.
|
|
100
|
+
By default, the content is written to a file with the object
|
|
101
|
+
key base name in the current directory.
|
|
102
|
+
Use --output to choose a custom destination path.
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
|
|
106
|
+
$ r2 download image.jpg
|
|
107
|
+
|
|
108
|
+
$ r2 download image.jpg --output ./images/photo.png
|
|
109
|
+
LONGDESC
|
|
110
|
+
|
|
111
|
+
# Downloads a file from the configured Cloudflare R2 bucket.
|
|
112
|
+
#
|
|
113
|
+
# @param key [String] object key in the bucket
|
|
114
|
+
def download(key)
|
|
115
|
+
destination = options[:output] || File.basename(key)
|
|
116
|
+
logger.debug("Starting download: #{key.inspect} to #{destination.inspect}.")
|
|
117
|
+
ensure_destination_writable(destination)
|
|
118
|
+
storage.download(key: key, destination: destination)
|
|
119
|
+
puts "Downloaded successfully: #{destination}"
|
|
120
|
+
logger.debug("Finished download: #{key.inspect}.")
|
|
121
|
+
rescue Errors::Error => e
|
|
122
|
+
warn "Error: #{e.message}"
|
|
123
|
+
exit 1
|
|
124
|
+
end
|
|
125
|
+
|
|
78
126
|
desc "delete FILE", "Deletes a file from R2"
|
|
79
127
|
|
|
80
128
|
long_desc <<~LONGDESC
|
|
@@ -92,8 +140,10 @@ module R2
|
|
|
92
140
|
#
|
|
93
141
|
# @param file [String] name of the file to delete
|
|
94
142
|
def delete(file)
|
|
143
|
+
logger.debug("Starting delete: #{file.inspect}.")
|
|
95
144
|
storage.delete(key: file)
|
|
96
|
-
puts "
|
|
145
|
+
puts "Deleted successfully: #{file}"
|
|
146
|
+
logger.debug("Finished delete: #{file.inspect}.")
|
|
97
147
|
rescue Errors::Error => e
|
|
98
148
|
warn "Error: #{e.message}"
|
|
99
149
|
exit 1
|
|
@@ -111,11 +161,13 @@ module R2
|
|
|
111
161
|
|
|
112
162
|
# Lists the files stored in the configured Cloudflare R2 bucket.
|
|
113
163
|
def list
|
|
164
|
+
logger.debug("Starting list.")
|
|
114
165
|
files = storage.list
|
|
115
166
|
|
|
116
167
|
files.each do |file|
|
|
117
168
|
puts file
|
|
118
169
|
end
|
|
170
|
+
logger.debug("Finished list: #{files.size} object(s).")
|
|
119
171
|
rescue Errors::Error => e
|
|
120
172
|
warn "Error: #{e.message}"
|
|
121
173
|
exit 1
|
|
@@ -153,9 +205,69 @@ module R2
|
|
|
153
205
|
|
|
154
206
|
# Returns the storage, creating it lazily on first use.
|
|
155
207
|
#
|
|
208
|
+
# Shares the CLI logger with the storage so diagnostics follow
|
|
209
|
+
# the requested verbosity.
|
|
210
|
+
#
|
|
156
211
|
# @return [Storage] storage used in object operations
|
|
157
212
|
def storage
|
|
158
|
-
@storage ||= Storage.new(configuration)
|
|
213
|
+
@storage ||= Storage.new(configuration, logger: logger)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Returns the logger used for diagnostics.
|
|
217
|
+
#
|
|
218
|
+
# An injected logger is always used as-is. Otherwise, a logger is
|
|
219
|
+
# built from the `--verbose` flag: verbose output goes to the
|
|
220
|
+
# error output, while the default is a null logger that silently
|
|
221
|
+
# ignores debug messages.
|
|
222
|
+
#
|
|
223
|
+
# @return [#debug] logger used for diagnostics
|
|
224
|
+
def logger
|
|
225
|
+
return @logger if logger_ready?
|
|
226
|
+
|
|
227
|
+
@logger_verbose = verbose?
|
|
228
|
+
@logger = build_logger
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Indicates whether verbose output was requested.
|
|
232
|
+
#
|
|
233
|
+
# @return [Boolean] true when `--verbose` was given
|
|
234
|
+
def verbose?
|
|
235
|
+
options[:verbose] == true
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Indicates whether the logger already matches the current verbosity.
|
|
239
|
+
#
|
|
240
|
+
# @return [Boolean] true when the logger is ready to use
|
|
241
|
+
def logger_ready?
|
|
242
|
+
!@logger.nil? && defined?(@logger_verbose) && @logger_verbose == verbose?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Builds the logger for the current verbosity.
|
|
246
|
+
#
|
|
247
|
+
# @return [#debug] logger used for diagnostics
|
|
248
|
+
def build_logger
|
|
249
|
+
return @injected_logger unless @injected_logger.nil?
|
|
250
|
+
|
|
251
|
+
R2::Logging.build(verbose: verbose?)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Ensures the download destination can be written.
|
|
255
|
+
#
|
|
256
|
+
# @param destination [String] local path where the content is written
|
|
257
|
+
# @raise [Errors::InvalidFileError] if the destination is a directory
|
|
258
|
+
# @raise [Errors::PermissionError] if the destination cannot be written
|
|
259
|
+
def ensure_destination_writable(destination)
|
|
260
|
+
if File.directory?(destination)
|
|
261
|
+
raise Errors::InvalidFileError, "The destination path is a directory: #{destination}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
parent = File.dirname(destination)
|
|
265
|
+
|
|
266
|
+
raise Errors::FileNotFoundError, "Destination directory not found: #{parent}" unless File.directory?(parent)
|
|
267
|
+
|
|
268
|
+
return if File.writable?(parent) && (!File.exist?(destination) || File.writable?(destination))
|
|
269
|
+
|
|
270
|
+
raise Errors::PermissionError, "Permission denied to write the file: #{destination}"
|
|
159
271
|
end
|
|
160
272
|
end
|
|
161
273
|
end
|
data/lib/r2/errors.rb
CHANGED
|
@@ -31,6 +31,10 @@ module R2
|
|
|
31
31
|
class BucketNotFoundError < Error
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# The requested object does not exist in the bucket.
|
|
35
|
+
class ObjectNotFoundError < Error
|
|
36
|
+
end
|
|
37
|
+
|
|
34
38
|
# Network failure while communicating with Cloudflare R2.
|
|
35
39
|
class NetworkError < Error
|
|
36
40
|
end
|
data/lib/r2/logging.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module R2
|
|
6
|
+
# Builds and configures loggers used for diagnostics.
|
|
7
|
+
module Logging
|
|
8
|
+
# Discards all diagnostic messages.
|
|
9
|
+
#
|
|
10
|
+
# Used as the default when no logger is provided, keeping
|
|
11
|
+
# collaborators free from nil checks.
|
|
12
|
+
class NullLogger
|
|
13
|
+
# Ignores a debug message.
|
|
14
|
+
#
|
|
15
|
+
# @param _message [String] message to ignore
|
|
16
|
+
# @return [nil]
|
|
17
|
+
def debug(_message); end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Builds a logger according to the requested verbosity.
|
|
21
|
+
#
|
|
22
|
+
# @param verbose [Boolean] whether detailed output is enabled
|
|
23
|
+
# @param output [IO] destination of the diagnostic messages
|
|
24
|
+
# @return [Logger, NullLogger] configured logger
|
|
25
|
+
def self.build(verbose: false, output: $stderr)
|
|
26
|
+
return NullLogger.new unless verbose
|
|
27
|
+
|
|
28
|
+
Logger.new(output).tap do |log|
|
|
29
|
+
log.level = Logger::DEBUG
|
|
30
|
+
log.formatter = proc { |_severity, _datetime, _progname, message| "#{message}\n" }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/r2/storage.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "aws-sdk-s3"
|
|
4
4
|
require_relative "errors"
|
|
5
|
+
require_relative "logging"
|
|
5
6
|
|
|
6
7
|
module R2
|
|
7
8
|
# Storage layer responsible for communicating with Cloudflare R2.
|
|
@@ -12,8 +13,10 @@ module R2
|
|
|
12
13
|
# Initializes the storage with the configured credentials and bucket.
|
|
13
14
|
#
|
|
14
15
|
# @param config [Configuration] application configuration
|
|
15
|
-
|
|
16
|
+
# @param logger [#debug, nil] logger used for diagnostics
|
|
17
|
+
def initialize(config, logger: nil)
|
|
16
18
|
@bucket = config.bucket
|
|
19
|
+
@logger = logger || R2::Logging::NullLogger.new
|
|
17
20
|
@s3 = Aws::S3::Client.new(
|
|
18
21
|
region: config.region,
|
|
19
22
|
access_key_id: config.access_key_id,
|
|
@@ -32,13 +35,16 @@ module R2
|
|
|
32
35
|
# @param body [IO, String] content of the object to upload
|
|
33
36
|
# @raise [Errors::Error] if the operation fails
|
|
34
37
|
def upload(key:, body:)
|
|
38
|
+
@logger.debug("Uploading object #{key.inspect} to bucket #{@bucket.inspect}.")
|
|
35
39
|
@s3.put_object(
|
|
36
40
|
bucket: @bucket,
|
|
37
41
|
key: key,
|
|
38
42
|
body: body
|
|
39
43
|
)
|
|
44
|
+
@logger.debug("Upload of object #{key.inspect} completed.")
|
|
45
|
+
nil
|
|
40
46
|
rescue StandardError => e
|
|
41
|
-
raise_storage_error(e)
|
|
47
|
+
raise_storage_error(e, operation: "upload", key: key)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
# Deletes an object from the configured bucket.
|
|
@@ -49,12 +55,15 @@ module R2
|
|
|
49
55
|
# @param key [String] object key in the bucket
|
|
50
56
|
# @raise [Errors::Error] if the operation fails
|
|
51
57
|
def delete(key:)
|
|
58
|
+
@logger.debug("Deleting object #{key.inspect} from bucket #{@bucket.inspect}.")
|
|
52
59
|
@s3.delete_object(
|
|
53
60
|
bucket: @bucket,
|
|
54
61
|
key: key
|
|
55
62
|
)
|
|
63
|
+
@logger.debug("Deletion of object #{key.inspect} completed.")
|
|
64
|
+
nil
|
|
56
65
|
rescue StandardError => e
|
|
57
|
-
raise_storage_error(e)
|
|
66
|
+
raise_storage_error(e, operation: "delete", key: key)
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
# Lists the objects stored in the configured bucket.
|
|
@@ -62,10 +71,33 @@ module R2
|
|
|
62
71
|
# @return [Array<String>] keys of the stored objects
|
|
63
72
|
# @raise [Errors::Error] if the operation fails
|
|
64
73
|
def list
|
|
74
|
+
@logger.debug("Listing objects in bucket #{@bucket.inspect}.")
|
|
65
75
|
response = @s3.list_objects_v2(bucket: @bucket)
|
|
66
|
-
response.contents.map(&:key)
|
|
76
|
+
keys = response.contents.map(&:key)
|
|
77
|
+
@logger.debug("Found #{keys.size} object(s) in bucket #{@bucket.inspect}.")
|
|
78
|
+
keys
|
|
67
79
|
rescue StandardError => e
|
|
68
|
-
raise_storage_error(e)
|
|
80
|
+
raise_storage_error(e, operation: "list")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Downloads an object from the configured bucket.
|
|
84
|
+
#
|
|
85
|
+
# The content is streamed directly to the destination file, so large
|
|
86
|
+
# objects do not need to be fully loaded into memory.
|
|
87
|
+
#
|
|
88
|
+
# @param key [String] object key in the bucket
|
|
89
|
+
# @param destination [String] local path where the content is written
|
|
90
|
+
# @return [String] destination path
|
|
91
|
+
# @raise [Errors::Error] if the operation fails
|
|
92
|
+
def download(key:, destination:)
|
|
93
|
+
@logger.debug("Downloading object #{key.inspect} to #{destination.inspect}.")
|
|
94
|
+
File.open(destination, "wb") do |file|
|
|
95
|
+
@s3.get_object(bucket: @bucket, key: key, response_target: file)
|
|
96
|
+
end
|
|
97
|
+
@logger.debug("Download of object #{key.inspect} completed.")
|
|
98
|
+
destination
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
raise_storage_error(e, operation: "download", key: key, destination: destination)
|
|
69
101
|
end
|
|
70
102
|
|
|
71
103
|
private
|
|
@@ -76,18 +108,39 @@ module R2
|
|
|
76
108
|
# The original message is preserved when useful.
|
|
77
109
|
#
|
|
78
110
|
# @param error [StandardError] original error
|
|
111
|
+
# @param operation [String] operation being performed
|
|
112
|
+
# @param key [String, nil] object key involved in the operation
|
|
113
|
+
# @param destination [String, nil] destination path involved
|
|
79
114
|
# @raise [Errors::Error] subclass matching the cause of the error
|
|
80
|
-
def raise_storage_error(error)
|
|
115
|
+
def raise_storage_error(error, operation:, key: nil, destination: nil)
|
|
116
|
+
raise error if error.is_a?(Errors::Error)
|
|
117
|
+
|
|
118
|
+
@logger.debug("Operation #{operation} failed: #{error.class}: #{error.message}")
|
|
119
|
+
|
|
120
|
+
mapped = map_storage_error(error, key: key, destination: destination)
|
|
121
|
+
raise mapped unless mapped.nil?
|
|
122
|
+
|
|
123
|
+
raise Errors::StorageError, error.message
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Maps known storage failures to domain errors.
|
|
127
|
+
#
|
|
128
|
+
# @param error [StandardError] original error
|
|
129
|
+
# @param key [String, nil] object key involved in the operation
|
|
130
|
+
# @param destination [String, nil] destination path involved
|
|
131
|
+
# @return [Errors::Error, nil] mapped domain error, if recognized
|
|
132
|
+
def map_storage_error(error, key:, destination:)
|
|
81
133
|
case error
|
|
82
134
|
when Aws::Errors::MissingCredentialsError
|
|
83
|
-
|
|
84
|
-
"Missing or invalid credential environment variables."
|
|
135
|
+
Errors::ConfigurationError.new("Missing or invalid credential environment variables.")
|
|
85
136
|
when Aws::S3::Errors::NoSuchBucket
|
|
86
|
-
|
|
137
|
+
Errors::BucketNotFoundError.new("Bucket not found: #{@bucket}")
|
|
138
|
+
when Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::NotFound
|
|
139
|
+
Errors::ObjectNotFoundError.new("Object not found: #{key}")
|
|
87
140
|
when Seahorse::Client::NetworkingError
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
141
|
+
Errors::NetworkError.new(error.message)
|
|
142
|
+
when Errno::EACCES, Errno::EPERM
|
|
143
|
+
Errors::PermissionError.new("Permission denied to write the file: #{destination || key}")
|
|
91
144
|
end
|
|
92
145
|
end
|
|
93
146
|
end
|
data/lib/r2/version.rb
CHANGED
data/lib/r2.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cloudflare-r2-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rpzerosixcode
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: aws-sdk-s3
|
|
@@ -117,25 +117,18 @@ extensions: []
|
|
|
117
117
|
extra_rdoc_files: []
|
|
118
118
|
files:
|
|
119
119
|
- CHANGELOG.md
|
|
120
|
-
-
|
|
120
|
+
- LICENSE
|
|
121
121
|
- README.md
|
|
122
122
|
- Rakefile
|
|
123
123
|
- bin/r2
|
|
124
124
|
- docs/ARCHITECTURE.md
|
|
125
|
-
- docs/DECISIONS.md
|
|
126
|
-
- docs/DEVELOPMENT.md
|
|
127
125
|
- docs/FEATURES.md
|
|
128
|
-
- docs/ROADMAP.md
|
|
129
126
|
- docs/SECURITY.md
|
|
130
|
-
- docs/architecture/cli.md
|
|
131
|
-
- docs/architecture/configuration.md
|
|
132
|
-
- docs/architecture/errors.md
|
|
133
|
-
- docs/architecture/storage.md
|
|
134
|
-
- docs/architecture/testing.md
|
|
135
127
|
- lib/r2.rb
|
|
136
128
|
- lib/r2/cli.rb
|
|
137
129
|
- lib/r2/configuration.rb
|
|
138
130
|
- lib/r2/errors.rb
|
|
131
|
+
- lib/r2/logging.rb
|
|
139
132
|
- lib/r2/storage.rb
|
|
140
133
|
- lib/r2/version.rb
|
|
141
134
|
homepage: https://github.com/rpzerosixcode/cloudflare-r2-cli
|
data/docs/DECISIONS.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Decisions
|
|
2
|
-
|
|
3
|
-
This document records the main architectural and project-level decisions.
|
|
4
|
-
|
|
5
|
-
## Name
|
|
6
|
-
|
|
7
|
-
The publication name of the project is **`cloudflare-r2-cli`**.
|
|
8
|
-
|
|
9
|
-
For command usage, **`r2`** is used.
|
|
10
|
-
|
|
11
|
-
## Content
|
|
12
|
-
|
|
13
|
-
The project's public content is maintained in **English**.
|
|
14
|
-
|
|
15
|
-
## Changelog
|
|
16
|
-
|
|
17
|
-
Formal changelog maintenance starts with version **`1.0.0`** in
|
|
18
|
-
[CHANGELOG.md](../CHANGELOG.md).
|
|
19
|
-
|
|
20
|
-
## Versioning
|
|
21
|
-
|
|
22
|
-
The project follows **Semantic Versioning**, starting with version **`1.0.0`**.
|
|
23
|
-
|
|
24
|
-
## Dependency Injection
|
|
25
|
-
|
|
26
|
-
Dependencies should preferably be provided through **dependency injection**,
|
|
27
|
-
avoiding unnecessary coupling to concrete implementations.
|
|
28
|
-
|
|
29
|
-
The project does not use a dependency injection container. Dependencies are
|
|
30
|
-
provided directly by the components that require them.
|
data/docs/DEVELOPMENT.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Development
|
|
2
|
-
|
|
3
|
-
## Branches
|
|
4
|
-
|
|
5
|
-
The project uses two main branches:
|
|
6
|
-
|
|
7
|
-
* `develop`: development.
|
|
8
|
-
* `main`: stable version.
|
|
9
|
-
|
|
10
|
-
## Pull Requests
|
|
11
|
-
|
|
12
|
-
Changes between branches must be made through Pull Requests.
|
|
13
|
-
|
|
14
|
-
Pull Requests must be clear, objective and pass the required checks before merging.
|
|
15
|
-
|
|
16
|
-
## Commits
|
|
17
|
-
|
|
18
|
-
Commits must follow the **Conventional Commits** convention, using types such as:
|
|
19
|
-
|
|
20
|
-
* `feat`: new feature.
|
|
21
|
-
* `fix`: bug fix.
|
|
22
|
-
* `docs`: documentation change.
|
|
23
|
-
* `refactor`: refactoring without behavior change.
|
|
24
|
-
* `test`: creation or change of tests.
|
|
25
|
-
* `chore`: maintenance tasks.
|
|
26
|
-
|
|
27
|
-
## Continuous Integration
|
|
28
|
-
|
|
29
|
-
The project uses **GitHub Actions** to automatically validate changes on every
|
|
30
|
-
push to the `develop` and `main` branches and on Pull Requests.
|
|
31
|
-
|
|
32
|
-
The workflow defined in `.github/workflows/ci.yml` runs:
|
|
33
|
-
|
|
34
|
-
* **Lint** — RuboCop.
|
|
35
|
-
* **Tests** — unit, integration and E2E suites. The E2E scenarios are marked
|
|
36
|
-
as pending when the test credentials are not configured in the repository
|
|
37
|
-
secrets (`R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_ENDPOINT`,
|
|
38
|
-
`R2_REGION` and `R2_TEST_BUCKET`).
|
|
39
|
-
* **Packaging** — gem build (`rake build`).
|
|
40
|
-
|
|
41
|
-
## Releases
|
|
42
|
-
|
|
43
|
-
Releases are published from tags in the `v*` format (for example, `v1.0.0`).
|
|
44
|
-
|
|
45
|
-
The workflow defined in `.github/workflows/release.yml`:
|
|
46
|
-
|
|
47
|
-
* validates the project (lint, tests and packaging);
|
|
48
|
-
* publishes the gem to RubyGems using the `RUBYGEMS_API_KEY` repository secret;
|
|
49
|
-
* creates a GitHub Release with the packed gem attached.
|
|
50
|
-
|
|
51
|
-
To release a new version:
|
|
52
|
-
|
|
53
|
-
1. Update the version in `lib/r2/version.rb` and the changelog in `CHANGELOG.md`.
|
|
54
|
-
2. Merge the changes into `main`.
|
|
55
|
-
3. Create and push the version tag:
|
|
56
|
-
```console
|
|
57
|
-
$ git tag v1.0.0
|
|
58
|
-
$ git push origin v1.0.0
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Principles
|
|
62
|
-
|
|
63
|
-
Development must prioritize simplicity, organization and code maintenance.
|
|
64
|
-
|
|
65
|
-
Changes must remain aligned with the current scope of the project and its documentation.
|
data/docs/ROADMAP.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# Roadmap
|
|
2
|
-
|
|
3
|
-
The roadmap tracks the planned evolution of the project.
|
|
4
|
-
|
|
5
|
-
## MVP
|
|
6
|
-
|
|
7
|
-
The MVP was completed through the phases below.
|
|
8
|
-
|
|
9
|
-
### Phase 1 — Initial Preparation
|
|
10
|
-
|
|
11
|
-
Initial structure and fundamental project definitions.
|
|
12
|
-
|
|
13
|
-
### Phase 2 — Features
|
|
14
|
-
|
|
15
|
-
Implementation of the essential MVP features.
|
|
16
|
-
|
|
17
|
-
#### Phase 2.1 — Upload
|
|
18
|
-
|
|
19
|
-
Implementation of the upload feature.
|
|
20
|
-
|
|
21
|
-
#### Phase 2.2 — Delete
|
|
22
|
-
|
|
23
|
-
Implementation of the delete feature.
|
|
24
|
-
|
|
25
|
-
#### Phase 2.3 — List
|
|
26
|
-
|
|
27
|
-
Implementation of the list feature.
|
|
28
|
-
|
|
29
|
-
### Phase 3 — Test Coverage
|
|
30
|
-
|
|
31
|
-
Implementation and expansion of the project's test coverage.
|
|
32
|
-
|
|
33
|
-
### Phase 4 — Refinement and Stabilization
|
|
34
|
-
|
|
35
|
-
Review, refinement and stabilization of the project.
|
|
36
|
-
|
|
37
|
-
- **Portability** — ensure the CLI works in different environments and operating systems.
|
|
38
|
-
- **Consistency** — review and standardize code, tests, messages and behaviors.
|
|
39
|
-
- **Error handling** — review exception handling and ensure clear, safe messages.
|
|
40
|
-
- **Security** — review settings and ensure sensitive information is not exposed.
|
|
41
|
-
- **Documentation** — review and update the public documentation according to the current state of the project.
|
|
42
|
-
- **Development context** — remove or isolate documentation exclusively related to the development process.
|
|
43
|
-
- **Packaging** — validate the build, installation and execution of the distributed package.
|
|
44
|
-
- **Continuous integration** — integrate the CI flow into the development process, ensuring automated execution of tests and checks.
|
|
45
|
-
- **Final validation** — run the full test suite and validate the project in a clean environment.
|
|
46
|
-
|
|
47
|
-
### Phase 5 — Release
|
|
48
|
-
|
|
49
|
-
Preparation and publication of the first stable version of the project.
|
|
50
|
-
|
|
51
|
-
- **Versioning** — adopt semantic versioning from `1.0.0`.
|
|
52
|
-
- **Changelog** — start formal changelog maintenance from `1.0.0`.
|
|
53
|
-
- **Documentation** — normalize the public documentation according to the stable version.
|
|
54
|
-
- **Development context** — remove or isolate development-specific documentation that is no longer relevant.
|
|
55
|
-
- **MVP context** — remove or update MVP-specific notes and references that no longer apply to the stable version.
|
|
56
|
-
- **Translation** — translate and standardize the project content to English.
|
|
57
|
-
- **Release validation** — validate the version, build and release artifacts before publication.
|
|
58
|
-
- **Publication** — publish the `cloudflare-r2-cli` package on RubyGems.
|
|
59
|
-
- **Post-release validation** — install the published package in a clean environment and confirm it works.
|
|
60
|
-
|
|
61
|
-
## Future Evolution
|
|
62
|
-
|
|
63
|
-
Possible next steps for the project:
|
|
64
|
-
|
|
65
|
-
- **Pagination and control of the number of returned objects** in the `list` command.
|
|
66
|
-
- **Additional configuration sources** such as files and command-line flags.
|
|
67
|
-
- **Multipart uploads** for large files.
|
|
68
|
-
|
|
69
|
-
> **Note:** The focus will remain a **minimally scalable base** and **essential features**, not optimizations.
|
data/docs/architecture/cli.md
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# CLI
|
|
2
|
-
|
|
3
|
-
The CLI is responsible for interpreting the user's input, executing the
|
|
4
|
-
corresponding actions and presenting the results.
|
|
5
|
-
|
|
6
|
-
The `thor` gem is used to define and execute the commands.
|
|
7
|
-
|
|
8
|
-
## Responsibility
|
|
9
|
-
|
|
10
|
-
The CLI acts as a **minimal orchestrator**, coordinating the operations at a
|
|
11
|
-
high level.
|
|
12
|
-
|
|
13
|
-
## Boundaries
|
|
14
|
-
|
|
15
|
-
The CLI must not implement business rules, directly handle files or know
|
|
16
|
-
details of the implementations and services used.
|
|
17
|
-
|
|
18
|
-
The execution of the operations must be delegated to the responsible
|
|
19
|
-
components.
|
|
20
|
-
|
|
21
|
-
## Commands
|
|
22
|
-
|
|
23
|
-
The available commands and their behaviors are documented in
|
|
24
|
-
[FEATURES.md](../FEATURES.md).
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# Configuration
|
|
2
|
-
|
|
3
|
-
The `Configuration` centralizes the application settings.
|
|
4
|
-
|
|
5
|
-
## Source
|
|
6
|
-
|
|
7
|
-
The settings are obtained directly from **environment variables**. The other
|
|
8
|
-
components must not access `ENV` directly.
|
|
9
|
-
|
|
10
|
-
## Variables
|
|
11
|
-
|
|
12
|
-
The required variables are `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`,
|
|
13
|
-
`R2_ENDPOINT` and `R2_BUCKET`.
|
|
14
|
-
|
|
15
|
-
The `R2_REGION` variable is optional and, when absent, uses the default value
|
|
16
|
-
`auto`, recommended for Cloudflare R2.
|
|
17
|
-
|
|
18
|
-
When a required variable is absent, the `Configuration` raises
|
|
19
|
-
`R2::Errors::ConfigurationError` with a message indicating the variable.
|
|
20
|
-
|
|
21
|
-
## Evolution
|
|
22
|
-
|
|
23
|
-
The source of the settings may be diversified in the future if a real need
|
|
24
|
-
arises.
|
data/docs/architecture/errors.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Errors
|
|
2
|
-
|
|
3
|
-
Errors are centralized to standardize their handling and keep the application
|
|
4
|
-
behavior consistent.
|
|
5
|
-
|
|
6
|
-
## Hierarchy
|
|
7
|
-
|
|
8
|
-
All domain errors inherit from `R2::Errors::Error`:
|
|
9
|
-
|
|
10
|
-
* `ConfigurationError` — required configuration missing or invalid.
|
|
11
|
-
* `FileNotFoundError` — the given file does not exist.
|
|
12
|
-
* `InvalidFileError` — the given path is not a file.
|
|
13
|
-
* `PermissionError` — no permission to read the given file.
|
|
14
|
-
* `BucketNotFoundError` — the configured bucket does not exist.
|
|
15
|
-
* `NetworkError` — network failure while communicating with Cloudflare R2.
|
|
16
|
-
* `StorageError` — unclassified failure in the storage layer.
|
|
17
|
-
|
|
18
|
-
## Handling
|
|
19
|
-
|
|
20
|
-
The specific exceptions of the implementations are converted to the
|
|
21
|
-
`R2::Errors` hierarchy, avoiding exposing internal details of the libraries
|
|
22
|
-
and allowing consumers to catch the generic error or a specific error.
|
|
23
|
-
|
|
24
|
-
The CLI catches `R2::Errors::Error`, presents the message on the error output
|
|
25
|
-
and exits with a non-zero status code.
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Storage
|
|
2
|
-
|
|
3
|
-
The storage abstracts the communication with Cloudflare R2, using the
|
|
4
|
-
`aws-sdk-s3` gem.
|
|
5
|
-
|
|
6
|
-
## Dependencies
|
|
7
|
-
|
|
8
|
-
The `aws-sdk-s3` gem requires an XML parser. The project uses `rexml` to meet
|
|
9
|
-
this requirement at runtime.
|
|
10
|
-
|
|
11
|
-
## Responsibility
|
|
12
|
-
|
|
13
|
-
The layer is responsible for communicating with Cloudflare R2, receiving the
|
|
14
|
-
data prepared by the upper layers and executing the storage operations.
|
|
15
|
-
|
|
16
|
-
## Region
|
|
17
|
-
|
|
18
|
-
The region is provided by the `Configuration` through `R2_REGION`, using `auto`
|
|
19
|
-
as the default.
|
|
20
|
-
|
|
21
|
-
## Boundaries
|
|
22
|
-
|
|
23
|
-
The layer must not:
|
|
24
|
-
|
|
25
|
-
* Read or locate files.
|
|
26
|
-
* Process or transform content.
|
|
27
|
-
* Determine the origin of the content.
|
|
28
|
-
|
|
29
|
-
These responsibilities belong to the layers that use the storage.
|
|
30
|
-
|
|
31
|
-
## Configuration
|
|
32
|
-
|
|
33
|
-
The bucket is defined by the `Storage` configuration and is not informed
|
|
34
|
-
individually in each operation.
|
|
35
|
-
|
|
36
|
-
Details of the features and their behavior for the user are documented in
|
|
37
|
-
[FEATURES.md](../FEATURES.md).
|
|
38
|
-
|
|
39
|
-
## Errors
|
|
40
|
-
|
|
41
|
-
The client failures are converted to the `R2::Errors` hierarchy:
|
|
42
|
-
|
|
43
|
-
* `Errors::ConfigurationError` — missing or invalid access credentials.
|
|
44
|
-
* `Errors::BucketNotFoundError` — the configured bucket does not exist.
|
|
45
|
-
* `Errors::NetworkError` — network failure in the communication.
|
|
46
|
-
* `Errors::StorageError` — other failures of the storage layer.
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# Testing
|
|
2
|
-
|
|
3
|
-
Each test type has its own folder, keeping objectives and responsibilities
|
|
4
|
-
separated.
|
|
5
|
-
|
|
6
|
-
## Unit Tests
|
|
7
|
-
|
|
8
|
-
Located in `spec/unit/`.
|
|
9
|
-
|
|
10
|
-
Test components in isolation.
|
|
11
|
-
|
|
12
|
-
## Integration Tests
|
|
13
|
-
|
|
14
|
-
Located in `spec/integration/`.
|
|
15
|
-
|
|
16
|
-
Test the interaction between components.
|
|
17
|
-
|
|
18
|
-
## E2E Tests
|
|
19
|
-
|
|
20
|
-
Located in `spec/e2e/`.
|
|
21
|
-
|
|
22
|
-
Focus on the main flows and expected results, keeping the scenarios simple and
|
|
23
|
-
avoiding tests of internal implementation details.
|
|
24
|
-
|
|
25
|
-
## Environment
|
|
26
|
-
|
|
27
|
-
Tests use a dedicated bucket configured through `R2_TEST_BUCKET`.
|
|
28
|
-
|
|
29
|
-
Temporary files must remain in `tmp/`, which must be included in `.gitignore`.
|
|
30
|
-
|
|
31
|
-
Resources created by tests must be cleaned up at the end of execution whenever
|
|
32
|
-
possible. E2E tests remove uploaded objects at the end of each scenario.
|
|
33
|
-
|
|
34
|
-
Credentials can be provided through the `.env` file or the environment.
|
|
35
|
-
|
|
36
|
-
## Coverage
|
|
37
|
-
|
|
38
|
-
Tests must cover the main behaviors, including success and error scenarios.
|
|
39
|
-
|
|
40
|
-
E2E tests are an exception: they must remain extremely simple and focused only
|
|
41
|
-
on the main user flows.
|
|
42
|
-
|
|
43
|
-
## Execution
|
|
44
|
-
|
|
45
|
-
Run the full test suite:
|
|
46
|
-
|
|
47
|
-
```console
|
|
48
|
-
$ bundle exec rake
|
/data/{LICENCE → LICENSE}
RENAMED
|
File without changes
|