Skip to content

Commit dda02cd

Browse files
committed
chore: url generation implemented
1 parent 11f2685 commit dda02cd

File tree

4 files changed

+396
-0
lines changed

4 files changed

+396
-0
lines changed

lib/imagekit/imagekit.rb

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# frozen_string_literal: true
2+
3+
$VERBOSE = nil
4+
5+
require_relative "./resource"
6+
require_relative "./file"
7+
require_relative "./url"
8+
require_relative "./utils/calculation"
9+
10+
module ImageKit
11+
class Error < StandardError
12+
end
13+
14+
# ImageKit class holds each method will be used by user
15+
class ImageKitClient
16+
attr_reader :file
17+
18+
def initialize(private_key, public_key, url_endpoint, transformation_pos = nil, options = nil)
19+
@private_key = private_key
20+
@public_key = public_key
21+
@url_endpoint = url_endpoint
22+
@transformation_position = transformation_pos
23+
@options = options
24+
25+
@ik_req = ImageKitRequest.new(private_key, public_key, url_endpoint)
26+
@file = ImageKitFile.new(@ik_req)
27+
@url_obj = Url.new(@ik_req)
28+
29+
end
30+
31+
def set_ik_request(ik_req)
32+
# setter for imagekit request mainly will be used for
33+
# test
34+
@ik_req = ik_req
35+
end
36+
37+
def url(options)
38+
@url_obj.generate_url(options)
39+
end
40+
41+
def upload_file(file = nil, file_name = nil, options = nil)
42+
# upload file to imagekit server
43+
@file.upload(file, file_name, options)
44+
end
45+
46+
def list_files(options)
47+
# list all files
48+
@file.list(options)
49+
end
50+
51+
def get_file_details(file_identifier)
52+
# Get file detail by file-id or file_url
53+
@file.details(file_identifier)
54+
end
55+
56+
def update_file_details(file_id, options)
57+
# update file details by file id and other options payload
58+
@file.update_details(file_id, options)
59+
end
60+
61+
def delete_file(file_id)
62+
# Delete a file by file-id
63+
@file.delete(file_id)
64+
end
65+
66+
def bulk_file_delete(file_ids)
67+
# Delete file in bulks by list of file id
68+
@file.batch_delete(file_ids)
69+
end
70+
71+
def get_file_metadata(file_id)
72+
# Get metadata of a file by file-id
73+
@file.get_metadata(file_id)
74+
end
75+
76+
def purge_file_cache(file_url)
77+
# Purge cache from ImageKit server by file_url
78+
@file.purge_cache(file_url)
79+
end
80+
81+
def purge_file_cache_status(request_id)
82+
@file.purge_cache_status(request_id.to_s)
83+
end
84+
85+
def get_remote_file_url_metadata(remote_file_url = "")
86+
@file.get_metadata_from_remote_url(remote_file_url)
87+
end
88+
89+
# Get metadata from remote_file_url
90+
# param remote_file_url: url string of remote file
91+
92+
def phash_distance(first, second)
93+
# Get hamming distance between two phash(image hash) to check
94+
# similarity between images
95+
96+
unless first && second
97+
raise ArgumentError, Error::MISSING_PHASH_VALUE
98+
end
99+
hamming_distance(first, second)
100+
end
101+
102+
def get_authentication_parameters(token = nil, expire = nil)
103+
# Get Authentication params
104+
get_authenticated_params(token, expire, @ik_req.private_key)
105+
end
106+
end
107+
end
108+

