11from __future__ import annotations
22
33from collections .abc import Mapping
4+ from typing import Any
5+ from typing import Iterable
46
57import httpx
68
79from .._base_client import BaseClient
810from ..exceptions import InsforgeHTTPError
911from .._utils import quote_path_segment
12+ from .models import DownloadStrategy
1013from .models import StorageBucketListResponse
14+ from .models import StorageBucketResponse
15+ from .models import StorageBucketUpdateResponse
16+ from .models import StorageDeleteBucketResponse
1117from .models import StorageDeleteObjectResponse
18+ from .models import StorageDownloadResult
1219from .models import StorageObjectResponse
20+ from .models import StoredFileList
21+ from .models import UploadStrategy
1322
1423
1524class StorageClient :
1625 def __init__ (self , client : BaseClient ) -> None :
1726 self ._client = client
1827
28+ def _bucket_path (self , bucket_name : str ) -> str :
29+ return f"/api/storage/buckets/{ quote_path_segment (bucket_name )} "
30+
1931 def _object_url_path (self , bucket_name : str , object_key : str ) -> str :
2032 return (
2133 "/api/storage/buckets/"
@@ -32,6 +44,81 @@ async def list_buckets(self, *, access_token: str | None = None) -> StorageBucke
3244 )
3345 return StorageBucketListResponse .model_validate (payload )
3446
47+ async def create_bucket (
48+ self ,
49+ * ,
50+ bucket_name : str ,
51+ is_public : bool | None = None ,
52+ access_token : str | None = None ,
53+ ) -> StorageBucketResponse :
54+ payload : dict [str , Any ] = {"bucketName" : bucket_name }
55+ if is_public is not None :
56+ payload ["isPublic" ] = is_public
57+
58+ response = await self ._client ._request_json (
59+ "POST" ,
60+ "/api/storage/buckets" ,
61+ json = payload ,
62+ access_token = access_token ,
63+ )
64+ return StorageBucketResponse .model_validate (response )
65+
66+ async def update_bucket (
67+ self ,
68+ bucket_name : str ,
69+ * ,
70+ is_public : bool ,
71+ access_token : str | None = None ,
72+ ) -> StorageBucketUpdateResponse :
73+ response = await self ._client ._request_json (
74+ "PATCH" ,
75+ self ._bucket_path (bucket_name ),
76+ json = {"isPublic" : is_public },
77+ access_token = access_token ,
78+ )
79+ return StorageBucketUpdateResponse .model_validate (response )
80+
81+ async def delete_bucket (
82+ self ,
83+ bucket_name : str ,
84+ * ,
85+ access_token : str | None = None ,
86+ ) -> StorageDeleteBucketResponse :
87+ response = await self ._client ._request_json (
88+ "DELETE" ,
89+ self ._bucket_path (bucket_name ),
90+ access_token = access_token ,
91+ )
92+ return StorageDeleteBucketResponse .model_validate (response )
93+
94+ async def list_objects (
95+ self ,
96+ bucket_name : str ,
97+ * ,
98+ prefix : str | None = None ,
99+ limit : int | None = None ,
100+ offset : int | None = None ,
101+ search : str | None = None ,
102+ access_token : str | None = None ,
103+ ) -> StoredFileList :
104+ params : dict [str , str ] = {}
105+ if prefix is not None :
106+ params ["prefix" ] = prefix
107+ if limit is not None :
108+ params ["limit" ] = str (limit )
109+ if offset is not None :
110+ params ["offset" ] = str (offset )
111+ if search is not None :
112+ params ["search" ] = search
113+
114+ payload = await self ._client ._request_json (
115+ "GET" ,
116+ f"/api/storage/buckets/{ quote_path_segment (bucket_name )} /objects" ,
117+ params = params or None ,
118+ access_token = access_token ,
119+ )
120+ return StoredFileList .model_validate (payload )
121+
35122 async def upload_object (
36123 self ,
37124 bucket_name : str ,
@@ -69,7 +156,7 @@ async def download_object(
69156 * ,
70157 access_token : str | None = None ,
71158 extra_headers : Mapping [str , str ] | None = None ,
72- ) -> bytes :
159+ ) -> StorageDownloadResult :
73160 path = self ._object_url_path (bucket_name , object_key )
74161 response = await self ._client .http_client .request (
75162 "GET" ,
@@ -87,7 +174,18 @@ async def download_object(
87174 response ,
88175 )
89176
90- return response .content
177+ headers = response .headers
178+ content_type = (headers .get ("Content-Type" ) or headers .get ("content-type" ))
179+ content_length = (
180+ headers .get ("Content-Length" )
181+ or headers .get ("content-length" )
182+ or str (len (response .content ))
183+ )
184+ return StorageDownloadResult (
185+ content = response .content ,
186+ content_type = content_type ,
187+ content_length = content_length ,
188+ )
91189
92190 async def delete_object (
93191 self ,
@@ -105,3 +203,92 @@ async def delete_object(
105203 extra_headers = extra_headers ,
106204 )
107205 return StorageDeleteObjectResponse .model_validate (payload )
206+
207+ async def upload_object_auto (
208+ self ,
209+ bucket_name : str ,
210+ * ,
211+ data : bytes ,
212+ filename : str ,
213+ content_type : str | None = None ,
214+ access_token : str | None = None ,
215+ ) -> StorageObjectResponse :
216+ path = f"/api/storage/buckets/{ quote_path_segment (bucket_name )} /objects"
217+ response = await self ._client .http_client .request (
218+ "POST" ,
219+ self ._client ._build_url (path ),
220+ files = {"file" : (filename , data , content_type or "application/octet-stream" )},
221+ headers = self ._client ._build_headers (access_token = access_token ),
222+ )
223+
224+ if response .is_error :
225+ raise InsforgeHTTPError .from_response ("POST" , path , response )
226+
227+ return StorageObjectResponse .model_validate (response .json ())
228+
229+ async def confirm_upload (
230+ self ,
231+ bucket_name : str ,
232+ object_key : str ,
233+ * ,
234+ size : int ,
235+ content_type : str | None = None ,
236+ etag : str | None = None ,
237+ access_token : str | None = None ,
238+ ) -> StorageObjectResponse :
239+ payload : dict [str , Any ] = {"size" : size }
240+ if content_type is not None :
241+ payload ["contentType" ] = content_type
242+ if etag is not None :
243+ payload ["etag" ] = etag
244+
245+ response = await self ._client ._request_json (
246+ "POST" ,
247+ f"{ self ._object_url_path (bucket_name , object_key )} /confirm-upload" ,
248+ json = payload ,
249+ access_token = access_token ,
250+ )
251+ return StorageObjectResponse .model_validate (response )
252+
253+ async def get_upload_strategy (
254+ self ,
255+ bucket_name : str ,
256+ * ,
257+ filename : str ,
258+ content_type : str | None = None ,
259+ size : int | None = None ,
260+ access_token : str | None = None ,
261+ ) -> UploadStrategy :
262+ payload : dict [str , Any ] = {"filename" : filename }
263+ if content_type is not None :
264+ payload ["contentType" ] = content_type
265+ if size is not None :
266+ payload ["size" ] = size
267+
268+ response = await self ._client ._request_json (
269+ "POST" ,
270+ f"/api/storage/buckets/{ quote_path_segment (bucket_name )} /upload-strategy" ,
271+ json = payload ,
272+ access_token = access_token ,
273+ )
274+ return UploadStrategy .model_validate (response )
275+
276+ async def get_download_strategy (
277+ self ,
278+ bucket_name : str ,
279+ object_key : str ,
280+ * ,
281+ expires_in : int | None = None ,
282+ access_token : str | None = None ,
283+ ) -> DownloadStrategy :
284+ payload : dict [str , Any ] = {}
285+ if expires_in is not None :
286+ payload ["expiresIn" ] = expires_in
287+
288+ response = await self ._client ._request_json (
289+ "POST" ,
290+ f"{ self ._object_url_path (bucket_name , object_key )} /download-strategy" ,
291+ json = payload or None ,
292+ access_token = access_token ,
293+ )
294+ return DownloadStrategy .model_validate (response )
0 commit comments