Skip to content

Commit 6080757

Browse files
committed
Fix CI: rubocop cleanups, API docs, and ActiveStorage refactors
1 parent 59a81aa commit 6080757

36 files changed

+523
-223
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ group :test do
2222
gem 'rubocop', '~> 1.48'
2323
gem 'vcr', '~> 6.1'
2424
gem 'webmock', '~> 3.18'
25+
gem 'yard', '~> 0.9'
2526
end

lib/active_storage/service/uploadcare_service.rb

Lines changed: 103 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -6,177 +6,133 @@
66
require 'net/http'
77
require 'uri'
88
require 'uploadcare-rails'
9+
require 'active_storage/service/uploadcare_service/helpers'
10+
require 'active_storage/service/uploadcare_service/uuid_mapping'
911

12+
# Namespace for ActiveStorage integration.
1013
module ActiveStorage
11-
class Service::UploadcareService < Service
12-
def initialize(public_key:, secret_key:, public: false, **options)
13-
@public = public
14-
@key_uuid_map = {}
15-
@client_config = Uploadcare::Rails.client_config(public_key: public_key, secret_key: secret_key, **options)
16-
end
17-
18-
def upload(key, io, checksum: nil, custom_metadata: {}, **)
19-
instrument :upload, key: key, checksum: checksum do
20-
uploaded_file = Uploadcare::Uploader.upload_file(
21-
file: io,
22-
config: @client_config,
23-
store: true,
24-
metadata: custom_metadata
25-
)
26-
27-
persist_uuid_mapping(key, uploaded_file.uuid)
28-
ensure_integrity(io, checksum) if checksum
14+
# Service implementations for ActiveStorage.
15+
class Service
16+
# ActiveStorage backend implementation for Uploadcare.
17+
class UploadcareService < Service
18+
include Helpers
19+
include UuidMapping
20+
21+
def initialize(public_key:, secret_key:, public: false, **options)
22+
super()
23+
@public = public
24+
@key_uuid_map = {}
25+
@client_config = Uploadcare::Rails.client_config(public_key: public_key, secret_key: secret_key, **options)
2926
end
30-
end
31-
32-
def download(key, &block)
33-
uuid = uuid_for!(key)
34-
file = Uploadcare::File.info(uuid: uuid, config: @client_config)
35-
download_url = file.original_file_url || file.cdn_url
3627

37-
if block_given?
38-
instrument :streaming_download, key: key do
39-
request(download_url) { |response| response.read_body { |chunk| yield chunk } }
40-
end
41-
else
42-
instrument :download, key: key do
43-
request(download_url, &:body)
28+
# Uploads a blob to Uploadcare and persists key-to-uuid mapping.
29+
# @param key [String]
30+
# @param io [IO]
31+
# @param checksum [String, nil]
32+
# @param custom_metadata [Hash]
33+
# @return [void]
34+
def upload(key, io, checksum: nil, custom_metadata: {}, **)
35+
instrument :upload, key: key, checksum: checksum do
36+
uploaded_file = Uploadcare::Uploader.upload_file(
37+
file: io,
38+
config: @client_config,
39+
store: true,
40+
metadata: custom_metadata
41+
)
42+
43+
persist_uuid_mapping(key, uploaded_file.uuid)
44+
ensure_integrity(io, checksum) if checksum
4445
end
4546
end
46-
rescue Uploadcare::Exception::NotFoundError
47-
raise ActiveStorage::FileNotFoundError
48-
end
49-
50-
def download_chunk(key, range)
51-
uuid = uuid_for!(key)
52-
file = Uploadcare::File.info(uuid: uuid, config: @client_config)
53-
download_url = file.original_file_url || file.cdn_url
5447

55-
instrument :download_chunk, key: key, range: range do
56-
request(download_url, range: range, &:body)
48+
# Downloads a blob from Uploadcare.
49+
# @param key [String]
50+
# @yield [chunk] optional streaming chunk callback
51+
# @return [String, void]
52+
def download(key, &block)
53+
uuid = uuid_for!(key)
54+
download_url = file_download_url(uuid)
55+
if block_given?
56+
stream_download(key, download_url, &block)
57+
else
58+
instrument(:download, key: key) { request(download_url, &:body) }
59+
end
60+
rescue Uploadcare::Exception::NotFoundError
61+
raise ActiveStorage::FileNotFoundError
5762
end
58-
rescue Uploadcare::Exception::NotFoundError
59-
raise ActiveStorage::FileNotFoundError
60-
end
6163