lib/imagekit/url.rb

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# frozen_string_literal: true
2+
3+
# Url holds url generation method
4+
5+
require "cgi"
6+
require "openssl"
7+
require_relative "utils/formatter"
8+
require_relative "constants/defaults"
9+
require_relative "constants/supported_transformation"
10+
11+
class Url
12+
def initialize(request_obj)
13+
@req_obj = request_obj
14+
end
15+
16+
def generate_url(options)
17+
if options.key? :src
18+
options[:transformation_position] = Default::TRANSFORMATION_POSITION
19+
end
20+
extended_options = extend_url_options(options)
21+
build_url(extended_options)
22+
end
23+
24+
def build_url(options)
25+
# build url from all options
26+
27+
path = options.fetch(:path, "")
28+
src = options.fetch(:src, "")
29+
url_endpoint = options.fetch(:url_endpoint, "")
30+
transformation_position = options[:transformation_position]
31+
32+
unless Default::VALID_TRANSFORMATION_POSITION.include? transformation_position
33+
raise ArgumentError, INVALID_TRANSFORMATION_POS
34+
end
35+
36+
src_param_used_for_url = false
37+
if (src != "") || (transformation_position == Default::QUERY_TRANSFORMATION_POSITION)
38+
src_param_used_for_url = true
39+
end
40+
41+
if path == "" && src == ""
42+
return ""
43+
end
44+
45+
result_url_hash = {'host': "", 'path': "", 'query': ""}
46+
existing_query=nil
47+
if path != ""
48+
parsed_url = URI.parse(path)
49+
existing_query=parsed_url.query
50+
parsed_host = URI(url_endpoint)
51+
result_url_hash[:scheme] = parsed_host.scheme
52+
53+
# making sure single '/' at end
54+
result_url_hash[:host] = parsed_host.host.to_s.chomp("/") + parsed_host.path.chomp("/") + "/"
55+
result_url_hash[:path] = trim_slash(parsed_url.path)
56+
else
57+
parsed_url = URI.parse(src)
58+
existing_query=parsed_url.query
59+
host = parsed_url.host
60+
result_url_hash[:userinfo] = parsed_url.userinfo if parsed_url.userinfo
61+
result_url_hash[:host] = host
62+
result_url_hash[:scheme] = parsed_url.scheme
63+
result_url_hash[:path] = parsed_url.path
64+
src_param_used_for_url = true
65+
end
66+
query_params = {}
67+
if existing_query!=nil
68+
existing_query.split("&").each do |part|
69+
parts=part.split("=")
70+
if parts.length==2
71+
query_params[parts[0]]=parts[1]
72+
else
73+
query_params[parts[0]]=""
74+
end
75+
end
76+
end
77+
options.fetch(:query_parameters, {}).each do |key, value|
78+
query_params[key]=value
79+
end
80+
transformation_str = transformation_to_str(options[:transformation]).chomp("/")
81+
82+
if transformation_str
83+
if (transformation_position == Default::QUERY_TRANSFORMATION_POSITION) || src_param_used_for_url == true
84+
result_url_hash[:query] = "#{Default::TRANSFORMATION_PARAMETER}=#{transformation_str}"
85+
query_params[:tr]=transformation_str
86+
else
87+
result_url_hash[:path] = "#{Default::TRANSFORMATION_PARAMETER}:#{transformation_str}/#{result_url_hash[:path]}"
88+
end
89+
90+
end
91+
92+
result_url_hash[:host] = result_url_hash[:host].to_s.reverse.chomp("/").reverse
93+
result_url_hash[:path] = result_url_hash[:path].chomp("/")
94+
result_url_hash[:scheme] ||= "https"
95+
96+
97+
# Signature String and Timestamp
98+
# We can do this only for URLs that are created using urlEndpoint and path parameter
99+
# because we need to know the endpoint to be able to remove it from the URL to create a signature
100+
# for the remaining. With the src parameter, we would not know the "pattern" in the URL
101+
query_param_arr = []
102+
query_param_arr.push("ik-sdk-version=ruby-"+Imagekit::Sdk::VERSION)
103+
if options[:signed] && !(options[:src])
104+
intermediate_url = result_url_hash.fetch(:scheme, "") + "://" + result_url_hash.fetch(:host, "") + result_url_hash.fetch(:path, "")
105+
if result_url_hash[:query]!=nil && result_url_hash[:query]!=""
106+
intermediate_url += result_url_hash.fetch(:query, "")
107+
end
108+
end
109+
query_params.each do |key, value|
110+
query_param_arr.push(key.to_s + "=" + value.to_s)
111+
end
112+
113+
query_param_str = query_param_arr.join("&")
114+
result_url_hash[:query] = query_param_str
115+
url=hash_to_url(result_url_hash)
116+
if options[:signed]
117+
private_key = options[:private_key]
118+
expire_seconds = options[:expire_seconds]
119+
expire_timestamp = get_signature_timestamp(expire_seconds)
120+
url_signature = get_signature(private_key, url, url_endpoint, expire_timestamp)
121+
query_param_arr.push(Default::SIGNATURE_PARAMETER + "=" + url_signature)
122+
123+
if expire_timestamp && (expire_timestamp != Default::TIMESTAMP)
124+
query_param_arr.push(Default::TIMESTAMP_PARAMETER + "=" + expire_timestamp.to_s)
125+
end
126+
127+
query_param_str = query_param_arr.join("&")
128+
result_url_hash[:query] = query_param_str
129+
130+
url=hash_to_url(result_url_hash)
131+
end
132+
url
133+
end
134+
135+
def transformation_to_str(transformation)
136+
# creates transformation_position string for url
137+
# from transformation dictionary
138+
139+
unless transformation.is_a?(Array)
140+
return ""
141+
end
142+
143+
parsed_transforms = []
144+
(0..(transformation.length - 1)).each do |i|
145+
parsed_transform_step = []
146+
147+
transformation[i].keys.each do |key|
148+
transform_key = SUPPORTED_TRANS.fetch(key, nil)
149+
transform_key ||= key
150+
151+
if transformation[i][key] == "-"
152+
parsed_transform_step.push(transform_key)
153+
else
154+
parsed_transform_step.push("#{transform_key}#{Default::TRANSFORM_KEY_VALUE_DELIMITER}#{transformation[i][key]}")
155+
end
156+
end
157+
parsed_transforms.push(parsed_transform_step.join(Default::TRANSFORM_DELIMITER))
158+
end
159+
parsed_transforms.join(Default::CHAIN_TRANSFORM_DELIMITER)
160+
end
161+
162+
def get_signature_timestamp(seconds)
163+
# this function returns either default time stamp
164+
# or current unix time and expiry seconds to get
165+
# signature time stamp
166+
167+
if seconds.to_i == 0
168+
Default::DEFAULT_TIMESTAMP
169+
else
170+
DateTime.now.strftime("%s").to_i + seconds.to_i
171+
end
172+
end
173+
174+
def get_signature(private_key, url, url_endpoint, expiry_timestamp)
175+
# creates signature(hashed hex key) and returns from
176+
# private_key, url, url_endpoint and expiry_timestamp
177+
if expiry_timestamp==0
178+
expiry_timestamp=Default::DEFAULT_TIMESTAMP
179+
end
180+
if url_endpoint[url_endpoint.length-1]!="/"
181+
url_endpoint+="/"
182+
end
183+
replaced_url=url.gsub(url_endpoint, "")
184+
replaced_url = replaced_url + expiry_timestamp.to_s
185+
OpenSSL::HMAC.hexdigest("SHA1", private_key, replaced_url)
186+
end
187+
188+
def extend_url_options(options)
189+
attr_dict = {"public_key": @req_obj.public_key,
190+
"private_key": @req_obj.private_key,
191+
"url_endpoint": @req_obj.url_endpoint,
192+
"transformation_position": @req_obj.transformation_position, }
193+
# extending url options
194+
attr_dict.merge(options)
195+
end
196+
197+
def hash_to_url(url_hash)
198+
generated_url = url_hash.fetch(:scheme, "") + "://" + url_hash.fetch(:host, "") + url_hash.fetch(:path, "")
199+
if url_hash[:query] != ""
200+
generated_url = generated_url + "?" + url_hash.fetch(:query, "")
201+
return generated_url
202+
end
203+
generated_url
204+
end
205+
206+
def trim_slash(str, both = true)
207+
if str == ""
208+
return ""
209+
end
210+
# remove slash from a string
211+
# if both is not provide trims both slash
212+
# example - '/abc/' returns 'abc'
213+
# if both=false it will only trim end slash
214+
# example - '/abc/' returns '/abc'
215+
# NOTE: IT'S RECOMMENDED TO USE inbuilt .chomp('string you want to remove')
216+
# FOR REMOVING ONLY TRAILING SLASh
217+
if both
218+
str[0].chomp("/") + str[1..-2] + str[-1].chomp("/")
219+
else
220+
str.chomp("/")
221+
end
222+
end
223+
end

