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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ceadf052d3952605f7b5c2d6adf2bb1baae97a8bcb00683713434916a7dc068
4
- data.tar.gz: 9e53105d7191fa1385f20eae36e936da2c3f1bb0700204c32b65591424ef0fd8
3
+ metadata.gz: 61fad913620eba6d2c642b7d1b8d82dca3598d429fbc96d0304b9b732647b139
4
+ data.tar.gz: 85a0718c8c066018a21f5603f279c30ff1572ed57d5d7e323b2e5692a3b00d63
5
5
  SHA512:
6
- metadata.gz: bfcd9ae92ac95ae4184f55099d0d324c40cc41d86698f6fd238aca320e436df6d22b71c3f21a8748a985fc2be14335b618493bbec1fd67c886442492b7efecd9
7
- data.tar.gz: f39eeffa35d7b63bf58cb7010654c17c5154d28f1fa31069f6b13274c2ca8a9ac2469b368191b9c7fc1b3995e7e8076d279e56d011cf022f40256d7870896348
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.0.0...HEAD
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.0.0.gem
40
+ $ gem install pkg/cloudflare-r2-cli-1.1.0.gem
25
41
  ```
26
42
 
27
- ## Usage
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 [`.env.example`](.env.example).
55
+ A fillable template is available in `.env.example`.
42
56
 
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.
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 an image to the configured bucket:
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
- The object key in the bucket will be the name of the given file. On success,
57
- a confirmation message is displayed.
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
- The operation is confirmed by the result of the deletion returned by the service.
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
- On any error, the CLI displays the corresponding message on the error output
78
- and exits with status code `1`.
102
+ ### Global Options
79
103
 
80
- ## Development
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
- The development guidelines, branches and commits are described in
83
- [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
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
- ### Tests
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
- ## Documentation
133
+ ## Changelog
106
134
 
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.
135
+ See [`CHANGELOG.md`](CHANGELOG.md) for the version history of the project.
114
136
 
115
137
  ## License
116
138
 
117
- [MIT License](./LICENCE) — Terms of use and distribution of the project.
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
- ## Test Strategy
3
+ ## Layers
4
4
 
5
- * [Testing](architecture/testing.md) — Test strategy and organization.
5
+ ### CLI
6
6
 
7
- ## Documentation
7
+ `lib/r2/cli.rb`
8
8
 
9
- ### Architecture
9
+ Built with `thor`. Acts as a minimal orchestrator:
10
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.
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
- ## Project Structure
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
- 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
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 an image to R2"
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 an image to the configured Cloudflare R2 bucket.
53
- The object key in the bucket will be the name of the given file.
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 an image to the configured Cloudflare R2 bucket.
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: File.basename(file), body: body)
70
- puts "Image uploaded successfully: #{File.basename(file)}"
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 "File deleted successfully: #{file}"
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
- def initialize(config)
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
- raise Errors::ConfigurationError,
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
- raise Errors::BucketNotFoundError, "Bucket not found: #{@bucket}"
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
- raise Errors::NetworkError, error.message
89
- else
90
- raise Errors::StorageError, error.message
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module R2
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/r2.rb CHANGED
@@ -3,5 +3,6 @@
3
3
  require "r2/cli"
4
4
  require "r2/configuration"
5
5
  require "r2/errors"
6
+ require "r2/logging"
6
7
  require "r2/storage"
7
8
  require "r2/version"
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.0.0
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-08-30 00:00:00.000000000 Z
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
- - LICENCE
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.
@@ -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.
@@ -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
File without changes