diff --git a/src/storage/src/storage3/_async/file_api.py b/src/storage/src/storage3/_async/file_api.py index a5d7363a..c26f7acb 100644 --- a/src/storage/src/storage3/_async/file_api.py +++ b/src/storage/src/storage3/_async/file_api.py @@ -201,8 +201,10 @@ async def upload_to_signed_url( return UploadResponse(path=path, Key=data["Key"]) def _make_signed_url( - self, signed_url: str, download_query: dict[str, str] + self, signed_url: Optional[str], download_query: dict[str, str] ) -> SignedUrlResponse: + if signed_url is None: + return {"signedURL": None, "signedUrl": None} url = URL(signed_url[1:]) # ignore starting slash signedURL = self._base_url.join(url).extend_query(download_query) return {"signedURL": str(signedURL), "signedUrl": str(signedURL)} diff --git a/src/storage/src/storage3/_sync/file_api.py b/src/storage/src/storage3/_sync/file_api.py index 831fb6a6..67f43c0b 100644 --- a/src/storage/src/storage3/_sync/file_api.py +++ b/src/storage/src/storage3/_sync/file_api.py @@ -201,8 +201,10 @@ def upload_to_signed_url( return UploadResponse(path=path, Key=data["Key"]) def _make_signed_url( - self, signed_url: str, download_query: dict[str, str] + self, signed_url: Optional[str], download_query: dict[str, str] ) -> SignedUrlResponse: + if signed_url is None: + return {"signedURL": None, "signedUrl": None} url = URL(signed_url[1:]) # ignore starting slash signedURL = self._base_url.join(url).extend_query(download_query) return {"signedURL": str(signedURL), "signedUrl": str(signedURL)} diff --git a/src/storage/src/storage3/types.py b/src/storage/src/storage3/types.py index 886f50b7..c6d59b22 100644 --- a/src/storage/src/storage3/types.py +++ b/src/storage/src/storage3/types.py @@ -152,15 +152,15 @@ def __init__(self, path: str, Key: str) -> None: class SignedUrlResponse(TypedDict): - signedURL: str - signedUrl: str + signedURL: Optional[str] + signedUrl: Optional[str] class CreateSignedUrlResponse(TypedDict): error: Optional[str] path: str - signedURL: str - signedUrl: str + signedURL: Optional[str] + signedUrl: Optional[str] class SignedUrlJsonResponse(BaseModel, extra="ignore"): @@ -170,7 +170,7 @@ class SignedUrlJsonResponse(BaseModel, extra="ignore"): class SignedUrlsJsonItem(BaseModel, extra="ignore"): error: Optional[str] path: str - signedURL: str + signedURL: Optional[str] SignedUrlsJsonResponse = TypeAdapter(list[SignedUrlsJsonItem]) diff --git a/src/storage/tests/_async/test_client.py b/src/storage/tests/_async/test_client.py index b1d7d069..44d36389 100644 --- a/src/storage/tests/_async/test_client.py +++ b/src/storage/tests/_async/test_client.py @@ -412,6 +412,7 @@ async def test_client_create_signed_url( # Test basic signed URL signed_url = await storage_file_client.create_signed_url(file.bucket_path, 60) + assert signed_url["signedURL"] async with HttpxClient(timeout=None) as client: response = await client.get(signed_url["signedURL"]) response.raise_for_status() @@ -421,6 +422,7 @@ async def test_client_create_signed_url( download_signed_url = await storage_file_client.create_signed_url( file.bucket_path, 60, options={"download": "custom_download.svg"} ) + assert download_signed_url["signedURL"] async with HttpxClient(timeout=None) as client: response = await client.get(download_signed_url["signedURL"]) @@ -441,6 +443,7 @@ async def test_client_create_signed_url( # assert "height=200" in transform_signed_url["signedURL"] # assert "resize=cover" in transform_signed_url["signedURL"] # assert "format=png" in transform_signed_url["signedURL"] + assert transform_signed_url["signedURL"] async with HttpxClient(timeout=None) as client: response = await client.get(transform_signed_url["signedURL"]) response.raise_for_status() @@ -461,6 +464,7 @@ async def test_client_create_signed_urls( async with HttpxClient() as client: for url in signed_urls: + assert url["signedURL"] response = await client.get(url["signedURL"]) response.raise_for_status() assert response.content == multi_file[0].file_content @@ -734,6 +738,7 @@ async def test_client_create_signed_urls_with_download( async with HttpxClient() as client: for i, url in enumerate(signed_urls): + assert url["signedURL"] response = await client.get(url["signedURL"]) response.raise_for_status() assert response.content == multi_file[i].file_content diff --git a/src/storage/tests/_sync/test_client.py b/src/storage/tests/_sync/test_client.py index 2e701857..187853e3 100644 --- a/src/storage/tests/_sync/test_client.py +++ b/src/storage/tests/_sync/test_client.py @@ -410,6 +410,7 @@ def test_client_create_signed_url( # Test basic signed URL signed_url = storage_file_client.create_signed_url(file.bucket_path, 60) + assert signed_url["signedURL"] with HttpxClient(timeout=None) as client: response = client.get(signed_url["signedURL"]) response.raise_for_status() @@ -419,6 +420,7 @@ def test_client_create_signed_url( download_signed_url = storage_file_client.create_signed_url( file.bucket_path, 60, options={"download": "custom_download.svg"} ) + assert download_signed_url["signedURL"] with HttpxClient(timeout=None) as client: response = client.get(download_signed_url["signedURL"]) @@ -439,6 +441,7 @@ def test_client_create_signed_url( # assert "height=200" in transform_signed_url["signedURL"] # assert "resize=cover" in transform_signed_url["signedURL"] # assert "format=png" in transform_signed_url["signedURL"] + assert transform_signed_url["signedURL"] with HttpxClient(timeout=None) as client: response = client.get(transform_signed_url["signedURL"]) response.raise_for_status() @@ -459,6 +462,7 @@ def test_client_create_signed_urls( with HttpxClient() as client: for url in signed_urls: + assert url["signedURL"] response = client.get(url["signedURL"]) response.raise_for_status() assert response.content == multi_file[0].file_content @@ -732,6 +736,7 @@ def test_client_create_signed_urls_with_download( with HttpxClient() as client: for i, url in enumerate(signed_urls): + assert url["signedURL"] response = client.get(url["signedURL"]) response.raise_for_status() assert response.content == multi_file[i].file_content @@ -770,3 +775,34 @@ def test_client_list_v2_folder( assert len(result.folders) == 1 folder = result.folders[0] assert folder.key == file.bucket_folder + + +def test_client_list_v2_paginated( + storage_file_client: SyncBucketProxy, file: FileForTesting +) -> None: + """Ensure we can upload files to a bucket""" + suffixes = ["zz", "bb", "xx", "ww", "cc", "aa", "yy", "oo"] + for suffix in suffixes: + storage_file_client.upload( + file.bucket_path + suffix, file.local_path, {"content-type": file.mime_type} + ) + + has_next = True + cursor = "" + pages = 0 + while has_next: + result = storage_file_client.list_v2( + { + "with_delimiter": True, + "prefix": f"{file.bucket_folder}/", + "limit": 2, + "cursor": cursor, + } + ) + has_next = result.hasNext + cursor = result.nextCursor or "" + + assert len(result.objects) == 2 + assert all(f.name.startswith(file.bucket_path) for f in result.objects) + pages += 1 + assert pages == 4