Skip to content

Commit 41f7c5c

Browse files
committed
maybe there
1 parent 75a17bf commit 41f7c5c

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

packages/models-library/src/models_library/api_schemas_storage.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,25 @@ class FileUploadSchema(BaseModel):
283283
urls: list[AnyUrl]
284284
links: FileUploadLinks
285285

286+
model_config = ConfigDict(
287+
extra="forbid",
288+
json_schema_extra={
289+
"examples": [
290+
# typical S3 entry
291+
{
292+
"chunk_size": "10000000",
293+
"urls": [
294+
"https://s3.amazonaws.com/bucket-name/key-name?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Expires=1698298164&Signature=WObYM%2F%2B4t7O3%2FZS3Kegb%2Bc4%3D",
295+
],
296+
"links": {
297+
"abort_upload": "https://storage.com:3021/bucket-name/key-name:abort",
298+
"complete_upload": "https://storage.com:3021/bucket-name/key-name:complete",
299+
},
300+
},
301+
]
302+
},
303+
)
304+
286305

287306
class TableSynchronisation(BaseModel):
288307
dry_run: bool | None = None

services/web/server/tests/unit/with_dbs/01/test_storage.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@
2020
FileLocation,
2121
FileMetaDataGet,
2222
FileMetaDataGetv010,
23+
FileUploadCompleteResponse,
24+
FileUploadCompletionBody,
25+
FileUploadSchema,
26+
LinkType,
2327
)
2428
from models_library.generics import Envelope
2529
from models_library.projects import ProjectID
30+
from models_library.projects_nodes_io import LocationID, StorageFileID
2631
from models_library.users import UserID
32+
from pydantic import AnyUrl, TypeAdapter
2733
from pytest_simcore.helpers.assert_checks import assert_status
2834
from pytest_simcore.helpers.logging_tools import log_context
2935
from servicelib.aiohttp import status
@@ -163,6 +169,86 @@ async def _list_dataset_files_metadata(user_id: UserID, request: Request):
163169
]
164170
)
165171

172+
@router.put(
173+
"/locations/{location_id}/files/{file_id:path}",
174+
response_model=Envelope[FileUploadSchema],
175+
)
176+
async def upload_file(
177+
user_id: UserID,
178+
location_id: LocationID,
179+
file_id: StorageFileID,
180+
request: Request,
181+
link_type: LinkType = LinkType.PRESIGNED,
182+
):
183+
assert "json_schema_extra" in FileUploadSchema.model_config
184+
assert isinstance(FileUploadSchema.model_config["json_schema_extra"], dict)
185+
assert isinstance(
186+
FileUploadSchema.model_config["json_schema_extra"]["examples"], list
187+
)
188+
189+
abort_url = (
190+
URL(f"{request.url}")
191+
.with_path(
192+
request.app.url_path_for(
193+
"abort_upload_file",
194+
location_id=f"{location_id}",
195+
file_id=file_id,
196+
)
197+
)
198+
.with_query(user_id=user_id)
199+
)
200+
201+
complete_url = (
202+
URL(f"{request.url}")
203+
.with_path(
204+
request.app.url_path_for(
205+
"complete_upload_file",
206+
location_id=f"{location_id}",
207+
file_id=file_id,
208+
)
209+
)
210+
.with_query(user_id=user_id)
211+
)
212+
response = FileUploadSchema.model_validate(
213+
random.choice( # noqa: S311
214+
FileUploadSchema.model_config["json_schema_extra"]["examples"]
215+
)
216+
)
217+
response.links.abort_upload = TypeAdapter(AnyUrl).validate_python(
218+
f"{abort_url}"
219+
)
220+
response.links.complete_upload = TypeAdapter(AnyUrl).validate_python(
221+
f"{complete_url}"
222+
)
223+
224+
return Envelope[FileUploadSchema](data=response)
225+
226+
@router.post(
227+
"/locations/{location_id}/files/{file_id:path}:complete",
228+
response_model=Envelope[FileUploadCompleteResponse],
229+
status_code=status.HTTP_202_ACCEPTED,
230+
)
231+
async def complete_upload_file(
232+
user_id: UserID,
233+
location_id: LocationID,
234+
file_id: StorageFileID,
235+
body: FileUploadCompletionBody,
236+
request: Request,
237+
):
238+
...
239+
240+
@router.post(
241+
"/locations/{location_id}/files/{file_id:path}:abort",
242+
status_code=status.HTTP_204_NO_CONTENT,
243+
)
244+
async def abort_upload_file(
245+
user_id: UserID,
246+
location_id: LocationID,
247+
file_id: StorageFileID,
248+
request: Request,
249+
):
250+
...
251+
166252
app.include_router(router)
167253

168254
return app
@@ -403,3 +489,35 @@ async def test_storage_list_filter(
403489
for item in data:
404490
model = FileMetaDataGet.model_validate(item)
405491
assert model
492+
493+
494+
@pytest.fixture
495+
def file_id(faker: Faker) -> StorageFileID:
496+
return TypeAdapter(StorageFileID).validate_python(
497+
f"{faker.uuid4()}/{faker.uuid4()}/{faker.file_name()}"
498+
)
499+
500+
501+
@pytest.mark.parametrize(
502+
"user_role,expected",
503+
[
504+
# (UserRole.ANONYMOUS, status.HTTP_401_UNAUTHORIZED),
505+
# (UserRole.GUEST, status.HTTP_200_OK),
506+
(UserRole.USER, status.HTTP_200_OK),
507+
# (UserRole.TESTER, status.HTTP_200_OK),
508+
],
509+
)
510+
async def test_upload_file(
511+
client: TestClient,
512+
logged_user: dict[str, Any],
513+
expected: int,
514+
file_id: StorageFileID,
515+
):
516+
url = f"/v0/storage/locations/0/files/{quote(file_id, safe='')}"
517+
518+
assert url.startswith(PREFIX)
519+
520+
resp = await client.put(url, params={"user_id": logged_user["id"]})
521+
data, error = await assert_status(resp, expected)
522+
assert not error
523+
assert data

0 commit comments

Comments
 (0)