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

Commit 18c14a4

Browse files
committed
chore: add sync version of the update function
1 parent da4d785 commit 18c14a4

File tree

2 files changed

+125
-6
lines changed

2 files changed

+125
-6
lines changed

storage3/_sync/file_api.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from dataclasses import dataclass, field
55
from io import BufferedReader, FileIO
66
from pathlib import Path
7-
from typing import Any, Optional, Union, cast
7+
from typing import Any, Literal, Optional, Union, cast
88

99
from httpx import HTTPError, Response
1010

@@ -342,8 +342,9 @@ def download(self, path: str, options: DownloadOptions = {}) -> bytes:
342342
)
343343
return response.content
344344

345-
def upload(
345+
def _upload_or_update(
346346
self,
347+
method: Literal["POST", "PUT"],
347348
path: str,
348349
file: Union[BufferedReader, bytes, FileIO, str, Path],
349350
file_options: Optional[FileOptions] = None,
@@ -365,9 +366,6 @@ def upload(
365366
file_options = {}
366367
cache_control = file_options.get("cache-control")
367368
_data = {}
368-
if cache_control:
369-
file_options["cache-control"] = f"max-age={cache_control}"
370-
_data = {"cacheControl": cache_control}
371369

372370
headers = {
373371
**self._client.headers,
@@ -376,6 +374,10 @@ def upload(
376374
}
377375
filename = path.rsplit("/", maxsplit=1)[-1]
378376

377+
if cache_control:
378+
headers["cache-control"] = f"max-age={cache_control}"
379+
_data = {"cacheControl": cache_control}
380+
379381
if (
380382
isinstance(file, BufferedReader)
381383
or isinstance(file, bytes)
@@ -396,9 +398,38 @@ def upload(
396398
_path = self._get_final_path(path)
397399

398400
return self._request(
399-
"POST", f"/object/{_path}", files=files, headers=headers, data=_data
401+
method, f"/object/{_path}", files=files, headers=headers, data=_data
400402
)
401403

404+
def upload(
405+
self,
406+
path: str,
407+
file: Union[BufferedReader, bytes, FileIO, str, Path],
408+
file_options: Optional[FileOptions] = None,
409+
) -> Response:
410+
"""
411+
Uploads a file to an existing bucket.
412+
413+
Parameters
414+
----------
415+
path
416+
The relative file path including the bucket ID. Should be of the format `bucket/folder/subfolder/filename.png`.
417+
The bucket must already exist before attempting to upload.
418+
file
419+
The File object to be stored in the bucket. or a async generator of chunks
420+
file_options
421+
HTTP headers.
422+
"""
423+
return self._upload_or_update("POST", path, file, file_options)
424+
425+
def update(
426+
self,
427+
path: str,
428+
file: Union[BufferedReader, bytes, FileIO, str, Path],
429+
file_options: Optional[FileOptions] = None,
430+
) -> Response:
431+
return self._upload_or_update("PUT", path, file, file_options)
432+
402433
def _get_final_path(self, path: str) -> str:
403434
return f"{self.id}/{path}"
404435

tests/_sync/test_client.py

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

156156

157+
@pytest.fixture
158+
def two_files(tmp_path: Path, uuid_factory: Callable[[], str]) -> list[FileForTesting]:
159+
"""Creates multiple test files (different 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+
file_content_2 = (
176+
b'<svg width="119" height="123" viewBox="0 0 119 123" fill="none" xmlns="http://www.w3.org/2000/svg"> '
177+
b'<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 '
178+
b'40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/> '
179+
b'<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 '
180+
b'40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" '
181+
b'fill-opacity="0.2"/> <path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 '
182+
b'72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3FDF8E"/> <defs>'
183+
b'<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295"'
184+
b'gradientUnits="userSpaceOnUse"> <stop stop-color="#249361"/> <stop offset="1" stop-color="#3FDF8E"/> '
185+
b'</linearGradient> <linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" '
186+
b'gradientUnits="userSpaceOnUse"> <stop/> <stop offset="1" stop-opacity="0"/> </linearGradient> </defs> </svg>'
187+
)
188+
bucket_folder = uuid_factory()
189+
bucket_path_1 = f"{bucket_folder}/{file_name_1}"
190+
bucket_path_2 = f"{bucket_folder}/{file_name_2}"
191+
file_path_1 = tmp_path / file_name_1
192+
file_path_2 = tmp_path / file_name_2
193+
with open(file_path_1, "wb") as f:
194+
f.write(file_content)
195+
with open(file_path_2, "wb") as f:
196+
f.write(file_content_2)
197+
198+
return [
199+
FileForTesting(
200+
name=file_name_1,
201+
local_path=str(file_path_1),
202+
bucket_folder=bucket_folder,
203+
bucket_path=bucket_path_1,
204+
mime_type="image/svg+xml",
205+
file_content=file_content,
206+
),
207+
FileForTesting(
208+
name=file_name_2,
209+
local_path=str(file_path_2),
210+
bucket_folder=bucket_folder,
211+
bucket_path=bucket_path_2,
212+
mime_type="image/svg+xml",
213+
file_content=file_content_2,
214+
),
215+
]
216+
217+
157218
@pytest.fixture
158219
def multi_file(tmp_path: Path, uuid_factory: Callable[[], str]) -> list[FileForTesting]:
159220
"""Creates multiple test files (same content, same bucket/folder path, different file names)"""
@@ -221,6 +282,33 @@ def test_client_upload(
221282
assert image_info.get("metadata", {}).get("mimetype") == file.mime_type
222283

223284

285+
def test_client_update(
286+
storage_file_client: SyncBucketProxy,
287+
two_files: list[FileForTesting],
288+
) -> None:
289+
"""Ensure we can upload files to a bucket"""
290+
storage_file_client.upload(
291+
two_files[0].bucket_path,
292+
two_files[0].local_path,
293+
{"content-type": two_files[0].mime_type},
294+
)
295+
296+
storage_file_client.update(
297+
two_files[0].bucket_path,
298+
two_files[1].local_path,
299+
{"content-type": two_files[1].mime_type},
300+
)
301+
302+
image = storage_file_client.download(two_files[0].bucket_path)
303+
file_list = storage_file_client.list(two_files[0].bucket_folder)
304+
image_info = next(
305+
(f for f in file_list if f.get("name") == two_files[0].name), None
306+
)
307+
308+
assert image == two_files[1].file_content
309+
assert image_info.get("metadata", {}).get("mimetype") == two_files[1].mime_type
310+
311+
224312
@pytest.mark.parametrize(
225313
"path", ["foobar.txt", "example/nested.jpg", "/leading/slash.png"]
226314
)

0 commit comments

Comments
 (0)