Skip to content

Commit a7a3e07

Browse files
author
Saurav Tiwary
committed
Refactor url generation
Handle case when both path and src is provided. Added test for it In case of signed urls, don't provide timestamp parameter when expired_seconds isn't provided. Added test for it. Added clarification comments wherever necessary Removed redundant code and improved overall style Signed-off-by: Saurav Tiwary <[email protected]>
1 parent f71390d commit a7a3e07

File tree

3 files changed

+71
-37
lines changed

3 files changed

+71
-37
lines changed

imagekitio/constants/defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ class Default(enum.Enum):
88
DEFAULT_TRANSFORMATION_POSITION,
99
QUERY_TRANSFORMATION_POSITION,
1010
]
11-
DEFAULT_TIMESTAMP = "9999999999"
11+
DEFAULT_TIMESTAMP = 9999999999
1212
SDK_VERSION = "python-2.2.4"

imagekitio/url.py

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@
2020

2121
SIGNATURE_PARAMETER = "ik-s"
2222
TIMESTAMP_PARAMETER = "ik-t"
23-
DEFAULT_TIMESTAMP = "9999999999"
2423

2524

26-
class Url(object):
25+
class Url:
2726
"""
2827
Url class holds the request and related methods
2928
to generate url(signed and unsigned)
@@ -42,44 +41,47 @@ def build_url(self, options: dict) -> str:
4241
builds url for from all options,
4342
"""
4443

44+
# important to strip the trailing slashes. later logic assumes no trailing slashes.
4545
path = options.get("path", "").strip("/")
4646
src = options.get("src", "").strip("/")
4747
url_endpoint = options.get("url_endpoint", "").strip("/")
4848
transformation_str = self.transformation_to_str(options.get("transformation"))
49-
transformation_position = options.get("transformation_position") or DEFAULT_TRANSFORMATION_POSITION
49+
transformation_position = options.get("transformation_position", DEFAULT_TRANSFORMATION_POSITION)
5050

5151
if transformation_position not in Default.VALID_TRANSFORMATION_POSITION.value:
5252
raise ValueError(ERRORS.INVALID_TRANSFORMATION_POSITION.value)
5353

54-
if (path is "" and src is ""):
54+
if (path == "" and src == ""):
5555
return ""
5656

57-
if src:
58-
temp_url = src.strip("/")
59-
transformation_position = QUERY_TRANSFORMATION_POSITION
60-
else:
57+
# if path is present then it is given priority over src parameter
58+
if path:
6159
if transformation_position == "path":
6260
temp_url = "{}/{}:{}/{}".format(
63-
url_endpoint.strip("/"),
61+
url_endpoint,
6462
TRANSFORMATION_PARAMETER,
6563
transformation_str.strip("/"),
66-
path.strip("/")
64+
path
6765
)
6866
else:
6967
temp_url = "{}/{}".format(
70-
url_endpoint.strip("/"),
71-
path.strip("/")
68+
url_endpoint,
69+
path
7270
)
71+
else:
72+
temp_url = src
73+
# if src parameter is used, then we force transformation position in query
74+
transformation_position = QUERY_TRANSFORMATION_POSITION
7375

74-
url_object = urlparse(temp_url.strip("/"))
76+
url_object = urlparse(temp_url)
7577

7678
query_params = dict(parse_qsl(url_object.query))
7779
query_params.update(options.get("query_parameters", {}))
7880
if transformation_position == QUERY_TRANSFORMATION_POSITION:
7981
query_params.update({"tr": transformation_str})
8082
query_params.update({"ik-sdk-version": Default.SDK_VERSION.value})
8183

82-
# Update query params
84+
# Update query params in the url
8385
url_object = url_object._replace(query=urlencode(query_params))
8486

8587
if options.get("signed"):
@@ -92,39 +94,41 @@ def build_url(self, options: dict) -> str:
9294
url_endpoint=url_endpoint,
9395
expiry_timestamp=expiry_timestamp,
9496
)
95-
query_params.update({TIMESTAMP_PARAMETER: expiry_timestamp, SIGNATURE_PARAMETER: url_signature})
97+
98+
"""
99+
If the expire_seconds parameter is specified then the output URL contains
100+
ik-t parameter (unix timestamp seconds when the URL expires) and
101+
the signature contains the timestamp for computation.
102+
103+
If not present, then no ik-t parameter and the value 9999999999 is used.
104+
"""
105+
if expire_seconds:
106+
query_params.update({TIMESTAMP_PARAMETER: expiry_timestamp, SIGNATURE_PARAMETER: url_signature})
107+
else:
108+
query_params.update({SIGNATURE_PARAMETER: url_signature})
109+
96110
# Update signature related query params
97111
url_object = url_object._replace(query=urlencode(query_params))
98112

