Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit 2c5e2fc

Browse files
dishwadanand2312
andauthored
feat: created functions to get multiple signed URLs. (#105)
* feat: created functions to get multiple signed URLs. * feat: Fixed optional params. Handling auth token issue #73 in separate PR. * feat: remove sync code as it will be generated by unasync. * chore: generate sync client --------- Co-authored-by: Alexander Leonov <[email protected]> Co-authored-by: anand2312 <[email protected]>
1 parent 7e86450 commit 2c5e2fc

File tree

5 files changed

+227
-9
lines changed

5 files changed

+227
-9
lines changed

storage3/_async/file_api.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ..types import (
1313
BaseBucket,
1414
CreateSignedURLOptions,
15+
CreateSignedURLsOptions,
1516
FileOptions,
1617
ListBucketFilesOptions,
1718
RequestMethod,
@@ -62,19 +63,56 @@ async def create_signed_url(
6263
file path to be downloaded, including the current file name.
6364
expires_in
6465
number of seconds until the signed URL expires.
66+
options
67+
options to be passed for downloading or transforming the file.
6568
"""
69+
json = {"expiresIn": str(expires_in)}
70+
if options.get("download"):
71+
json.update({"download": options["download"]})
72+
if options.get("transform"):
73+
json.update({"transform": options["transform"]})
74+
6675
path = self._get_final_path(path)
6776
response = await self._request(
6877
"POST",
6978
f"/object/sign/{path}",
70-
json={"expiresIn": str(expires_in)},
79+
json=json,
7180
)
7281
data = response.json()
7382
data[
7483
"signedURL"
7584
] = f"{self._client.base_url}{cast(str, data['signedURL']).lstrip('/')}"
7685
return data
7786

87+
async def create_signed_urls(
88+
self, paths: list[str], expires_in: int, options: CreateSignedURLsOptions = {}
89+
) -> list[dict[str, str]]:
90+
"""
91+
Parameters
92+
----------
93+
path
94+
file path to be downloaded, including the current file name.
95+
expires_in
96+
number of seconds until the signed URL expires.
97+
options
98+
options to be passed for downloading the file.
99+
"""
100+
json = {"paths": paths, "expiresIn": str(expires_in)}
101+
if options.get("download"):
102+
json.update({"download": options["download"]})
103+
104+
response = await self._request(
105+
"POST",
106+
f"/object/sign/{self.id}",
107+
json=json,
108+
)
109+
data = response.json()
110+
for item in data:
111+
item[
112+
"signedURL"
113+
] = f"{self._client.base_url}{cast(str, item['signedURL']).lstrip('/')}"
114+
return data
115+
78116
async def get_public_url(self, path: str, options: TransformOptions = {}) -> str:
79117
"""
80118
Parameters

storage3/_sync/file_api.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ..types import (
1313
BaseBucket,
1414
CreateSignedURLOptions,
15+
CreateSignedURLsOptions,
1516
FileOptions,
1617
ListBucketFilesOptions,
1718
RequestMethod,
@@ -62,19 +63,56 @@ def create_signed_url(
6263
file path to be downloaded, including the current file name.
6364
expires_in
6465
number of seconds until the signed URL expires.
66+
options
67+
options to be passed for downloading or transforming the file.
6568
"""
69+
json = {"expiresIn": str(expires_in)}
70+
if options.get("download"):
71+
json.update({"download": options["download"]})
72+
if options.get("transform"):
73+
json.update({"transform": options["transform"]})
74+
6675
path = self._get_final_path(path)
6776
response = self._request(
6877
"POST",
6978
f"/object/sign/{path}",
70-
json={"expiresIn": str(expires_in)},
79+
json=json,
7180
)
7281
data = response.json()
7382
data[
7483
"signedURL"
7584
] = f"{self._client.base_url}{cast(str, data['signedURL']).lstrip('/')}"
7685
return data
7786

87+
def create_signed_urls(
88+
self, paths: list[str], expires_in: int, options: CreateSignedURLsOptions = {}
89+
) -> list[dict[str, str]]:
90+
"""
91+
Parameters
92+
----------
93+
path
94+
file path to be downloaded, including the current file name.
95+
expires_in
96+
number of seconds until the signed URL expires.
97+
options
98+
options to be passed for downloading the file.
99+
"""
100+
json = {"paths": paths, "expiresIn": str(expires_in)}
101+
if options.get("download"):
102+
json.update({"download": options["download"]})
103+
104+
response = self._request(
105+
"POST",
106+
f"/object/sign/{self.id}",
107+
json=json,
108+
)
109+
data = response.json()
110+
for item in data:
111+
item[
112+
"signedURL"
113+
] = f"{self._client.base_url}{cast(str, item['signedURL']).lstrip('/')}"
114+
return data
115+
78116
def get_public_url(self, path: str, options: TransformOptions = {}) -> str:
79117
"""
80118
Parameters

storage3/types.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,21 @@ class ListBucketFilesOptions(TypedDict):
4848
sortBy: _sortByType
4949

5050

51-
class TransformOptions(TypedDict):
52-
height: Optional[float]
53-
width: Optional[float]
54-
resize: Optional[Union[Literal["cover"], Literal["contain"], Literal["fill"]]]
51+
class TransformOptions(TypedDict, total=False):
52+
height: int
53+
width: int
54+
resize: Literal["cover", "contain", "fill"]
55+
format: Literal["origin", "avif"]
56+
quality: int
5557

5658

57-
class CreateSignedURLOptions(TypedDict):
58-
download: Optional[Union[str, bool]]
59-
transform: Optional[TransformOptions]
59+
class CreateSignedURLOptions(TypedDict, total=False):
60+
download: Union[str, bool]
61+
transform: TransformOptions
62+
63+
64+
class CreateSignedURLsOptions(TypedDict):
65+
download: Union[str, bool]
6066

6167

6268
FileOptions = TypedDict(

tests/_async/test_client.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,54 @@ def file(tmp_path: Path, uuid_factory: Callable[[], str]) -> FileForTesting:
156156
)
157157

158158

159+
@pytest.fixture
160+
def multi_file(tmp_path: Path, uuid_factory: Callable[[], str]) -> list[FileForTesting]:
161+
"""Creates multiple test files (same content, same bucket/folder path, different file names)"""
162+
file_name_1 = "test_image_1.svg"
163+
file_name_2 = "test_image_2.svg"
164+
file_content = (
165+
b'<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg"> '
166+
b'<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 '
167+
b'40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/> '
168+
b'<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 '
169+
b'40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" '
170+
b'fill-opacity="0.2"/> <path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 '
171+
b'72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/> <defs>'
172+
b'<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295"'
173+
b'gradientUnits="userSpaceOnUse"> <stop stop-color="#249361"/> <stop offset="1" stop-color="#3ECF8E"/> '
174+
b'</linearGradient> <linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" '
175+
b'gradientUnits="userSpaceOnUse"> <stop/> <stop offset="1" stop-opacity="0"/> </linearGradient> </defs> </svg>'
176+
)
177+
bucket_folder = uuid_factory()
178+
bucket_path_1 = f"{bucket_folder}/{file_name_1}"
179+
bucket_path_2 = f"{bucket_folder}/{file_name_2}"
180+
file_path_1 = tmp_path / file_name_1
181+
file_path_2 = tmp_path / file_name_2
182+
with open(file_path_1, "wb") as f:
183+
f.write(file_content)
184+
with open(file_path_2, "wb") as f:
185+
f.write(file_content)
186+
187+
return [
188+
FileForTesting(
189+
name=file_name_1,
190+
local_path=str(file_path_1),
191+
bucket_folder=bucket_folder,
192+
bucket_path=bucket_path_1,
193+
mime_type="image/svg+xml",
194+
file_content=file_content,
195+
),
196+
FileForTesting(
197+
name=file_name_2,
198+
local_path=str(file_path_2),
199+
bucket_folder=bucket_folder,
200+
bucket_path=bucket_path_2,
201+
mime_type="image/svg+xml",
202+
file_content=file_content,
203+
),
204+
]
205+
206+
159207
# TODO: Test create_bucket, delete_bucket, empty_bucket, list_buckets, fileAPI.list before upload test
160208

161209

@@ -194,6 +242,26 @@ async def test_client_create_signed_url(
194242
assert response.content == file.file_content
195243

196244

245+
async def test_client_create_signed_urls(
246+
storage_file_client: AsyncBucketProxy, multi_file: list[FileForTesting]
247+
) -> None:
248+
"""Ensure we can create signed urls for files in a bucket"""
249+
paths = []
250+
for file in multi_file:
251+
paths.append(file.bucket_path)
252+
await storage_file_client.upload(
253+
file.bucket_path, file.local_path, {"content-type": file.mime_type}
254+
)
255+
256+
signed_urls = await storage_file_client.create_signed_urls(paths, 10)
257+
258+
async with HttpxClient() as client:
259+
for url in signed_urls:
260+
response = await client.get(url["signedURL"])
261+
response.raise_for_status()
262+
assert response.content == multi_file[0].file_content
263+
264+
197265
async def test_client_get_public_url(
198266
storage_file_client_public: AsyncBucketProxy, file: FileForTesting
199267
) -> None:

tests/_sync/test_client.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,54 @@ def file(tmp_path: Path, uuid_factory: Callable[[], str]) -> FileForTesting:
154154
)
155155

156156

157+
@pytest.fixture
158+
def multi_file(tmp_path: Path, uuid_factory: Callable[[], str]) -> list[FileForTesting]:
159+
"""Creates multiple test files (same content, same bucket/folder path, different file names)"""
160+
file_name_1 = "test_image_1.svg"
161+
file_name_2 = "test_image_2.svg"
162+
file_content = (
163+
b'<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg"> '
164+
b'<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 '
165+
b'40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/> '
166+
b'<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 '
167+
b'40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" '
168+
b'fill-opacity="0.2"/> <path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 '
169+
b'72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/> <defs>'
170+
b'<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295"'
171+
b'gradientUnits="userSpaceOnUse"> <stop stop-color="#249361"/> <stop offset="1" stop-color="#3ECF8E"/> '
172+
b'</linearGradient> <linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" '
173+
b'gradientUnits="userSpaceOnUse"> <stop/> <stop offset="1" stop-opacity="0"/> </linearGradient> </defs> </svg>'
174+
)
175+
bucket_folder = uuid_factory()
176+
bucket_path_1 = f"{bucket_folder}/{file_name_1}"
177+
bucket_path_2 = f"{bucket_folder}/{file_name_2}"
178+
file_path_1 = tmp_path / file_name_1
179+
file_path_2 = tmp_path / file_name_2
180+
with open(file_path_1, "wb") as f:
181+
f.write(file_content)
182+
with open(file_path_2, "wb") as f:
183+
f.write(file_content)
184+
185+
return [
186+
FileForTesting(
187+
name=file_name_1,
188+
local_path=str(file_path_1),
189+
bucket_folder=bucket_folder,
190+
bucket_path=bucket_path_1,
191+
mime_type="image/svg+xml",
192+
file_content=file_content,
193+
),
194+
FileForTesting(
195+
name=file_name_2,
196+
local_path=str(file_path_2),
197+
bucket_folder=bucket_folder,
198+
bucket_path=bucket_path_2,
199+
mime_type="image/svg+xml",
200+
file_content=file_content,
201+
),
202+
]
203+
204+
157205
# TODO: Test create_bucket, delete_bucket, empty_bucket, list_buckets, fileAPI.list before upload test
158206

159207

@@ -192,6 +240,26 @@ def test_client_create_signed_url(
192240
assert response.content == file.file_content
193241

194242

243+
def test_client_create_signed_urls(
244+
storage_file_client: SyncBucketProxy, multi_file: list[FileForTesting]
245+
) -> None:
246+
"""Ensure we can create signed urls for files in a bucket"""
247+
paths = []
248+
for file in multi_file:
249+
paths.append(file.bucket_path)
250+
storage_file_client.upload(
251+
file.bucket_path, file.local_path, {"content-type": file.mime_type}
252+
)
253+
254+
signed_urls = storage_file_client.create_signed_urls(paths, 10)
255+
256+
with HttpxClient() as client:
257+
for url in signed_urls:
258+
response = client.get(url["signedURL"])
259+
response.raise_for_status()
260+
assert response.content == multi_file[0].file_content
261+
262+
195263
def test_client_get_public_url(
196264
storage_file_client_public: SyncBucketProxy, file: FileForTesting
197265
) -> None:

0 commit comments

Comments
 (0)