62-
def delete(key)
63-
uuid = uuid_for(key)
64-
return unless uuid
65-
66-
instrument :delete, key: key do
67-
Uploadcare::File.new({ uuid: uuid }, @client_config).delete
68-
end
69-
rescue Uploadcare::Exception::NotFoundError
70-
nil
71-
end
64+
# Downloads a specific byte range from a blob.
65+
# @param key [String]
66+
# @param range [Range]
67+
# @return [String]
68+
def download_chunk(key, range)
69+
uuid = uuid_for!(key)
70+
download_url = file_download_url(uuid)
7271

73-
def delete_prefixed(prefix)
74-
instrument :delete_prefixed, prefix: prefix do
75-
keys_for_prefix(prefix).each { |key| delete(key) }
72+
instrument :download_chunk, key: key, range: range do
73+
request(download_url, range: range, &:body)
74+
end
75+
rescue Uploadcare::Exception::NotFoundError
76+
raise ActiveStorage::FileNotFoundError
7677
end
77-
end
7878

79-
def exist?(key)
80-
instrument :exist, key: key do |payload|
79+
# Deletes a blob from Uploadcare if mapping exists.
80+
# @param key [String]
81+
# @return [void]
82+
def delete(key)
8183
uuid = uuid_for(key)
82-
answer = if uuid
83-
Uploadcare::File.info(uuid: uuid, config: @client_config)
84-
true
85-
else
86-
false
87-
end
88-
payload[:exist] = answer
89-
answer
84+
return unless uuid
85+
86+
instrument :delete, key: key do
87+
Uploadcare::File.new({ uuid: uuid }, @client_config).delete
88+
end
9089
rescue Uploadcare::Exception::NotFoundError
91-
payload[:exist] = false
92-
false
90+
nil
9391
end
94-
end
95-
96-
def url_for_direct_upload(*)
97-
raise NotImplementedError, 'Direct uploads are not supported for UploadcareService yet'
98-
end
99-
100-
def headers_for_direct_upload(*)
101-
{}
102-
end
103-
104-
private
105-
106-
def private_url(key, **)
107-
uuid = uuid_for!(key)
108-
Uploadcare::File.new({ uuid: uuid }, @client_config).cdn_url
109-
end
110-
111-
def public_url(key, **)
112-
uuid = uuid_for!(key)
113-
Uploadcare::File.new({ uuid: uuid }, @client_config).cdn_url
114-
end
115-
116-
def request(url, range: nil)
117-
uri = URI.parse(url)
118-
request = Net::HTTP::Get.new(uri)
119-
request['Range'] = "bytes=#{range.begin}-#{range.end}" if range
12092

121-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
122-
response = http.request(request)
123-
raise ActiveStorage::FileNotFoundError if response.is_a?(Net::HTTPNotFound)
124-
125-
yield response
93+
# Deletes all blobs whose key starts with prefix.
94+
# @param prefix [String]
95+
# @return [void]
96+
def delete_prefixed(prefix)
97+
instrument :delete_prefixed, prefix: prefix do
98+
keys_for_prefix(prefix).each { |key| delete(key) }
99+
end
126100
end
127-
end
128-
129-
def ensure_integrity(io, checksum)
130-
io.rewind
131-
actual_checksum = Base64.strict_encode64(Digest::MD5.digest(io.read))
132-
raise ActiveStorage::IntegrityError unless actual_checksum == checksum
133-
ensure
134-
io.rewind
135-
end
136-
137-
def uuid_for!(key)
138-
uuid_for(key) || raise(ActiveStorage::FileNotFoundError)
139-
end
140-
141-
def uuid_for(key)
142-
@key_uuid_map[key] || uuid_from_blob(key) || key_if_uuid(key)
143-
end
144101

145-
def persist_uuid_mapping(key, uuid)
146-
@key_uuid_map[key] = uuid
147-
persist_uuid_to_blob(key, uuid)
148-
end
149-
150-
def uuid_from_blob(key)
151-
return unless defined?(ActiveStorage::Blob)
152-
153-
blob = ActiveStorage::Blob.find_by(key: key)
154-
blob&.metadata&.[]('uploadcare_uuid')
155-
end
156-
157-
def persist_uuid_to_blob(key, uuid)
158-
return unless defined?(ActiveStorage::Blob)
102+
# Checks whether mapped blob exists in Uploadcare.
103+
# @param key [String]
104+
# @return [Boolean]
105+
def exist?(key)
106+
instrument :exist, key: key do |payload|
107+
payload[:exist] = file_exists?(key)
108+
rescue Uploadcare::Exception::NotFoundError
109+
payload[:exist] = false
110+
end
111+
end
159112

160-
blob = ActiveStorage::Blob.find_by(key: key)
161-
return unless blob
113+
# Direct uploads are not supported.
114+
# @raise [NotImplementedError]
115+
def url_for_direct_upload(*)
116+
raise NotImplementedError, 'Direct uploads are not supported for UploadcareService yet'
117+
end
162118

