Skip to content

Commit 39e1114

Browse files
vipulnswardclaude
andcommitted
Add missing APIs from main branch to rewrite architecture
This commit ports essential functionality from the main branch that was missing in the rewrite-part-1 branch, maintaining the new ActiveModel-style architecture: Upload API Implementation: - Add upload_client.rb base class for Upload API operations - Implement uploader_client.rb for file uploads, URL uploads, and file info - Add multipart_upload_client.rb for large file uploads with chunking - Create Uploader resource class with ActiveModel-style interface CDN & Security Features: - Add CnameGenerator for subdomain-based CDN optimization - Implement cdn_url methods in File and Group resources - Add signed URL generators (base class and Akamai implementation) - Update Configuration with CDN settings (cdn_base, use_subdomains, cdn_base_postfix) API Enhancements: - Add unified Api entry point class for backwards compatibility - Ensure batch operations (batch_store, batch_delete) are available - Verify copy operations (local_copy, remote_copy) are implemented All implementations follow the modernized pattern established in PR #177, providing a clean resource-based API while maintaining feature parity. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ccf41eb commit 39e1114

File tree

12 files changed

+579
-2
lines changed

12 files changed

+579
-2
lines changed

lib/uploadcare.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,9 @@ def configuration
2424
def eager_load!
2525
@loader.eager_load
2626
end
27+
28+
def api(config = nil)
29+
Api.new(config || configuration)
30+
end
2731
end
2832
end

