|
6 | 6 | require 'net/http' |
7 | 7 | require 'uri' |
8 | 8 | require 'uploadcare-rails' |
| 9 | +require 'active_storage/service/uploadcare_service/helpers' |
| 10 | +require 'active_storage/service/uploadcare_service/uuid_mapping' |
9 | 11 |
|
| 12 | +# Namespace for ActiveStorage integration. |
10 | 13 | 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) |
29 | 26 | 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 |
36 | 27 |
|
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 |
44 | 45 | end |
45 | 46 | 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 |
54 | 47 |
|
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 |
57 | 62 | end |
58 | | - rescue Uploadcare::Exception::NotFoundError |
59 | | - raise ActiveStorage::FileNotFoundError |
60 | | - end |
61 | 63 |
|
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) |
72 | 71 |
|
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 |
76 | 77 | end |
77 | | - end |
78 | 78 |
|
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) |
81 | 83 | 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 |
90 | 89 | rescue Uploadcare::Exception::NotFoundError |
91 | | - payload[:exist] = false |
92 | | - false |
| 90 | + nil |
93 | 91 | 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 |
120 | 92 |
|
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 |
126 | 100 | 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 |
144 | 101 |
|
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 |
159 | 112 |
|
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 |
162 | 118 |
|
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 |
165 | 124 |
|
166 | | - metadata['uploadcare_uuid'] = uuid |
167 | | - blob.update!(metadata: metadata) |
168 | | - end |
| 125 | + private |
169 | 126 |
|
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 |
175 | 130 | end |
176 | | - end |
177 | 131 |
|
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 |
180 | 136 | end |
181 | 137 | end |
182 | 138 | end |
0 commit comments