Skip to content

Commit 3740bd1

Browse files
Merge branch 'master' into bug/fix-wallets-update-api
2 parents 8ef1c24 + 0f9d22d commit 3740bd1

File tree

6 files changed

+327
-62
lines changed

6 files changed

+327
-62
lines changed
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
from typing import Optional
2-
31
from pydantic import ByteSize
42

53

6-
def byte_size_ids(val) -> Optional[str]:
4+
def byte_size_ids(val) -> str | None:
75
if isinstance(val, ByteSize):
86
return val.human_readable()
97
return None

services/storage/src/simcore_service_storage/s3_client.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import json
33
import logging
44
import urllib.parse
5+
from collections.abc import AsyncGenerator, Callable
56
from contextlib import AsyncExitStack
67
from dataclasses import dataclass
78
from pathlib import Path
8-
from typing import AsyncGenerator, Callable, Final, TypeAlias, cast
9+
from typing import Final, TypeAlias, cast
910

1011
import aioboto3
1112
from aiobotocore.session import ClientCreatorContext
@@ -350,14 +351,14 @@ async def copy_file(
350351
:type src_file: SimcoreS3FileID
351352
:type dst_file: SimcoreS3FileID
352353
"""
353-
copy_options = dict(
354-
CopySource={"Bucket": bucket, "Key": src_file},
355-
Bucket=bucket,
356-
Key=dst_file,
357-
Config=TransferConfig(max_concurrency=self.transfer_max_concurrency),
358-
)
354+
copy_options = {
355+
"CopySource": {"Bucket": bucket, "Key": src_file},
356+
"Bucket": bucket,
357+
"Key": dst_file,
358+
"Config": TransferConfig(max_concurrency=self.transfer_max_concurrency),
359+
}
359360
if bytes_transfered_cb:
360-
copy_options |= dict(Callback=bytes_transfered_cb)
361+
copy_options |= {"Callback": bytes_transfered_cb}
361362
await self.client.copy(**copy_options)
362363

363364
@s3_exception_handler(_logger)
@@ -387,10 +388,8 @@ async def list_files(
387388
async def file_exists(self, bucket: S3BucketName, *, s3_object: str) -> bool:
388389
"""Checks if an S3 object exists"""
389390
# SEE https://www.peterbe.com/plog/fastest-way-to-find-out-if-a-file-exists-in-s3
390-
s3_objects, _ = await _list_objects_v2_paginated(
391-
self.client, bucket, s3_object, max_total_items=EXPAND_DIR_MAX_ITEM_COUNT
392-
)
393-
return len(s3_objects) > 0
391+
response = await self.client.list_objects_v2(Bucket=bucket, Prefix=s3_object)
392+
return len(response.get("Contents", [])) > 0
394393

395394
@s3_exception_handler(_logger)
396395
async def upload_file(
@@ -406,13 +405,13 @@ async def upload_file(
406405
:type file: Path
407406
:type file_id: SimcoreS3FileID
408407
"""
409-
upload_options = dict(
410-
Bucket=bucket,
411-
Key=file_id,
412-
Config=TransferConfig(max_concurrency=self.transfer_max_concurrency),
413-
)
408+
upload_options = {
409+
"Bucket": bucket,
410+
"Key": file_id,
411+
"Config": TransferConfig(max_concurrency=self.transfer_max_concurrency),
412+
}
414413
if bytes_transfered_cb:
415-
upload_options |= dict(Callback=bytes_transfered_cb)
414+
upload_options |= {"Callback": bytes_transfered_cb}
416415
await self.client.upload_file(f"{file}", **upload_options)
417416

418417
@staticmethod

services/storage/src/simcore_service_storage/simcore_s3_dsm.py

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@
6666
from .s3_client import S3MetaData, StorageS3Client
6767
from .s3_utils import S3TransferDataCB, update_task_progress
6868
from .settings import Settings
69-
from .simcore_s3_dsm_utils import expand_directory, get_simcore_directory
69+
from .simcore_s3_dsm_utils import (
70+
expand_directory,
71+
get_directory_file_id,
72+
get_simcore_directory,
73+
)
7074
from .utils import (
7175
convert_db_to_model,
7276
download_to_file_or_raise,
@@ -383,37 +387,91 @@ async def complete_file_upload(
383387
async def create_file_download_link(
384388
self, user_id: UserID, file_id: StorageFileID, link_type: LinkType
385389
) -> AnyUrl:
390+
"""
391+
Cases:
392+
1. the file_id maps 1:1 to `file_meta_data` (e.g. it is not a file inside a directory)
393+
2. the file_id represents a file inside a directory (its root path maps 1:1 to a `file_meta_data` defined as a directory)
394+
395+
3. Raises FileNotFoundError if the file does not exist
396+
4. Raises FileAccessRightError if the user does not have access to the file
397+
"""
386398
async with self.engine.acquire() as conn:
387-
can: AccessRights | None = await get_file_access_rights(
388-
conn, user_id, file_id
399+
directory_file_id: SimcoreS3FileID | None = await get_directory_file_id(
400+
conn, cast(SimcoreS3FileID, file_id)
389401
)
390-
if not can.read:
391-
# NOTE: this is tricky. A user with read access can download and data!
392-
# If write permission would be required, then shared projects as views cannot
393-
# recover data in nodes (e.g. jupyter cannot pull work data)
394-
#
395-
raise FileAccessRightError(access_right="read", file_id=file_id)
396-
397-
fmd = await db_file_meta_data.get(
398-
conn, parse_obj_as(SimcoreS3FileID, file_id)
402+
return (
403+
await self._get_link_for_directory_fmd(
404+
conn, user_id, directory_file_id, file_id, link_type
405+
)
406+
if directory_file_id
407+
else await self._get_link_for_file_fmd(
408+
conn, user_id, file_id, link_type
409+
)
399410
)
400-
if not is_file_entry_valid(fmd):
401-
# try lazy update
402-
fmd = await self._update_database_from_storage(conn, fmd)
403411

412+
@staticmethod
413+
async def __ensure_read_access_rights(
414+
conn: SAConnection, user_id: UserID, storage_file_id: StorageFileID
415+
) -> None:
416+
can: AccessRights | None = await get_file_access_rights(
417+
conn, user_id, storage_file_id
418+
)
419+
if not can.read:
420+
# NOTE: this is tricky. A user with read access can download and data!
421+
# If write permission would be required, then shared projects as views cannot
422+
# recover data in nodes (e.g. jupyter cannot pull work data)
423+
#
424+
raise FileAccessRightError(access_right="read", file_id=storage_file_id)
425+
426+
async def __get_link(
427+
self, s3_file_id: SimcoreS3FileID, link_type: LinkType
428+
) -> AnyUrl:
404429
link: AnyUrl = parse_obj_as(
405430
AnyUrl,
406-
f"s3://{self.simcore_bucket_name}/{urllib.parse.quote(fmd.object_name)}",
431+
f"s3://{self.simcore_bucket_name}/{urllib.parse.quote(s3_file_id)}",
407432
)
408433
if link_type == LinkType.PRESIGNED:
409434
link = await get_s3_client(self.app).create_single_presigned_download_link(
410435
self.simcore_bucket_name,
411-
fmd.object_name,
436+
s3_file_id,
412437
self.settings.STORAGE_DEFAULT_PRESIGNED_LINK_EXPIRATION_SECONDS,
413438
)
414439

415440
return link
416441

442+
async def _get_link_for_file_fmd(
443+
self,
444+
conn: SAConnection,
445+
user_id: UserID,
446+
file_id: StorageFileID,
447+
link_type: LinkType,
448+
) -> AnyUrl:
449+
# 1. the file_id maps 1:1 to `file_meta_data`
450+
await self.__ensure_read_access_rights(conn, user_id, file_id)
451+
452+
fmd = await db_file_meta_data.get(conn, parse_obj_as(SimcoreS3FileID, file_id))
453+
if not is_file_entry_valid(fmd):
454+
# try lazy update
455+
fmd = await self._update_database_from_storage(conn, fmd)
456+
457+
return await self.__get_link(fmd.object_name, link_type)
458+
459+
async def _get_link_for_directory_fmd(
460+
self,
461+
conn: SAConnection,
462+
user_id: UserID,
463+
directory_file_id: SimcoreS3FileID,
464+
file_id: StorageFileID,
465+
link_type: LinkType,
466+
) -> AnyUrl:
467+
# 2. the file_id represents a file inside a directory
468+
await self.__ensure_read_access_rights(conn, user_id, directory_file_id)
469+
if not await get_s3_client(self.app).file_exists(
470+
self.simcore_bucket_name, s3_object=f"{file_id}"
471+
):
472+
raise S3KeyNotFoundError(key=file_id, bucket=self.simcore_bucket_name)
473+
return await self.__get_link(parse_obj_as(SimcoreS3FileID, file_id), link_type)
474+
417475
async def delete_file(self, user_id: UserID, file_id: StorageFileID):
418476
async with self.engine.acquire() as conn, conn.begin():
419477
can: AccessRights | None = await get_file_access_rights(

services/storage/src/simcore_service_storage/simcore_s3_dsm_utils.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
from contextlib import suppress
2+
13
from aiohttp import web
4+
from aiopg.sa.connection import SAConnection
25
from models_library.api_schemas_storage import S3BucketName
3-
from models_library.projects_nodes_io import SimcoreS3DirectoryID, SimcoreS3FileID
6+
from models_library.projects_nodes_io import (
7+
SimcoreS3DirectoryID,
8+
SimcoreS3FileID,
9+
StorageFileID,
10+
)
411
from pydantic import ByteSize, NonNegativeInt, parse_obj_as
512
from servicelib.utils import ensure_ends_with
613

14+
from . import db_file_meta_data
15+
from .exceptions import FileMetaDataNotFoundError
716
from .models import FileMetaData, FileMetaDataAtDB
817
from .s3 import get_s3_client
918
from .s3_client import S3MetaData
@@ -58,3 +67,36 @@ def get_simcore_directory(file_id: SimcoreS3FileID) -> str:
5867
except ValueError:
5968
return ""
6069
return f"{directory_id}"
70+
71+
72+
async def get_directory_file_id(
73+
conn: SAConnection, file_id: SimcoreS3FileID
74+
) -> SimcoreS3FileID | None:
75+
"""
76+
returns the containing file's `directory_file_id` if the entry exists
77+
in the `file_meta_data` table
78+
"""
79+
80+
async def _get_fmd(
81+
conn: SAConnection, s3_file_id: StorageFileID
82+
) -> FileMetaDataAtDB | None:
83+
with suppress(FileMetaDataNotFoundError):
84+
return await db_file_meta_data.get(
85+
conn, parse_obj_as(SimcoreS3FileID, s3_file_id)
86+
)
87+
return None
88+
89+
provided_file_id_fmd = await _get_fmd(conn, file_id)
90+
if provided_file_id_fmd:
91+
# file_meta_data exists it is not a directory
92+
return None
93+
94+
directory_file_id_str: str = get_simcore_directory(file_id)
95+
if directory_file_id_str == "":
96+
# could not extract a directory name from the provided path
97+
return None
98+
99+
directory_file_id = parse_obj_as(SimcoreS3FileID, directory_file_id_str.rstrip("/"))
100+
directory_file_id_fmd = await _get_fmd(conn, directory_file_id)
101+
102+
return directory_file_id if directory_file_id_fmd else None

services/storage/src/simcore_service_storage/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async def download_to_file_or_raise(
5757

5858

5959
def is_file_entry_valid(file_metadata: FileMetaData | FileMetaDataAtDB) -> bool:
60+
"""checks if the file_metadata is valid"""
6061
return (
6162
file_metadata.entity_tag is not None
6263
and file_metadata.file_size > 0

0 commit comments

Comments
 (0)