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

Commit f756460

Browse files
committed
feat: add upload_to_signed_url
1 parent 1489757 commit f756460

File tree

2 files changed

+134
-8
lines changed

2 files changed

+134
-8
lines changed

storage3/_async/file_api.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,69 @@ async def create_signed_upload_url(self, path: str) -> SignedUploadURL:
7878
"path": path,
7979
}
8080

81+
async def upload_to_signed_url(
82+
self,
83+
path: str,
84+
token: str,
85+
file: Union[BufferedReader, bytes, FileIO, str, Path],
86+
file_options: Optional[FileOptions] = None,
87+
) -> Response:
88+
"""
89+
Upload a file with a token generated from :meth:`.create_signed_url`
90+
91+
Parameters
92+
----------
93+
path
94+
The file path, including the file name
95+
token
96+
The token generated from :meth:`.create_signed_url`
97+
file
98+
The file contents or a file-like object to upload
99+
file_options
100+
Additional options for the uploaded file
101+
"""
102+
_path = self._get_final_path(path)
103+
_url = urllib.parse.urlparse(f"/object/upload/sign/{_path}")
104+
query_params = urllib.parse.urlencode({"token": token})
105+
final_url = f"{_url.geturl()}?{query_params}"
106+
107+
if file_options is None:
108+
file_options = {}
109+
110+
cache_control = file_options.get("cache-control")
111+
if cache_control:
112+
file_options["cache-control"] = f"max-age={cache_control}"
113+
114+
headers = {
115+
**self._client.headers,
116+
**DEFAULT_FILE_OPTIONS,
117+
**file_options,
118+
}
119+
filename = path.rsplit("/", maxsplit=1)[-1]
120+
121+
if (
122+
isinstance(file, BufferedReader)
123+
or isinstance(file, bytes)
124+
or isinstance(file, FileIO)
125+
):
126+
# bytes or byte-stream-like object received
127+
_file = {"file": (filename, file, headers.pop("content-type"))}
128+
else:
129+
# str or pathlib.path received
130+
_file = {
131+
"file": (
132+
filename,
133+
open(file, "rb"),
134+
headers.pop("content-type"),
135+
)
136+
}
137+
return await self._request(
138+
"PUT",
139+
final_url,
140+
files=_file,
141+
headers=headers,
142+
)
143+
81144
async def create_signed_url(
82145
self, path: str, expires_in: int, options: CreateSignedURLOptions = {}
83146
) -> dict[str, str]:
@@ -281,13 +344,13 @@ async def upload(
281344
The File object to be stored in the bucket. or a async generator of chunks
282345
file_options
283346
HTTP headers.
284-
The expected keys are:
285-
`cache-control`: The number of seconds the asset is cached in the browser and in the Supabase CDN.
286-
`content-type`: This SHOULD be set properly, otherwise the default value of text/plain will be used.
287-
`x-upsert`: If set to true, the file will be updated if it already exists.
288347
"""
289348
if file_options is None:
290349
file_options = {}
350+
cache_control = file_options.get("cache-control")
351+
if cache_control:
352+
file_options["cache-control"] = f"max-age={cache_control}"
353+
291354
headers = {
292355
**self._client.headers,
293356
**DEFAULT_FILE_OPTIONS,

storage3/_sync/file_api.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,69 @@ def create_signed_upload_url(self, path: str) -> SignedUploadURL:
7878
"path": path,
7979
}
8080

81+
def upload_to_signed_url(
82+
self,
83+
path: str,
84+
token: str,
85+
file: Union[BufferedReader, bytes, FileIO, str, Path],
86+
file_options: Optional[FileOptions] = None,
87+
) -> Response:
88+
"""
89+
Upload a file with a token generated from :meth:`.create_signed_url`
90+
91+
Parameters
92+
----------
93+
path
94+
The file path, including the file name
95+
token
96+
The token generated from :meth:`.create_signed_url`
97+
file
98+
The file contents or a file-like object to upload
99+
file_options
100+
Additional options for the uploaded file
101+
"""
102+
_path = self._get_final_path(path)
103+
_url = urllib.parse.urlparse(f"/object/upload/sign/{_path}")
104+
query_params = urllib.parse.urlencode({"token": token})
105+
final_url = f"{_url.geturl()}?{query_params}"
106+
107+
if file_options is None:
108+
file_options = {}
109+
110+
cache_control = file_options.get("cache-control")
111+
if cache_control:
112+
file_options["cache-control"] = f"max-age={cache_control}"
113+
114+
headers = {
115+
**self._client.headers,
116+
**DEFAULT_FILE_OPTIONS,
117+
**file_options,
118+
}
119+
filename = path.rsplit("/", maxsplit=1)[-1]
120+
121+
if (
122+
isinstance(file, BufferedReader)
123+
or isinstance(file, bytes)
124+
or isinstance(file, FileIO)
125+
):
126+
# bytes or byte-stream-like object received
127+
_file = {"file": (filename, file, headers.pop("content-type"))}
128+
else:
129+
# str or pathlib.path received
130+
_file = {
131+
"file": (
132+
filename,
133+
open(file, "rb"),
134+
headers.pop("content-type"),
135+
)
136+
}
137+
return self._request(
138+
"PUT",
139+
final_url,
140+
files=_file,
141+
headers=headers,
142+
)
143+
81144
def create_signed_url(
82145
self, path: str, expires_in: int, options: CreateSignedURLOptions = {}
83146
) -> dict[str, str]:
@@ -281,13 +344,13 @@ def upload(
281344
The File object to be stored in the bucket. or a async generator of chunks
282345
file_options
283346
HTTP headers.
284-
The expected keys are:
285-
`cache-control`: The number of seconds the asset is cached in the browser and in the Supabase CDN.
286-
`content-type`: This SHOULD be set properly, otherwise the default value of text/plain will be used.
287-
`x-upsert`: If set to true, the file will be updated if it already exists.
288347
"""
289348
if file_options is None:
290349
file_options = {}
350+
cache_control = file_options.get("cache-control")
351+
if cache_control:
352+
file_options["cache-control"] = f"max-age={cache_control}"
353+
291354
headers = {
292355
**self._client.headers,
293356
**DEFAULT_FILE_OPTIONS,

0 commit comments

Comments
 (0)