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

Commit a501d41

Browse files
authored
Merge pull request #49 from supabase-community/j0/add_transformation_bindings
feat: add transformation bindings
2 parents 51de064 + 3e985a9 commit a501d41

File tree

3 files changed

+105
-12
lines changed

3 files changed

+105
-12
lines changed

storage3/_async/file_api.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import urllib.parse
34
from dataclasses import dataclass, field
45
from io import BufferedReader, FileIO
56
from pathlib import Path
@@ -8,7 +9,13 @@
89
from httpx import HTTPError, Response
910

1011
from ..constants import DEFAULT_FILE_OPTIONS, DEFAULT_SEARCH_OPTIONS
11-
from ..types import BaseBucket, ListBucketFilesOptions, RequestMethod
12+
from ..types import (
13+
BaseBucket,
14+
CreateSignedURLOptions,
15+
ListBucketFilesOptions,
16+
RequestMethod,
17+
TransformOptions,
18+
)
1219
from ..utils import AsyncClient, StorageException
1320

1421
__all__ = ["AsyncBucket"]
@@ -44,7 +51,9 @@ async def _request(
4451

4552
return response
4653

47-
async def create_signed_url(self, path: str, expires_in: int) -> dict[str, str]:
54+
async def create_signed_url(
55+
self, path: str, expires_in: int, options: CreateSignedURLOptions = {}
56+
) -> dict[str, str]:
4857
"""
4958
Parameters
5059
----------
@@ -65,15 +74,18 @@ async def create_signed_url(self, path: str, expires_in: int) -> dict[str, str]:
6574
] = f"{self._client.base_url}{cast(str, data['signedURL']).lstrip('/')}"
6675
return data
6776

68-
async def get_public_url(self, path: str) -> str:
77+
async def get_public_url(self, path: str, options: TransformOptions = {}) -> str:
6978
"""
7079
Parameters
7180
----------
7281
path
7382
file path, including the path and file name. For example `folder/image.png`.
7483
"""
84+
render_path = "render/image" if options.get("transform") else "object"
85+
transformation_query = urllib.parse.urlencode(options)
86+
query_string = f"?{transformation_query}" if transformation_query else ""
7587
_path = self._get_final_path(path)
76-
return f"{self._client.base_url}object/public/{_path}"
88+
return f"{self._client.base_url}{render_path}/public/{_path}{query_string}"
7789

7890
async def move(self, from_path: str, to_path: str) -> dict[str, str]:
7991
"""
@@ -97,6 +109,28 @@ async def move(self, from_path: str, to_path: str) -> dict[str, str]:
97109
)
98110
return res.json()
99111

112+
async def copy(self, from_path: str, to_path: str) -> dict[str, str]:
113+
"""
114+
Copies an existing file to a new path in the same bucket.
115+
116+
Parameters
117+
----------
118+
from_path
119+
The original file path, including the current file name. For example `folder/image.png`.
120+
to_path
121+
The new file path, including the new file name. For example `folder/image-copy.png`.
122+
"""
123+
res = await self._request(
124+
"POST",
125+
"/object/copy",
126+
json={
127+
"bucketId": self.id,
128+
"sourceKey": from_path,
129+
"destinationKey": to_path,
130+
},
131+
)
132+
return res.json()
133+
100134
async def remove(self, paths: list) -> dict[str, str]:
101135
"""
102136
Deletes files within the same bucket
@@ -139,7 +173,7 @@ async def list(
139173
)
140174
return response.json()
141175

142-
async def download(self, path: str) -> bytes:
176+
async def download(self, path: str, options: TransformOptions = {}) -> bytes:
143177
"""
144178
Downloads a file.
145179
@@ -148,10 +182,16 @@ async def download(self, path: str) -> bytes:
148182
path
149183
The file path to be downloaded, including the path and file name. For example `folder/image.png`.
150184
"""
185+
render_path = (
186+
"render/image/authenticated" if options.get("transform") else "object"
187+
)
188+
transformation_query = urllib.parse.urlencode(options)
189+
query_string = f"?{transformation_query}" if transformation_query else ""
190+
151191
_path = self._get_final_path(path)
152192
response = await self._request(
153193
"GET",
154-
f"/object/{_path}",
194+
f"{render_path}/{_path}{query_string}",
155195
)
156196
return response.content
157197

storage3/_sync/file_api.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import urllib.parse
34
from dataclasses import dataclass, field
45
from io import BufferedReader, FileIO
56
from pathlib import Path
@@ -8,7 +9,13 @@
89
from httpx import HTTPError, Response
910

1011
from ..constants import DEFAULT_FILE_OPTIONS, DEFAULT_SEARCH_OPTIONS
11-
from ..types import BaseBucket, ListBucketFilesOptions, RequestMethod
12+
from ..types import (
13+
BaseBucket,
14+
CreateSignedURLOptions,
15+
ListBucketFilesOptions,
16+
RequestMethod,
17+
TransformOptions,
18+
)
1219
from ..utils import StorageException, SyncClient
1320

1421
__all__ = ["SyncBucket"]
@@ -44,7 +51,9 @@ def _request(
4451

4552
return response
4653

47-
def create_signed_url(self, path: str, expires_in: int) -> dict[str, str]:
54+
def create_signed_url(
55+
self, path: str, expires_in: int, options: CreateSignedURLOptions = {}
56+
) -> dict[str, str]:
4857
"""
4958
Parameters
5059
----------
@@ -65,15 +74,18 @@ def create_signed_url(self, path: str, expires_in: int) -> dict[str, str]:
6574
] = f"{self._client.base_url}{cast(str, data['signedURL']).lstrip('/')}"
6675
return data
6776

