cloudflare-r2-cli 1.0.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 +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENCE +21 -0
- data/README.md +117 -0
- data/Rakefile +44 -0
- data/bin/r2 +7 -0
- data/docs/ARCHITECTURE.md +92 -0
- data/docs/DECISIONS.md +30 -0
- data/docs/DEVELOPMENT.md +65 -0
- data/docs/FEATURES.md +75 -0
- data/docs/ROADMAP.md +69 -0
- data/docs/SECURITY.md +19 -0
- data/docs/architecture/cli.md +24 -0
- data/docs/architecture/configuration.md +24 -0
- data/docs/architecture/errors.md +25 -0
- data/docs/architecture/storage.md +46 -0
- data/docs/architecture/testing.md +48 -0
- data/lib/r2/cli.rb +161 -0
- data/lib/r2/configuration.rb +45 -0
- data/lib/r2/errors.rb +42 -0
- data/lib/r2/storage.rb +94 -0
- data/lib/r2/version.rb +5 -0
- data/lib/r2.rb +7 -0
- metadata +167 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4ceadf052d3952605f7b5c2d6adf2bb1baae97a8bcb00683713434916a7dc068
|
|
4
|
+
data.tar.gz: 9e53105d7191fa1385f20eae36e936da2c3f1bb0700204c32b65591424ef0fd8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bfcd9ae92ac95ae4184f55099d0d324c40cc41d86698f6fd238aca320e436df6d22b71c3f21a8748a985fc2be14335b618493bbec1fd67c886442492b7efecd9
|
|
7
|
+
data.tar.gz: f39eeffa35d7b63bf58cb7010654c17c5154d28f1fa31069f6b13274c2ca8a9ac2469b368191b9c7fc1b3995e7e8076d279e56d011cf022f40256d7870896348
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-08-30
|
|
9
|
+
|
|
10
|
+
First stable release of `cloudflare-r2-cli`.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
* Initial release with the essential commands to manage objects on Cloudflare R2:
|
|
15
|
+
* `r2 upload` — uploads a file to the configured bucket.
|
|
16
|
+
* `r2 delete` — deletes a file from the configured bucket.
|
|
17
|
+
* `r2 list` — lists the files stored in the configured bucket.
|
|
18
|
+
* Configuration through environment variables (`R2_ACCESS_KEY_ID`,
|
|
19
|
+
`R2_SECRET_ACCESS_KEY`, `R2_ENDPOINT`, `R2_REGION` and `R2_BUCKET`).
|
|
20
|
+
* Clear error messages and non-zero exit codes on failures.
|
|
21
|
+
* Unit, integration and E2E test suites.
|
|
22
|
+
* Continuous integration via GitHub Actions.
|
|
23
|
+
|
|
24
|
+
[Unreleased]: https://github.com/rpzerosixcode/cloudflare-r2-cli/compare/v1.0.0...HEAD
|
|
25
|
+
[1.0.0]: https://github.com/rpzerosixcode/cloudflare-r2-cli/releases/tag/v1.0.0
|
data/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rpzerosixcode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Cloudflare R2 CLI
|
|
2
|
+
|
|
3
|
+
Ruby CLI to manage objects on Cloudflare R2 from the terminal.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
* Ruby **3.3** or higher.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### From a published gem
|
|
12
|
+
|
|
13
|
+
```console
|
|
14
|
+
$ gem install cloudflare-r2-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### From the source code
|
|
18
|
+
|
|
19
|
+
```console
|
|
20
|
+
$ git clone https://github.com/rpzerosixcode/cloudflare-r2-cli.git
|
|
21
|
+
$ cd cloudflare-r2-cli
|
|
22
|
+
$ bundle install
|
|
23
|
+
$ bundle exec rake build
|
|
24
|
+
$ gem install pkg/cloudflare-r2-cli-1.0.0.gem
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Configuration
|
|
30
|
+
|
|
31
|
+
Before using the CLI, define the required environment variables:
|
|
32
|
+
|
|
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. |
|
|
38
|
+
| `R2_REGION` | Region of the S3-compatible endpoint. *(optional, default `auto`)* |
|
|
39
|
+
| `R2_BUCKET` | Default bucket used by the CLI. |
|
|
40
|
+
|
|
41
|
+
A fillable template is available in [`.env.example`](.env.example).
|
|
42
|
+
|
|
43
|
+
Credentials are read only from environment variables and must never be
|
|
44
|
+
inserted into code or versioned files. See
|
|
45
|
+
[`docs/SECURITY.md`](docs/SECURITY.md) for more details.
|
|
46
|
+
|
|
47
|
+
### Upload
|
|
48
|
+
|
|
49
|
+
Uploads an image to the configured bucket:
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
$ r2 upload image.jpg
|
|
53
|
+
$ r2 upload ./images/photo.png
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The object key in the bucket will be the name of the given file. On success,
|
|
57
|
+
a confirmation message is displayed.
|
|
58
|
+
|
|
59
|
+
### Delete
|
|
60
|
+
|
|
61
|
+
Deletes a file stored in the configured bucket:
|
|
62
|
+
|
|
63
|
+
```console
|
|
64
|
+
$ r2 delete image.jpg
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The operation is confirmed by the result of the deletion returned by the service.
|
|
68
|
+
|
|
69
|
+
### List
|
|
70
|
+
|
|
71
|
+
Lists the files stored in the configured bucket:
|
|
72
|
+
|
|
73
|
+
```console
|
|
74
|
+
$ r2 list
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
On any error, the CLI displays the corresponding message on the error output
|
|
78
|
+
and exits with status code `1`.
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
The development guidelines, branches and commits are described in
|
|
83
|
+
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
|
|
84
|
+
|
|
85
|
+
### Tests
|
|
86
|
+
|
|
87
|
+
Run the full suite (unit, integration and E2E):
|
|
88
|
+
|
|
89
|
+
```console
|
|
90
|
+
$ bundle exec rake
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run only one level:
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
$ bundle exec rake unit
|
|
97
|
+
$ bundle exec rake integration
|
|
98
|
+
$ bundle exec rake e2e
|
|
99
|
+
```
|
|
100
|
+
|
|
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.
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
* [Architecture](docs/ARCHITECTURE.md) — Overview of the project architecture.
|
|
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.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
[MIT License](./LICENCE) — Terms of use and distribution of the project.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rubocop/rake_task"
|
|
5
|
+
require "rspec/core/rake_task"
|
|
6
|
+
require_relative "lib/r2/version"
|
|
7
|
+
|
|
8
|
+
RuboCop::RakeTask.new
|
|
9
|
+
|
|
10
|
+
desc "Runs all tests"
|
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
12
|
+
|
|
13
|
+
desc "Runs the unit tests"
|
|
14
|
+
RSpec::Core::RakeTask.new(:unit) do |task|
|
|
15
|
+
task.pattern = "spec/unit/**/*_spec.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Runs the integration tests"
|
|
19
|
+
RSpec::Core::RakeTask.new(:integration) do |task|
|
|
20
|
+
task.pattern = "spec/integration/**/*_spec.rb"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "Runs the E2E tests"
|
|
24
|
+
RSpec::Core::RakeTask.new(:e2e) do |task|
|
|
25
|
+
task.pattern = "spec/e2e/**/*_spec.rb"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Builds the gem into pkg/"
|
|
29
|
+
task :build do
|
|
30
|
+
FileUtils.mkdir_p("pkg")
|
|
31
|
+
gem_file = "pkg/cloudflare-r2-cli-#{R2::VERSION}.gem"
|
|
32
|
+
sh "gem build r2.gemspec --output #{gem_file}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "Installs the gem locally"
|
|
36
|
+
task install: :build do
|
|
37
|
+
gem_file = "pkg/cloudflare-r2-cli-#{R2::VERSION}.gem"
|
|
38
|
+
sh "gem install #{gem_file} --no-document"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "Runs the CI checks (style, tests and packaging)"
|
|
42
|
+
task ci: %i[rubocop spec build]
|
|
43
|
+
|
|
44
|
+
task default: :spec
|
data/bin/r2
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Test Strategy
|
|
4
|
+
|
|
5
|
+
* [Testing](architecture/testing.md) — Test strategy and organization.
|
|
6
|
+
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
### Architecture
|
|
10
|
+
|
|
11
|
+
* [CLI](architecture/cli.md) — Structure and operation of the command line interface.
|
|
12
|
+
* [Configuration](architecture/configuration.md) — Organization and management of the application settings.
|
|
13
|
+
* [Errors](architecture/errors.md) — Strategy and organization of error handling.
|
|
14
|
+
* [Storage](architecture/storage.md) — Organization of the storage and persistence layer.
|
|
15
|
+
* [Testing](architecture/testing.md) — Test strategy and organization.
|
|
16
|
+
|
|
17
|
+
## Project Structure
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
C:.
|
|
21
|
+
| CHANGELOG.md
|
|
22
|
+
| LICENCE
|
|
23
|
+
| README.md
|
|
24
|
+
| Rakefile
|
|
25
|
+
| r2.gemspec
|
|
26
|
+
| Gemfile
|
|
27
|
+
| Gemfile.lock
|
|
28
|
+
| .env.example
|
|
29
|
+
| .gitattributes
|
|
30
|
+
| .gitignore
|
|
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
|
|
92
|
+
```
|
data/docs/DECISIONS.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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/FEATURES.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Features
|
|
2
|
+
|
|
3
|
+
## Upload
|
|
4
|
+
|
|
5
|
+
Uploads a file to the configured Cloudflare R2 bucket.
|
|
6
|
+
|
|
7
|
+
**Usage:**
|
|
8
|
+
|
|
9
|
+
```console
|
|
10
|
+
$ r2 upload <file>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Examples:**
|
|
14
|
+
|
|
15
|
+
```console
|
|
16
|
+
$ r2 upload image.jpg
|
|
17
|
+
$ r2 upload ./images/photo.png
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Behavior:**
|
|
21
|
+
|
|
22
|
+
* Validates that the given file exists.
|
|
23
|
+
* Validates that the path is not a directory.
|
|
24
|
+
* Opens the file in binary read mode.
|
|
25
|
+
* Uploads the content to the configured Cloudflare R2 bucket.
|
|
26
|
+
* Uses the file name as the object key in the bucket.
|
|
27
|
+
* Displays a success message after the upload.
|
|
28
|
+
|
|
29
|
+
On error, the corresponding message is displayed and the CLI exits with a non-zero status code.
|
|
30
|
+
|
|
31
|
+
## Delete
|
|
32
|
+
|
|
33
|
+
Deletes a file stored in the configured Cloudflare R2 bucket.
|
|
34
|
+
|
|
35
|
+
**Usage:**
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
$ r2 delete <file>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Example:**
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
$ r2 delete image.jpg
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Behavior:**
|
|
48
|
+
|
|
49
|
+
* Receives the name of the file to delete.
|
|
50
|
+
* Uses the file name as the object key.
|
|
51
|
+
* Requests the object deletion from Cloudflare R2.
|
|
52
|
+
* Considers the operation successful when the storage completes the request without errors.
|
|
53
|
+
* Displays a success message after the operation.
|
|
54
|
+
|
|
55
|
+
The feature does not perform a follow-up query to check that the object no longer exists. The success confirmation is based on the result of the deletion operation provided by the storage layer.
|
|
56
|
+
|
|
57
|
+
On error, the corresponding message is displayed and the CLI exits with a non-zero status code.
|
|
58
|
+
|
|
59
|
+
## List
|
|
60
|
+
|
|
61
|
+
Lists the files stored in the configured Cloudflare R2 bucket.
|
|
62
|
+
|
|
63
|
+
**Usage:**
|
|
64
|
+
|
|
65
|
+
```console
|
|
66
|
+
$ r2 list
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Behavior:**
|
|
70
|
+
|
|
71
|
+
* Queries the objects stored in the configured bucket.
|
|
72
|
+
* Displays the files found.
|
|
73
|
+
* Returns all objects without pagination or control over the amount of returned objects.
|
|
74
|
+
|
|
75
|
+
On error, the corresponding message is displayed and the CLI exits with a non-zero status code.
|
data/docs/ROADMAP.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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/SECURITY.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
The project follows the following security practices:
|
|
4
|
+
|
|
5
|
+
- **Credentials through environment variables** — access credentials and
|
|
6
|
+
sensitive settings are obtained through environment variables and are not
|
|
7
|
+
stored in source code or versioned files.
|
|
8
|
+
|
|
9
|
+
- **Isolated test environment** — E2E tests use a dedicated bucket
|
|
10
|
+
(`R2_TEST_BUCKET`) separated from the default application bucket.
|
|
11
|
+
|
|
12
|
+
- **Ignored environment files** — `.env` files are excluded from version
|
|
13
|
+
control. Only the `.env.example` template is versioned.
|
|
14
|
+
|
|
15
|
+
- **CI and secrets** — CI credentials are provided through repository secrets
|
|
16
|
+
and are not exposed in workflow logs.
|
|
17
|
+
|
|
18
|
+
If an access key is accidentally exposed, revoke it immediately through the
|
|
19
|
+
Cloudflare dashboard and generate a new one.
|
|
@@ -0,0 +1,24 @@
|
|
|
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).
|
|
@@ -0,0 +1,24 @@
|
|
|
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.
|
|
@@ -0,0 +1,25 @@
|
|
|
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.
|
|
@@ -0,0 +1,46 @@
|
|
|
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.
|
|
@@ -0,0 +1,48 @@
|
|
|
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/lib/r2/cli.rb
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module R2
|
|
7
|
+
# Command line interface of the project.
|
|
8
|
+
#
|
|
9
|
+
# Acts as a minimal orchestrator: interprets the user's input, delegates
|
|
10
|
+
# the execution to the responsible components and presents the results.
|
|
11
|
+
class CLI < Thor
|
|
12
|
+
# Initializes the CLI with its dependencies.
|
|
13
|
+
#
|
|
14
|
+
# Dependencies are loaded lazily: they are only created when the first
|
|
15
|
+
# command actually uses them. This allows help and command listing to
|
|
16
|
+
# work without a configured environment.
|
|
17
|
+
#
|
|
18
|
+
# @param configuration [Configuration] application configuration
|
|
19
|
+
# @param storage [Storage] storage used in object operations
|
|
20
|
+
def initialize(
|
|
21
|
+
*,
|
|
22
|
+
configuration: nil,
|
|
23
|
+
storage: nil
|
|
24
|
+
)
|
|
25
|
+
super(*)
|
|
26
|
+
@configuration = configuration
|
|
27
|
+
@storage = storage
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Ensures that Thor exits with a non-zero status code when an
|
|
31
|
+
# operation fails.
|
|
32
|
+
def self.exit_on_failure?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Starts the CLI and clearly presents domain errors not handled by
|
|
37
|
+
# the commands.
|
|
38
|
+
#
|
|
39
|
+
# Errors raised before a command runs (such as missing configuration)
|
|
40
|
+
# are caught here, shown on the error output and terminate the CLI
|
|
41
|
+
# with status code 1, avoiding stack traces.
|
|
42
|
+
def self.start(*args)
|
|
43
|
+
super
|
|
44
|
+
rescue Errors::Error => e
|
|
45
|
+
warn "Error: #{e.message}"
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "upload FILE", "Uploads an image to R2"
|
|
50
|
+
|
|
51
|
+
long_desc <<~LONGDESC
|
|
52
|
+
Uploads an image to the configured Cloudflare R2 bucket.
|
|
53
|
+
The object key in the bucket will be the name of the given file.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
|
|
57
|
+
$ r2 upload image.jpg
|
|
58
|
+
|
|
59
|
+
$ r2 upload ./images/photo.png
|
|
60
|
+
LONGDESC
|
|
61
|
+
|
|
62
|
+
# Uploads an image to the configured Cloudflare R2 bucket.
|
|
63
|
+
#
|
|
64
|
+
# The object key in the bucket will be the name of the given file.
|
|
65
|
+
#
|
|
66
|
+
# @param file [String] path of the file to upload
|
|
67
|
+
def upload(file)
|
|
68
|
+
body = open_file(file)
|
|
69
|
+
storage.upload(key: File.basename(file), body: body)
|
|
70
|
+
puts "Image uploaded successfully: #{File.basename(file)}"
|
|
71
|
+
rescue Errors::Error => e
|
|
72
|
+
warn "Error: #{e.message}"
|
|
73
|
+
exit 1
|
|
74
|
+
ensure
|
|
75
|
+
body&.close
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
desc "delete FILE", "Deletes a file from R2"
|
|
79
|
+
|
|
80
|
+
long_desc <<~LONGDESC
|
|
81
|
+
Deletes a file from the configured Cloudflare R2 bucket.
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
|
|
85
|
+
$ r2 delete image.jpg
|
|
86
|
+
LONGDESC
|
|
87
|
+
|
|
88
|
+
# Deletes a file from the configured Cloudflare R2 bucket.
|
|
89
|
+
#
|
|
90
|
+
# The operation is considered successful when the storage completes
|
|
91
|
+
# the request without errors.
|
|
92
|
+
#
|
|
93
|
+
# @param file [String] name of the file to delete
|
|
94
|
+
def delete(file)
|
|
95
|
+
storage.delete(key: file)
|
|
96
|
+
puts "File deleted successfully: #{file}"
|
|
97
|
+
rescue Errors::Error => e
|
|
98
|
+
warn "Error: #{e.message}"
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
desc "list", "Lists the files stored in R2"
|
|
103
|
+
|
|
104
|
+
long_desc <<~LONGDESC
|
|
105
|
+
Lists the files stored in the configured Cloudflare R2 bucket.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
|
|
109
|
+
$ r2 list
|
|
110
|
+
LONGDESC
|
|
111
|
+
|
|
112
|
+
# Lists the files stored in the configured Cloudflare R2 bucket.
|
|
113
|
+
def list
|
|
114
|
+
files = storage.list
|
|
115
|
+
|
|
116
|
+
files.each do |file|
|
|
117
|
+
puts file
|
|
118
|
+
end
|
|
119
|
+
rescue Errors::Error => e
|
|
120
|
+
warn "Error: #{e.message}"
|
|
121
|
+
exit 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Opens the given file for reading.
|
|
127
|
+
#
|
|
128
|
+
# The file must remain open during the upload, so the block form is
|
|
129
|
+
# not used; closing is guaranteed in the `ensure` of the `upload`
|
|
130
|
+
# command.
|
|
131
|
+
#
|
|
132
|
+
# @param file [String] file path
|
|
133
|
+
# @return [File] file opened in binary read mode
|
|
134
|
+
# @raise [Errors::FileNotFoundError] if the file does not exist
|
|
135
|
+
# @raise [Errors::InvalidFileError] if the path is not a file
|
|
136
|
+
# @raise [Errors::PermissionError] if the file cannot be read
|
|
137
|
+
def open_file(file)
|
|
138
|
+
raise Errors::FileNotFoundError, "File not found: #{file}" unless File.exist?(file)
|
|
139
|
+
raise Errors::InvalidFileError, "The provided path is not a file: #{file}" unless File.file?(file)
|
|
140
|
+
raise Errors::PermissionError, "Permission denied to read the file: #{file}" unless File.readable?(file)
|
|
141
|
+
|
|
142
|
+
File.open(file, "rb")
|
|
143
|
+
rescue Errno::EACCES, Errno::EPERM
|
|
144
|
+
raise Errors::PermissionError, "Permission denied to read the file: #{file}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Returns the application configuration, creating it lazily on first use.
|
|
148
|
+
#
|
|
149
|
+
# @return [Configuration] application configuration
|
|
150
|
+
def configuration
|
|
151
|
+
@configuration ||= Configuration.new
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Returns the storage, creating it lazily on first use.
|
|
155
|
+
#
|
|
156
|
+
# @return [Storage] storage used in object operations
|
|
157
|
+
def storage
|
|
158
|
+
@storage ||= Storage.new(configuration)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module R2
|
|
6
|
+
# Centralized application configuration.
|
|
7
|
+
#
|
|
8
|
+
# Settings are read directly from environment variables.
|
|
9
|
+
class Configuration
|
|
10
|
+
attr_reader :access_key_id,
|
|
11
|
+
:secret_access_key,
|
|
12
|
+
:endpoint,
|
|
13
|
+
:bucket,
|
|
14
|
+
:region
|
|
15
|
+
|
|
16
|
+
# Initializes the configuration from environment variables.
|
|
17
|
+
#
|
|
18
|
+
# `R2_REGION` is optional and defaults to `auto` when absent, which
|
|
19
|
+
# is recommended for Cloudflare R2 endpoints.
|
|
20
|
+
#
|
|
21
|
+
# @raise [Errors::ConfigurationError] when any required variable
|
|
22
|
+
# is not defined
|
|
23
|
+
def initialize
|
|
24
|
+
@access_key_id = fetch_required("R2_ACCESS_KEY_ID")
|
|
25
|
+
@secret_access_key = fetch_required("R2_SECRET_ACCESS_KEY")
|
|
26
|
+
@endpoint = fetch_required("R2_ENDPOINT")
|
|
27
|
+
@bucket = fetch_required("R2_BUCKET")
|
|
28
|
+
@region = ENV.fetch("R2_REGION", "auto")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Gets the value of a required environment variable.
|
|
34
|
+
#
|
|
35
|
+
# @param name [String] name of the environment variable
|
|
36
|
+
# @return [String] variable value
|
|
37
|
+
# @raise [Errors::ConfigurationError] if the variable is not defined
|
|
38
|
+
def fetch_required(name)
|
|
39
|
+
ENV.fetch(name) do
|
|
40
|
+
raise Errors::ConfigurationError,
|
|
41
|
+
"Required environment variable not defined: #{name}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/r2/errors.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module R2
|
|
4
|
+
# Centralization of project errors.
|
|
5
|
+
module Errors
|
|
6
|
+
# Base error of the project.
|
|
7
|
+
#
|
|
8
|
+
# All domain errors inherit from this class, allowing consumers to
|
|
9
|
+
# catch the generic error when they do not need to distinguish the
|
|
10
|
+
# cause, or a specific error when they do.
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Required configuration missing or invalid.
|
|
15
|
+
class ConfigurationError < Error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The given file does not exist.
|
|
19
|
+
class FileNotFoundError < Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The given path is not a file.
|
|
23
|
+
class InvalidFileError < Error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# No permission to read the given file.
|
|
27
|
+
class PermissionError < Error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The configured bucket does not exist.
|
|
31
|
+
class BucketNotFoundError < Error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Network failure while communicating with Cloudflare R2.
|
|
35
|
+
class NetworkError < Error
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Unclassified failure in the storage layer.
|
|
39
|
+
class StorageError < Error
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/r2/storage.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "aws-sdk-s3"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module R2
|
|
7
|
+
# Storage layer responsible for communicating with Cloudflare R2.
|
|
8
|
+
#
|
|
9
|
+
# Uses the `aws-sdk-s3` gem with the S3-compatible endpoint provided by
|
|
10
|
+
# the application configuration.
|
|
11
|
+
class Storage
|
|
12
|
+
# Initializes the storage with the configured credentials and bucket.
|
|
13
|
+
#
|
|
14
|
+
# @param config [Configuration] application configuration
|
|
15
|
+
def initialize(config)
|
|
16
|
+
@bucket = config.bucket
|
|
17
|
+
@s3 = Aws::S3::Client.new(
|
|
18
|
+
region: config.region,
|
|
19
|
+
access_key_id: config.access_key_id,
|
|
20
|
+
secret_access_key: config.secret_access_key,
|
|
21
|
+
endpoint: config.endpoint,
|
|
22
|
+
force_path_style: true
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Uploads an object to the configured bucket.
|
|
27
|
+
#
|
|
28
|
+
# Receives content already prepared by the layer that uses the storage
|
|
29
|
+
# and delivers it to Cloudflare R2.
|
|
30
|
+
#
|
|
31
|
+
# @param key [String] object key in the bucket
|
|
32
|
+
# @param body [IO, String] content of the object to upload
|
|
33
|
+
# @raise [Errors::Error] if the operation fails
|
|
34
|
+
def upload(key:, body:)
|
|
35
|
+
@s3.put_object(
|
|
36
|
+
bucket: @bucket,
|
|
37
|
+
key: key,
|
|
38
|
+
body: body
|
|
39
|
+
)
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
raise_storage_error(e)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Deletes an object from the configured bucket.
|
|
45
|
+
#
|
|
46
|
+
# The operation is considered successful when Cloudflare R2 completes
|
|
47
|
+
# the request without raising an error.
|
|
48
|
+
#
|
|
49
|
+
# @param key [String] object key in the bucket
|
|
50
|
+
# @raise [Errors::Error] if the operation fails
|
|
51
|
+
def delete(key:)
|
|
52
|
+
@s3.delete_object(
|
|
53
|
+
bucket: @bucket,
|
|
54
|
+
key: key
|
|
55
|
+
)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
raise_storage_error(e)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Lists the objects stored in the configured bucket.
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<String>] keys of the stored objects
|
|
63
|
+
# @raise [Errors::Error] if the operation fails
|
|
64
|
+
def list
|
|
65
|
+
response = @s3.list_objects_v2(bucket: @bucket)
|
|
66
|
+
response.contents.map(&:key)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
raise_storage_error(e)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Converts storage layer errors into project domain errors, avoiding
|
|
74
|
+
# exposing internal details of the implementations.
|
|
75
|
+
#
|
|
76
|
+
# The original message is preserved when useful.
|
|
77
|
+
#
|
|
78
|
+
# @param error [StandardError] original error
|
|
79
|
+
# @raise [Errors::Error] subclass matching the cause of the error
|
|
80
|
+
def raise_storage_error(error)
|
|
81
|
+
case error
|
|
82
|
+
when Aws::Errors::MissingCredentialsError
|
|
83
|
+
raise Errors::ConfigurationError,
|
|
84
|
+
"Missing or invalid credential environment variables."
|
|
85
|
+
when Aws::S3::Errors::NoSuchBucket
|
|
86
|
+
raise Errors::BucketNotFoundError, "Bucket not found: #{@bucket}"
|
|
87
|
+
when Seahorse::Client::NetworkingError
|
|
88
|
+
raise Errors::NetworkError, error.message
|
|
89
|
+
else
|
|
90
|
+
raise Errors::StorageError, error.message
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/r2/version.rb
ADDED
data/lib/r2.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cloudflare-r2-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- rpzerosixcode
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: aws-sdk-s3
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 1.229.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 1.229.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rexml
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 3.4.4
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 3.4.4
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: thor
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 1.5.0
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 1.5.0
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.4'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.4'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.13'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.13'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.90'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.90'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop-rake
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0.7'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0.7'
|
|
111
|
+
description: Command line tool in Ruby to manage objects on Cloudflare R2 directly
|
|
112
|
+
from the terminal.
|
|
113
|
+
email:
|
|
114
|
+
executables:
|
|
115
|
+
- r2
|
|
116
|
+
extensions: []
|
|
117
|
+
extra_rdoc_files: []
|
|
118
|
+
files:
|
|
119
|
+
- CHANGELOG.md
|
|
120
|
+
- LICENCE
|
|
121
|
+
- README.md
|
|
122
|
+
- Rakefile
|
|
123
|
+
- bin/r2
|
|
124
|
+
- docs/ARCHITECTURE.md
|
|
125
|
+
- docs/DECISIONS.md
|
|
126
|
+
- docs/DEVELOPMENT.md
|
|
127
|
+
- docs/FEATURES.md
|
|
128
|
+
- docs/ROADMAP.md
|
|
129
|
+
- 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
|
+
- lib/r2.rb
|
|
136
|
+
- lib/r2/cli.rb
|
|
137
|
+
- lib/r2/configuration.rb
|
|
138
|
+
- lib/r2/errors.rb
|
|
139
|
+
- lib/r2/storage.rb
|
|
140
|
+
- lib/r2/version.rb
|
|
141
|
+
homepage: https://github.com/rpzerosixcode/cloudflare-r2-cli
|
|
142
|
+
licenses:
|
|
143
|
+
- MIT
|
|
144
|
+
metadata:
|
|
145
|
+
homepage_uri: https://github.com/rpzerosixcode/cloudflare-r2-cli
|
|
146
|
+
changelog_uri: https://github.com/rpzerosixcode/cloudflare-r2-cli/blob/main/CHANGELOG.md
|
|
147
|
+
rubygems_mfa_required: 'true'
|
|
148
|
+
post_install_message:
|
|
149
|
+
rdoc_options: []
|
|
150
|
+
require_paths:
|
|
151
|
+
- lib
|
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
153
|
+
requirements:
|
|
154
|
+
- - ">="
|
|
155
|
+
- !ruby/object:Gem::Version
|
|
156
|
+
version: 3.3.0
|
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
|
+
requirements:
|
|
159
|
+
- - ">="
|
|
160
|
+
- !ruby/object:Gem::Version
|
|
161
|
+
version: '0'
|
|
162
|
+
requirements: []
|
|
163
|
+
rubygems_version: 3.5.22
|
|
164
|
+
signing_key:
|
|
165
|
+
specification_version: 4
|
|
166
|
+
summary: Ruby CLI to manage objects on Cloudflare R2.
|
|
167
|
+
test_files: []
|