Class: R2::Storage
- Inherits:
-
Object
- Object
- R2::Storage
- Defined in:
- lib/r2/storage.rb
Overview
Storage layer responsible for communicating with Cloudflare R2.
Uses the aws-sdk-s3 gem with the S3-compatible endpoint provided by
the application configuration.
Transient network failures are retried with exponential backoff
(see R2::Retry), so brief instabilities do not fail the operation.
Constant Summary collapse
- RETRYABLE_ERRORS =
Error classes considered transient and therefore retried.
[Seahorse::Client::NetworkingError].freeze
Instance Method Summary collapse
-
#delete(key:) ⇒ Object
Deletes an object from the configured bucket.
-
#download(key:, destination:) ⇒ String
Downloads an object from the configured bucket.
-
#exists?(key:) ⇒ Boolean
Checks whether an object exists in the configured bucket.
-
#initialize(config, logger: nil, retry_policy: nil, sleeper: nil) ⇒ Storage
constructor
Initializes the storage with the configured credentials and bucket.
-
#list(prefix: nil) ⇒ Array<String>
Lists the objects stored in the configured bucket.
-
#upload(key:, body:, content_type: nil) ⇒ Object
Uploads an object to the configured bucket.
Constructor Details
#initialize(config, logger: nil, retry_policy: nil, sleeper: nil) ⇒ Storage
Initializes the storage with the configured credentials and bucket.
27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/r2/storage.rb', line 27 def initialize(config, logger: nil, retry_policy: nil, sleeper: nil) @bucket = config.bucket @logger = logger || R2::Logging::NullLogger.new @retry_policy = retry_policy || Retry::Policy.new @sleeper = sleeper @s3 = Aws::S3::Client.new( region: config.region, access_key_id: config.access_key_id, secret_access_key: config.secret_access_key, endpoint: config.endpoint, force_path_style: true ) end |
Instance Method Details
#delete(key:) ⇒ Object
Deletes an object from the configured bucket.
The operation is considered successful when Cloudflare R2 completes the request without raising an error.
82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/r2/storage.rb', line 82 def delete(key:) @logger.debug("Deleting object #{key.inspect} from bucket #{@bucket.inspect}.") with_retries(operation: "delete", key: key) do @s3.delete_object( bucket: @bucket, key: key ) end @logger.debug("Deletion of object #{key.inspect} completed.") nil rescue StandardError => e raise_storage_error(e, operation: "delete", key: key) end |
#download(key:, destination:) ⇒ String
Downloads an object from the configured bucket.
The content is streamed directly to the destination file, so large objects do not need to be fully loaded into memory. Every attempt writes the file from the beginning, avoiding a partially written content when a retry is needed.
133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/r2/storage.rb', line 133 def download(key:, destination:) @logger.debug("Downloading object #{key.inspect} to #{destination.inspect}.") with_retries(operation: "download", key: key) do File.open(destination, "wb") do |file| @s3.get_object(bucket: @bucket, key: key, response_target: file) end end @logger.debug("Download of object #{key.inspect} completed.") destination rescue StandardError => e raise_storage_error(e, operation: "download", key: key, destination: destination) end |
#exists?(key:) ⇒ Boolean
Checks whether an object exists in the configured bucket.
The object metadata is requested instead of the content, so the check is cheap regardless of the object size.
154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/r2/storage.rb', line 154 def exists?(key:) @logger.debug("Checking whether object #{key.inspect} exists in bucket #{@bucket.inspect}.") with_retries(operation: "exists", key: key) do @s3.head_object(bucket: @bucket, key: key) end @logger.debug("Object #{key.inspect} was found in bucket #{@bucket.inspect}.") true rescue Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::NotFound @logger.debug("Object #{key.inspect} was not found in bucket #{@bucket.inspect}.") false rescue StandardError => e raise_storage_error(e, operation: "exists", key: key) end |
#list(prefix: nil) ⇒ Array<String>
Lists the objects stored in the configured bucket.
Every page of the listing is requested, so all the stored objects are returned regardless of the amount of keys in the bucket.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/r2/storage.rb', line 104 def list(prefix: nil) @logger.debug("Listing objects in bucket #{@bucket.inspect} with prefix #{prefix.inspect}.") keys = [] token = nil loop do response = list_page(token, prefix) keys.concat(response.contents.map(&:key)) token = response.next_continuation_token break if token.nil? || token.empty? end @logger.debug("Found #{keys.size} object(s) in bucket #{@bucket.inspect}.") keys rescue StandardError => e raise_storage_error(e, operation: "list") end |
#upload(key:, body:, content_type: nil) ⇒ Object
Uploads an object to the configured bucket.
Receives content already prepared by the layer that uses the storage and delivers it to Cloudflare R2.
The content type is determined from the object key, unless an explicit value is given, so the stored object is served with the correct type instead of the generic binary type.
When the content comes from a stream, it is rewound before every attempt, since a retry must read the content from the beginning.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/r2/storage.rb', line 57 def upload(key:, body:, content_type: nil) type = content_type || ContentType.for(key) @logger.debug("Uploading object #{key.inspect} as #{type.inspect} to bucket #{@bucket.inspect}.") with_retries(operation: "upload", key: key) do body.rewind if body.respond_to?(:rewind) @s3.put_object( bucket: @bucket, key: key, body: body, content_type: type ) end @logger.debug("Upload of object #{key.inspect} completed.") nil rescue StandardError => e raise_storage_error(e, operation: "upload", key: key) end |