163-
metadata = (blob.metadata || {}).dup
164-
return if metadata['uploadcare_uuid'] == uuid
119+
# Returns direct upload headers.
120+
# @return [Hash]
121+
def headers_for_direct_upload(*)
122+
{}
123+
end
165124

166-
metadata['uploadcare_uuid'] = uuid
167-
blob.update!(metadata: metadata)
168-
end
125+
private
169126

170-
def keys_for_prefix(prefix)
171-
if defined?(ActiveStorage::Blob)
172-
ActiveStorage::Blob.where('key LIKE ?', "#{prefix}%").pluck(:key)
173-
else
174-
@key_uuid_map.keys.select { |key| key.start_with?(prefix) }
127+
def private_url(key, **)
128+
uuid = uuid_for!(key)
129+
Uploadcare::File.new({ uuid: uuid }, @client_config).cdn_url
175130
end
176-
end
177131

178-
def key_if_uuid(key)
179-
key if key.to_s.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
132+
def public_url(key, **)
133+
uuid = uuid_for!(key)
134+
Uploadcare::File.new({ uuid: uuid }, @client_config).cdn_url
135+
end
180136
end
181137
end
182138
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveStorage
4+
# Service implementations for ActiveStorage.
5+
class Service
6+
class UploadcareService < Service
7+
# Shared helper methods for Uploadcare service operations.
8+
module Helpers
9+
private
10+
11+
def stream_download(key, download_url, &block)
12+
instrument :streaming_download, key: key do
13+
request(download_url) { |response| response.read_body(&block) }
14+
end
15+
end
16+
17+
def file_download_url(uuid)
18+
file = Uploadcare::File.info(uuid: uuid, config: @client_config)
19+
file.original_file_url || file.cdn_url
20+
end
21+
22+
def file_exists?(key)
23+
uuid = uuid_for(key)
24+
return false unless uuid
25+
26+
Uploadcare::File.info(uuid: uuid, config: @client_config)
27+
true
28+
end
29+
30+
def request(url, range: nil)
31+
uri = URI.parse(url)
32+
request = Net::HTTP::Get.new(uri)
33+
request['Range'] = "bytes=#{range.begin}-#{range.end}" if range
34+
35+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
36+
response = http.request(request)
37+
raise ActiveStorage::FileNotFoundError if response.is_a?(Net::HTTPNotFound)
38+
39+
yield response
40+
end
41+
end
42+
43+
def ensure_integrity(io, checksum)
44+
io.rewind
45+
actual_checksum = Base64.strict_encode64(Digest::MD5.digest(io.read))
46+
raise ActiveStorage::IntegrityError unless actual_checksum == checksum
47+
ensure
48+
io.rewind
49+
end
50+
end
51+
end
52+
end
53+
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveStorage
4+
class Service
5+
class UploadcareService < Service
6+
# UUID lookup and persistence helpers for key-to-file mapping.
7+
module UuidMapping
8+
private
9+
10+
def uuid_for!(key)
11+
uuid_for(key) || raise(ActiveStorage::FileNotFoundError)
12+
end
13+
14+
def uuid_for(key)
15+
@key_uuid_map[key] || uuid_from_blob(key) || key_if_uuid(key)
16+
end
17+
18+
def persist_uuid_mapping(key, uuid)
19+
@key_uuid_map[key] = uuid
20+
persist_uuid_to_blob(key, uuid)
21+
end
22+
23+
def uuid_from_blob(key)
24+
return unless defined?(ActiveStorage::Blob)
25+
26+
blob = ActiveStorage::Blob.find_by(key: key)
27+
blob&.metadata&.[]('uploadcare_uuid')
28+
end
29+
30+
def persist_uuid_to_blob(key, uuid)
31+
return unless defined?(ActiveStorage::Blob)
32+
33+
blob = ActiveStorage::Blob.find_by(key: key)
34+
return unless blob
35+
36+
metadata = (blob.metadata || {}).dup
37+
return if metadata['uploadcare_uuid'] == uuid
38+
39+
metadata['uploadcare_uuid'] = uuid
40+
blob.update!(metadata: metadata)
41+
end
42+
43+
def keys_for_prefix(prefix)
44+
return ActiveStorage::Blob.where('key LIKE ?', "#{prefix}%").pluck(:key) if defined?(ActiveStorage::Blob)
45+
46+
@key_uuid_map.keys.select { |key| key.start_with?(prefix) }
47+
end
48+
49+
def key_if_uuid(key)
50+
key if key.to_s.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
51+
end
52+
end
53+
end
54+
end
55+
end

0 commit comments

Comments
 (0)