lib/uploadcare/api.rb

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# frozen_string_literal: true
2+
3+
module Uploadcare
4+
class Api
5+
attr_reader :config
6+
7+
def initialize(config = nil)
8+
@config = config || Uploadcare.configuration
9+
end
10+
11+
# File operations
12+
def file(uuid)
13+
File.new({ uuid: uuid }, config).info
14+
end
15+
16+
def file_list(options = {})
17+
File.list(options, config)
18+
end
19+
20+
def store_file(uuid)
21+
File.new({ uuid: uuid }, config).store
22+
end
23+
24+
def delete_file(uuid)
25+
File.new({ uuid: uuid }, config).delete
26+
end
27+
28+
def batch_store(uuids)
29+
File.batch_store(uuids, config)
30+
end
31+
32+
def batch_delete(uuids)
33+
File.batch_delete(uuids, config)
34+
end
35+
36+
def local_copy(source, options = {})
37+
File.local_copy(source, options, config)
38+
end
39+
40+
def remote_copy(source, target, options = {})
41+
File.remote_copy(source, target, options, config)
42+
end
43+
44+
# Upload operations
45+
def upload(input, options = {})
46+
Uploader.upload(input, options, config)
47+
end
48+
49+
def upload_file(file, options = {})
50+
Uploader.upload_file(file, options, config)
51+
end
52+
53+
def upload_files(files, options = {})
54+
Uploader.upload_files(files, options, config)
55+
end
56+
57+
def upload_from_url(url, options = {})
58+
Uploader.upload_from_url(url, options, config)
59+
end
60+
61+
def check_upload_status(token)
62+
Uploader.check_upload_status(token, config)
63+
end
64+
65+
# Group operations
66+
def group(uuid)
67+
Group.new({ id: uuid }, config).info
68+
end
69+
70+
def group_list(options = {})
71+
Group.list(options, config)
72+
end
73+
74+
def create_group(files, options = {})
75+
Group.create(files, options, config)
76+
end
77+
78+
def store_group(uuid)
79+
Group.new({ id: uuid }, config).store
80+
end
81+
82+
def delete_group(uuid)
83+
Group.new({ id: uuid }, config).delete
84+
end
85+
86+
# Project operations
87+
def project
88+
Project.info(config)
89+
end
90+
91+
# Webhook operations
92+
def create_webhook(target_url, options = {})
93+
Webhook.create({ target_url: target_url }.merge(options), config)
94+
end
95+
96+
def list_webhooks(options = {})
97+
Webhook.list(options, config)
98+
end
99+
100+
def update_webhook(id, options = {})
101+
webhook = Webhook.new({ id: id }, config)
102+
webhook.update(options)
103+
end
104+
105+
def delete_webhook(target_url)
106+
Webhook.delete(target_url, config)
107+
end
108+
109+
# Document conversion
110+
def convert_document(paths, options = {})
111+
DocumentConverter.convert(paths, options, config)
112+
end
113+
114+
def document_conversion_status(token)
115+
DocumentConverter.status(token, config)
116+
end
117+
118+
# Video conversion
119+
def convert_video(paths, options = {})
120+
VideoConverter.convert(paths, options, config)
121+
end
122+
123+
def video_conversion_status(token)
124+
VideoConverter.status(token, config)
125+
end
126+
127+
# Add-ons operations
128+
def execute_addon(addon_name, target, options = {})
129+
AddOns.execute(addon_name, target, options, config)
130+
end
131+
132+
def check_addon_status(addon_name, request_id)
133+
AddOns.status(addon_name, request_id, config)
134+
end
135+
136+
# File metadata operations
137+
def file_metadata(uuid)
138+
FileMetadata.index(uuid, config)
139+
end
140+
141+
def get_file_metadata(uuid, key)
142+
FileMetadata.show(uuid, key, config)
143+
end
144+
145+
def update_file_metadata(uuid, key, value)
146+
FileMetadata.update(uuid, key, value, config)
147+
end
148+
149+
def delete_file_metadata(uuid, key)
150+
FileMetadata.delete(uuid, key, config)
151+
end
152+
end
153+
end
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
require 'net/http'
4+
5+
module Uploadcare
6+
class MultipartUploadClient < UploadClient
7+
CHUNK_SIZE = 5 * 1024 * 1024 # 5MB chunks
8+
9+
def start(filename, size, content_type = 'application/octet-stream', options = {})
10+
params = {
11+
filename: filename,
12+
size: size,
13+
content_type: content_type,
14+
UPLOADCARE_STORE: options[:store] || 'auto'
15+
}
16+
17+
if options[:metadata]
18+
options[:metadata].each do |key, value|
19+
params["metadata[#{key}]"] = value
20+
end
21+
end
22+
23+
execute_request(:post, '/multipart/start/', params)
24+
end
25+
26+
def upload_chunk(file_path, upload_data)
27+
File.open(file_path, 'rb') do |file|
28+
upload_data['parts'].each do |part|
29+
file.seek(part['start_offset'])
30+
chunk = file.read(part['end_offset'] - part['start_offset'])
31+
32+
upload_part_to_s3(part['url'], chunk)
33+
end
34+
end
35+
end
36+
37+
def complete(uuid)
38+
execute_request(:post, '/multipart/complete/', { uuid: uuid })
39+
end
40+
41+
def upload_file(file_path, options = {})
42+
file_size = File.size(file_path)
43+
filename = options[:filename] || File.basename(file_path)
44+
45+
# Start multipart upload
46+
upload_data = start(filename, file_size, 'application/octet-stream', options)
47+
48+
# Upload chunks
49+
upload_chunk(file_path, upload_data)
50+
51+
# Complete upload
52+
complete(upload_data['uuid'])
53+
end
54+
55+
private
56+
57+
def upload_part_to_s3(presigned_url, chunk)
58+
uri = URI(presigned_url)
59+
http = Net::HTTP.new(uri.host, uri.port)
60+
http.use_ssl = true
61+
62+
request = Net::HTTP::Put.new(uri)
63+
request.body = chunk
64+
request['Content-Type'] = 'application/octet-stream'
65+
66+
response = http.request(request)
67+
68+
unless response.is_a?(Net::HTTPSuccess)
69+
raise Uploadcare::RequestError, "Failed to upload chunk: #{response.code}"
70+
end
71+
end
72+
end
73+
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
module Uploadcare
4+
class UploadClient
5+
BASE_URL = 'https://upload.uploadcare.com'
6+
7+
def initialize(config = Uploadcare.configuration)
8+
@config = config
9+
end
10+
11+
private
12+
13+
attr_reader :config
14+
15+
def connection
16+
@connection ||= Faraday.new(url: BASE_URL) do |faraday|
17+
faraday.request :multipart
18+
faraday.request :url_encoded
19+
faraday.response :json, content_type: /\bjson$/
20+
faraday.adapter Faraday.default_adapter
21+
end
22+
end
23+
24+
def execute_request(method, uri, params = {}, headers = {})
25+
params[:pub_key] = config.public_key
26+
headers['User-Agent'] = user_agent
27+
28+
response = connection.send(method, uri, params, headers)
29+
30+
handle_response(response)
31+
rescue Faraday::Error => e
32+
handle_faraday_error(e)
33+
end
34+
35+
def handle_response(response)
36+
if response.success?
37+
response.body
38+
else
39+
raise_upload_error(response)
40+
end
41+
end
42+
43+
def handle_faraday_error(error)
44+
message = error.message
45+
raise Uploadcare::RequestError, "Request failed: #{message}"
46+
end
47+
48+
def raise_upload_error(response)
49+
body = response.body
50+
error_message = if body.is_a?(Hash)
51+
body['error'] || body['detail'] || "Upload failed"
52+
else
53+
"Upload failed with status #{response.status}"
54+
end
55+
56+
raise Uploadcare::RequestError.new(error_message, response.status)
57+
end
58+
59+
def user_agent
60+
"Uploadcare Ruby/#{Uploadcare::VERSION} (Ruby/#{RUBY_VERSION})"
61+
end
62+
end
63+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
module Uploadcare
4+
class UploaderClient < UploadClient
5+
def upload_file(file, options = {})
6+
File.open(file, 'rb') do |file_io|
7+
params = build_upload_params(options)
8+
params[:file] = Faraday::UploadIO.new(file_io, 'application/octet-stream')
9+
10+
execute_request(:post, '/base/', params)
11+
end
12+
end
13+
14+
def upload_files(files, options = {})
15+
results = files.map do |file|
16+
upload_file(file, options)
17+
end
18+
19+
{ files: results }
20+
end
21+
22+
def upload_from_url(url, options = {})
23+
params = build_upload_params(options)
24+
params[:source_url] = url
25+
26+
execute_request(:post, '/from_url/', params)
27+
end
28+
29+
def check_upload_status(token)
30+
execute_request(:get, '/from_url/status/', { token: token })
31+
end
32+
33+
def file_info(uuid)
34+
execute_request(:get, '/info/', { file_id: uuid })
35+
end
36+
37+
private
38+
39+
def build_upload_params(options)
40+
params = {}
41+
42+
params[:store] = options[:store] if options.key?(:store)
43+
params[:filename] = options[:filename] if options[:filename]
44+
params[:check_URL_duplicates] = options[:check_duplicates] if options.key?(:check_duplicates)
45+
params[:save_URL_duplicates] = options[:save_duplicates] if options.key?(:save_duplicates)
46+
47+
if options[:metadata]
48+
options[:metadata].each do |key, value|
49+
params["metadata[#{key}]"] = value
50+
end
51+
end
52+
53+
params
54+
end
55+
end
56+
end

lib/uploadcare/cname_generator.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
require 'digest'
4+
require 'uri'
5+
6+
module Uploadcare
7+
class CnameGenerator
8+
class << self
9+
def generate(public_key)
10+
return nil unless public_key
11+
12+
hash = Digest::SHA256.hexdigest(public_key)
13+
hash.to_i(16).to_s(36)[0, 10]
14+
end
15+
16+
def cdn_base_url(public_key, cdn_base_postfix)
17+
subdomain = generate(public_key)
18+
return cdn_base_postfix unless subdomain
19+
20+
uri = URI.parse(cdn_base_postfix)
21+
uri.host = "#{subdomain}.#{uri.host}"
22+
uri.to_s
23+
end
24+
end
25+
end
26+
end

0 commit comments

Comments
 (0)