Class: Everywhere::S3
- Inherits:
-
Object
- Object
- Everywhere::S3
- Defined in:
- lib/everywhere/s3.rb
Overview
Minimal S3 client — just the four operations every publish needs
(PUT / HEAD / GET / CopyObject), signed with AWS Signature V4, stdlib only.
Deliberately not aws-sdk-s3: the gem keeps a near-zero runtime dependency footprint, and SigV4 is a stable, testable ~150 lines. Works against any S3-compatible endpoint — AWS, Cloudflare R2, DigitalOcean Spaces, MinIO.
Addressing: virtual-host style for AWS proper, path style for custom endpoints (R2/Spaces/MinIO all accept it; MinIO requires it). Override with force_path_style.
Defined Under Namespace
Modules: Signer
Constant Summary collapse
- Error =
Class.new(StandardError)
- ALGORITHM =
"AWS4-HMAC-SHA256"- UNSIGNED =
"UNSIGNED-PAYLOAD"
Instance Attribute Summary collapse
-
#bucket ⇒ Object
readonly
Returns the value of attribute bucket.
Instance Method Summary collapse
-
#copy_object(source_key, dest_key, content_type: nil, cache_control: nil) ⇒ Object
Server-side copy within the bucket (how the -latest alias is refreshed without re-uploading the artifact).
-
#get_object(key) ⇒ Object
-> body String, or nil when the key doesn't exist.
-
#head_object(key) ⇒ Object
-> { size:, etag: } or nil when the key doesn't exist.
-
#initialize(bucket:, region: "auto", endpoint: nil, force_path_style: nil, access_key_id:, secret_access_key:, session_token: nil) ⇒ S3
constructor
A new instance of S3.
-
#put_object(key, body, content_type: nil, cache_control: nil, sha256: nil, size: nil) ⇒ Object
body: String or IO (IO must respond to #size or be paired with size:).
Constructor Details
#initialize(bucket:, region: "auto", endpoint: nil, force_path_style: nil, access_key_id:, secret_access_key:, session_token: nil) ⇒ S3
Returns a new instance of S3.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/everywhere/s3.rb', line 27 def initialize(bucket:, region: "auto", endpoint: nil, force_path_style: nil, access_key_id:, secret_access_key:, session_token: nil) @bucket = bucket @region = region.to_s.empty? ? "auto" : region @access_key_id = access_key_id @secret_access_key = secret_access_key @session_token = session_token if endpoint && !endpoint.empty? @endpoint = URI(endpoint) @path_style = force_path_style.nil? ? true : force_path_style else @endpoint = URI("https://s3.#{aws_region}.amazonaws.com") @path_style = force_path_style || false end raise Error, "endpoint must be http(s)" unless %w[http https].include?(@endpoint.scheme) end |
Instance Attribute Details
#bucket ⇒ Object (readonly)
Returns the value of attribute bucket.
25 26 27 |
# File 'lib/everywhere/s3.rb', line 25 def bucket @bucket end |
Instance Method Details
#copy_object(source_key, dest_key, content_type: nil, cache_control: nil) ⇒ Object
Server-side copy within the bucket (how the -latest alias is refreshed without re-uploading the artifact).
78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/everywhere/s3.rb', line 78 def copy_object(source_key, dest_key, content_type: nil, cache_control: nil) headers = { "x-amz-copy-source" => "/#{@bucket}/#{uri_encode_path(source_key)}" } if content_type || cache_control headers["x-amz-metadata-directive"] = "REPLACE" headers["content-type"] = content_type if content_type headers["cache-control"] = cache_control if cache_control end res = request(:put, dest_key, headers: headers) # CopyObject reports some failures inside a 200 body. raise Error, "copy #{source_key} -> #{dest_key} failed: #{res.body}" if res.body.to_s.include?("<Error>") true end |
#get_object(key) ⇒ Object
-> body String, or nil when the key doesn't exist.
71 72 73 74 |
# File 'lib/everywhere/s3.rb', line 71 def get_object(key) res = request(:get, key, allow_404: true) res.code == "404" ? nil : res.body end |
#head_object(key) ⇒ Object
-> { size:, etag: } or nil when the key doesn't exist.
63 64 65 66 67 68 |
# File 'lib/everywhere/s3.rb', line 63 def head_object(key) res = request(:head, key, allow_404: true) return nil if res.code == "404" { size: res["content-length"]&.to_i, etag: res["etag"]&.delete('"') } end |
#put_object(key, body, content_type: nil, cache_control: nil, sha256: nil, size: nil) ⇒ Object
body: String or IO (IO must respond to #size or be paired with size:). sha256: precomputed hex payload digest; IOs default to UNSIGNED-PAYLOAD.
47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/everywhere/s3.rb', line 47 def put_object(key, body, content_type: nil, cache_control: nil, sha256: nil, size: nil) headers = {} headers["content-type"] = content_type if content_type headers["cache-control"] = cache_control if cache_control if body.is_a?(String) sha256 ||= OpenSSL::Digest::SHA256.hexdigest(body) headers["content-length"] = body.bytesize.to_s else sha256 ||= UNSIGNED headers["content-length"] = (size || body.size).to_s end request(:put, key, headers: headers, body: body, payload_sha256: sha256) true end |