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 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,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "r2"
6
+
7
+ R2::CLI.start(ARGV)
@@ -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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module R2
4
+ VERSION = "1.0.0"
5
+ end
data/lib/r2.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "r2/cli"
4
+ require "r2/configuration"
5
+ require "r2/errors"
6
+ require "r2/storage"
7
+ require "r2/version"
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: []