68-
def get_public_url(self, path: str) -> str:
77+
def get_public_url(self, path: str, options: TransformOptions = {}) -> str:
6978
"""
7079
Parameters
7180
----------
7281
path
7382
file path, including the path and file name. For example `folder/image.png`.
7483
"""
84+
render_path = "render/image" if options.get("transform") else "object"
85+
transformation_query = urllib.parse.urlencode(options)
86+
query_string = f"?{transformation_query}" if transformation_query else ""
7587
_path = self._get_final_path(path)
76-
return f"{self._client.base_url}object/public/{_path}"
88+
return f"{self._client.base_url}{render_path}/public/{_path}{query_string}"
7789

7890
def move(self, from_path: str, to_path: str) -> dict[str, str]:
7991
"""
@@ -97,6 +109,28 @@ def move(self, from_path: str, to_path: str) -> dict[str, str]:
97109
)
98110
return res.json()
99111

112+
def copy(self, from_path: str, to_path: str) -> dict[str, str]:
113+
"""
114+
Copies an existing file to a new path in the same bucket.
115+
116+
Parameters
117+
----------
118+
from_path
119+
The original file path, including the current file name. For example `folder/image.png`.
120+
to_path
121+
The new file path, including the new file name. For example `folder/image-copy.png`.
122+
"""
123+
res = self._request(
124+
"POST",
125+
"/object/copy",
126+
json={
127+
"bucketId": self.id,
128+
"sourceKey": from_path,
129+
"destinationKey": to_path,
130+
},
131+
)
132+
return res.json()
133+
100134
def remove(self, paths: list) -> dict[str, str]:
101135
"""
102136
Deletes files within the same bucket
@@ -139,7 +173,7 @@ def list(
139173
)
140174
return response.json()
141175

142-
def download(self, path: str) -> bytes:
176+
def download(self, path: str, options: TransformOptions = {}) -> bytes:
143177
"""
144178
Downloads a file.
145179
@@ -148,10 +182,16 @@ def download(self, path: str) -> bytes:
148182
path
149183
The file path to be downloaded, including the path and file name. For example `folder/image.png`.
150184
"""
185+
render_path = (
186+
"render/image/authenticated" if options.get("transform") else "object"
187+
)
188+
transformation_query = urllib.parse.urlencode(options)
189+
query_string = f"?{transformation_query}" if transformation_query else ""
190+
151191
_path = self._get_final_path(path)
152192
response = self._request(
153193
"GET",
154-
f"/object/{_path}",
194+
f"{render_path}/{_path}{query_string}",
155195
)
156196
return response.content
157197

@@ -188,6 +228,7 @@ def upload(
188228
files = {"file": (filename, open(file, "rb"), headers.pop("content-type"))}
189229

190230
_path = self._get_final_path(path)
231+
191232
return self._request(
192233
"POST",
193234
f"/object/{_path}",

storage3/types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from dataclasses import dataclass
22
from datetime import datetime
3+
from typing import Optional, Union
34

45
import dateutil.parser
56
from typing_extensions import Literal, TypedDict
@@ -35,3 +36,14 @@ class ListBucketFilesOptions(TypedDict):
3536
limit: int
3637
offset: int
3738
sortBy: _sortByType
39+
40+
41+
class TransformOptions(TypedDict):
42+
height: Optional[float]
43+
width: Optional[float]
44+
resize: Optional[Union[Literal["cover"], Literal["contain"], Literal["fill"]]]
45+
46+
47+
class CreateSignedURLOptions(TypedDict):
48+
download: Optional[Union[str, bool]]
49+
transform: Optional[TransformOptions]

0 commit comments

Comments
 (0)