Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions lib/active_storage/service/bunny_service.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
# frozen_string_literal: true

require 'active_support/core_ext/numeric/bytes'
require 'cgi'
require 'bunny_storage_client'

module ActiveStorage
# Wraps the BunnyCDN Storage as an Active Storage service.
# See ActiveStorage::Service for the generic API documentation that applies to all services.
class Service::BunnyService < Service

attr_reader :client, :base_url
attr_reader :client, :base_url, :access_key, :storage_zone, :region

def initialize(access_key:, api_key:, storage_zone:, region:, cdn: false)
@access_key = access_key
@storage_zone = storage_zone
@region = blank_to_nil(region)
@client = BunnyStorageClient.new(access_key, api_key, storage_zone, region)

if cdn
Expand Down Expand Up @@ -73,9 +77,7 @@ def url(key, expires_in:, disposition:, filename:, **options)

def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:, custom_metadata: {})
instrument :url, key: key do |payload|
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
content_type: content_type, content_length: content_length, content_md5: checksum,
metadata: custom_metadata, whitelist_headers: ['content-length'], **upload_options
generated_url = storage_api_url(key)

payload[:url] = generated_url

Expand All @@ -84,20 +86,39 @@ def url_for_direct_upload(key, expires_in:, content_type:, content_length:, chec
end

def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, custom_metadata: {}, **)
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
{
'AccessKey' => access_key,
'Content-Type' => content_type,
**custom_metadata_headers(custom_metadata)
}.compact
end

def download_chunk(key, range_or_options, **options)
actual_range = extract_range_argument(range_or_options, options)

{ 'Content-Type' => content_type, 'Content-MD5' => checksum, 'Content-Disposition' => content_disposition, **custom_metadata_headers(custom_metadata) }
instrument :download_chunk, key: key, range: actual_range do
data = object_for(key).get_file
actual_range ? data.byteslice(actual_range) : data
end
end

private

def blank_to_nil(value)
return nil if value.nil?
return nil if value.respond_to?(:strip) && value.strip.empty?
return nil if value.respond_to?(:empty?) && value.empty?

value
end

def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
# BunnyStorageClient does not natively support this operation yet.
public_url(key)
end

def public_url(key)
File.join(base_url, key)
"#{base_url.to_s.sub(%r{/*$}, '')}/#{key}"
end

def upload_with_single_part(key, io, checksum: nil, content_type: nil, content_disposition: nil, custom_metadata: {})
Expand All @@ -121,5 +142,28 @@ def stream(key, options = {}, &block)
def object_for(key)
client.object(key)
end

def custom_metadata_headers(_metadata)
{}
end

def extract_range_argument(range_or_options, options)
return options[:range] if range_or_options.is_a?(Hash) && range_or_options.empty?
return range_or_options[:range] if range_or_options.is_a?(Hash)

options[:range] || range_or_options
end

def storage_api_url(key)
"https://#{storage_api_host}/#{storage_zone}/#{escaped_key(key)}"
end

def storage_api_host
region ? "#{region}.storage.bunnycdn.com" : 'storage.bunnycdn.com'
end

def escaped_key(key)
key.split('/').map { |part| CGI.escape(part) }.join('/')
end
end
end
2 changes: 1 addition & 1 deletion lib/active_storage_bunny/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module ActiveStorageBunny
VERSION = '1.0.1'.freeze
VERSION = '1.0.6'.freeze
end