lib/imagekit/utils/calculation.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require "date"
2+
require "securerandom"
3+
4+
DEFAULT_TIME_DIFF = 60 * 30
5+
6+
def is_valid_hex(hex_string)
7+
# checks if hexadecimal value is valid or not
8+
/^[[:xdigit:]]+$/ === hex_string
9+
end
10+
11+
def hamming_distance(first, second)
12+
# Calculate Hamming distance between to hex string
13+
unless is_valid_hex(first) && is_valid_hex(second)
14+
raise ArgumentError, "Both argument should be hexadecimal"
15+
end
16+
a = first.to_i(16)
17+
b = second.to_i(16)
18+
(a ^ b).to_s(2).count("1")
19+
end
20+
21+
def get_authenticated_params(token, expire, private_key)
22+
# return authenticated param
23+
default_expire = DateTime.now.strftime("%s").to_i + DEFAULT_TIME_DIFF
24+
token ||= SecureRandom.uuid
25+
26+
auth_params = {'token': token, 'expire': expire, 'signature': ""}
27+
unless private_key
28+
return nil
29+
end
30+
31+
signature = OpenSSL::HMAC.hexdigest("SHA1", private_key, token.to_s + expire.to_s)
32+
auth_params[:token] = token
33+
auth_params[:expire] = expire || default_expire
34+
auth_params[:signature] = signature
35+
auth_params
36+
end

0 commit comments

Comments
 (0)