99113
return url_object.geturl()
100114

101115
@staticmethod
102-
def get_signature_timestamp(seconds: int = None) -> int:
116+
def get_signature_timestamp(expiry_seconds: int = None) -> int:
103117
"""
104-
this function returns either default time stamp
105-
or current unix time and expiry seconds to get
106-
signature time stamp
118+
this function returns the signature timestamp to be used
119+
with the generated url.
120+
If expiry_seconds is provided, it returns expiry_seconds added
121+
to the current unix time, otherwise the default time stamp
122+
is returned.
107123
"""
108-
if not seconds:
124+
if not expiry_seconds:
109125
return Default.DEFAULT_TIMESTAMP.value
110126
current_timestamp = int(dt.now().strftime("%s"))
111127

112-
return current_timestamp + seconds
113-
114-
@staticmethod
115-
def prepare_dict_for_unparse(url_dict: dict) -> dict:
116-
"""
117-
remove and add required back slash of 'netloc' and 'path'
118-
to parse it properly, urllib.parse.unparse() function can't
119-
create url properly if path doesn't have '/' at the start
120-
"""
121-
url_dict["netloc"] = url_dict["netloc"].rstrip("/")
122-
url_dict["path"] = "/" + url_dict["path"].strip("/")
123-
124-
return url_dict
128+
return current_timestamp + expiry_seconds
125129

126130
@staticmethod
127-
def get_signature(private_key, url, url_endpoint, expiry_timestamp) -> str:
131+
def get_signature(private_key, url, url_endpoint, expiry_timestamp : int) -> str:
128132
""""
129133
create signature(hashed hex key) from
130134
private_key, url, url_endpoint and expiry_timestamp
@@ -133,8 +137,8 @@ def get_signature(private_key, url, url_endpoint, expiry_timestamp) -> str:
133137
if url_endpoint[-1] != '/':
134138
url_endpoint += '/'
135139

136-
if isinstance(expiry_timestamp, int) and expiry_timestamp < 1:
137-
expiry_timestamp = DEFAULT_TIMESTAMP
140+
if expiry_timestamp < 1:
141+
expiry_timestamp = Default.DEFAULT_TIMESTAMP.value
138142

139143
replaced_url = url.replace(url_endpoint, "") + str(expiry_timestamp)
140144

tests/test_generate_url.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,19 @@ def test_generate_url_with_path_and_signed_in_proper_form(self):
194194
url = self.client.url(options)
195195
self.assertIn("ik-s", url)
196196

197+
def test_generate_url_signed_without_expiry_does_not_have_timestamp_parameter(self):
198+
"""
199+
Check query params does not contain timestamp parameter if expire_seconds isn't specified.
200+
"""
201+
options = {
202+
"path": "/test-signed-url.jpg",
203+
"signed": True,
204+
"transformation": [{"width": 100}],
205+
}
206+
207+
url = self.client.url(options)
208+
self.assertNotIn("ik-t", url)
209+
197210
def test_url_with_new_transformation_returns_as_it_is(self):
198211
options = {
199212
"path": "/default-image.jpg",
@@ -370,6 +383,23 @@ def test_url_signed_with_expire_in_seconds(self):
370383
url = self.client.url(options)
371384
self.assertIn("ik-t", url)
372385

386+
def test_generate_url_with_path_and_src_uses_path(self):
387+
"""
388+
In case when both path and src fields are provided, the `path` should be preferred
389+
"""
390+
options = {
391+
"path": "/default-image.jpg",
392+
"src": "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg",
393+
"transformation": [{"height": "300", "width": "400"}],
394+
}
395+
url = self.client.url(options)
396+
self.assertEqual(
397+
url,
398+
"https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg?ik-sdk-version={}".format(
399+
Default.SDK_VERSION.value
400+
),
401+
)
402+
373403
def test_get_signature_with_100_expire_seconds(self):
374404
url = "https://test-domain.com/test-endpoint/tr:w-100/test-signed-url.png"
375405
signature = self.client.url_obj.get_signature(

0 commit comments

Comments
 (0)