|
28 | 28 | from settings_library.s3 import S3Settings |
29 | 29 | from types_aiobotocore_s3 import S3Client |
30 | 30 | from types_aiobotocore_s3.literals import BucketLocationConstraintType |
31 | | -from types_aiobotocore_s3.type_defs import ObjectIdentifierTypeDef |
| 31 | +from types_aiobotocore_s3.type_defs import ( |
| 32 | + ListObjectsV2RequestRequestTypeDef, |
| 33 | + ObjectIdentifierTypeDef, |
| 34 | +) |
32 | 35 |
|
33 | | -from ._constants import MULTIPART_COPY_THRESHOLD, MULTIPART_UPLOADS_MIN_TOTAL_SIZE |
| 36 | +from ._constants import ( |
| 37 | + MULTIPART_COPY_THRESHOLD, |
| 38 | + MULTIPART_UPLOADS_MIN_TOTAL_SIZE, |
| 39 | + S3_OBJECT_DELIMITER, |
| 40 | +) |
34 | 41 | from ._error_handler import s3_exception_handler, s3_exception_handler_async_gen |
35 | 42 | from ._errors import S3DestinationNotEmptyError, S3KeyNotFoundError |
36 | 43 | from ._models import ( |
37 | 44 | MultiPartUploadLinks, |
| 45 | + PathCursor, |
38 | 46 | S3DirectoryMetaData, |
39 | 47 | S3MetaData, |
40 | 48 | S3ObjectKey, |
| 49 | + S3ObjectPrefix, |
41 | 50 | UploadID, |
42 | 51 | ) |
43 | | -from ._utils import compute_num_file_chunks |
| 52 | +from ._utils import compute_num_file_chunks, create_final_prefix |
44 | 53 |
|
45 | 54 | _logger = logging.getLogger(__name__) |
46 | 55 |
|
@@ -167,7 +176,99 @@ async def get_directory_metadata( |
167 | 176 | size = 0 |
168 | 177 | async for s3_object in self._list_all_objects(bucket=bucket, prefix=prefix): |
169 | 178 | size += s3_object.size |
170 | | - return S3DirectoryMetaData(size=size) |
| 179 | + return S3DirectoryMetaData(prefix=S3ObjectPrefix(prefix), size=ByteSize(size)) |
| 180 | + |
| 181 | + @s3_exception_handler(_logger) |
| 182 | + async def count_objects( |
| 183 | + self, |
| 184 | + *, |
| 185 | + bucket: S3BucketName, |
| 186 | + prefix: S3ObjectPrefix | None, |
| 187 | + start_after: S3ObjectKey | None, |
| 188 | + is_partial_prefix: bool = False, |
| 189 | + use_delimiter: bool = True, |
| 190 | + ) -> int: |
| 191 | + """returns the number of entries in the bucket, defined |
| 192 | + by prefix and start_after same as list_objects |
| 193 | + """ |
| 194 | + paginator = self._client.get_paginator("list_objects_v2") |
| 195 | + total_count = 0 |
| 196 | + async for page in paginator.paginate( |
| 197 | + Bucket=bucket, |
| 198 | + Prefix=create_final_prefix(prefix, is_partial_prefix=is_partial_prefix), |
| 199 | + StartAfter=start_after or "", |
| 200 | + Delimiter=S3_OBJECT_DELIMITER if use_delimiter else "", |
| 201 | + ): |
| 202 | + total_count += page.get("KeyCount", 0) |
| 203 | + return total_count |
| 204 | + |
| 205 | + @s3_exception_handler(_logger) |
| 206 | + async def list_objects( |
| 207 | + self, |
| 208 | + *, |
| 209 | + bucket: S3BucketName, |
| 210 | + prefix: S3ObjectPrefix | None, |
| 211 | + start_after: S3ObjectKey | None, |
| 212 | + limit: int = _MAX_ITEMS_PER_PAGE, |
| 213 | + next_cursor: PathCursor | None = None, |
| 214 | + is_partial_prefix: bool = False, |
| 215 | + ) -> tuple[list[S3MetaData | S3DirectoryMetaData], PathCursor | None]: |
| 216 | + """returns a number of entries in the bucket, defined by limit |
| 217 | + the entries are sorted alphabetically by key. If a cursor is returned |
| 218 | + then the client can call the function again with the cursor to get the |
| 219 | + next entries. |
| 220 | +
|
| 221 | + the first entry is defined by start_after |
| 222 | + if start_after is None, the first entry is the first one in the bucket |
| 223 | + if prefix is not None, only entries with the given prefix are returned |
| 224 | + if prefix is None, all entries in the bucket are returned |
| 225 | + if next_cursor is set, then the call will return the next entries after the cursor |
| 226 | + if is_partial_prefix is set then the prefix is not auto-delimited |
| 227 | + (if False equivalent to `ls /home/user/` |
| 228 | + if True equivalent to `ls /home/user*`) |
| 229 | + limit must be >= 1 and <= _AWS_MAX_ITEMS_PER_PAGE |
| 230 | +
|
| 231 | + Raises: |
| 232 | + ValueError: in case of invalid limit |
| 233 | + """ |
| 234 | + if limit < 1: |
| 235 | + msg = "num_objects must be >= 1" |
| 236 | + raise ValueError(msg) |
| 237 | + if limit > _AWS_MAX_ITEMS_PER_PAGE: |
| 238 | + msg = f"num_objects must be <= {_AWS_MAX_ITEMS_PER_PAGE}" |
| 239 | + raise ValueError(msg) |
| 240 | + |
| 241 | + list_config: ListObjectsV2RequestRequestTypeDef = { |
| 242 | + "Bucket": bucket, |
| 243 | + "Prefix": create_final_prefix(prefix, is_partial_prefix=is_partial_prefix), |
| 244 | + "MaxKeys": limit, |
| 245 | + "Delimiter": S3_OBJECT_DELIMITER, |
| 246 | + } |
| 247 | + if start_after: |
| 248 | + list_config["StartAfter"] = start_after |
| 249 | + if next_cursor: |
| 250 | + list_config["ContinuationToken"] = next_cursor |
| 251 | + listed_objects = await self._client.list_objects_v2(**list_config) |
| 252 | + found_objects: list[S3MetaData | S3DirectoryMetaData] = [] |
| 253 | + if "CommonPrefixes" in listed_objects: |
| 254 | + # we have folders here |
| 255 | + list_subfolders = listed_objects["CommonPrefixes"] |
| 256 | + found_objects.extend( |
| 257 | + S3DirectoryMetaData.model_construct( |
| 258 | + prefix=S3ObjectPrefix(subfolder["Prefix"], size=None) |
| 259 | + ) |
| 260 | + for subfolder in list_subfolders |
| 261 | + if "Prefix" in subfolder |
| 262 | + ) |
| 263 | + if "Contents" in listed_objects: |
| 264 | + found_objects.extend( |
| 265 | + S3MetaData.from_botocore_list_objects(obj) |
| 266 | + for obj in listed_objects["Contents"] |
| 267 | + ) |
| 268 | + next_cursor = None |
| 269 | + if listed_objects["IsTruncated"]: |
| 270 | + next_cursor = listed_objects["NextContinuationToken"] |
| 271 | + return found_objects, next_cursor |
171 | 272 |
|
172 | 273 | @s3_exception_handler_async_gen(_logger) |
173 | 274 | async def list_objects_paginated( |
@@ -459,7 +560,7 @@ async def copy_objects_recursively( |
459 | 560 | dst_metadata = await self.get_directory_metadata( |
460 | 561 | bucket=bucket, prefix=dst_prefix |
461 | 562 | ) |
462 | | - if dst_metadata.size > 0: |
| 563 | + if dst_metadata.size and dst_metadata.size > 0: |
463 | 564 | raise S3DestinationNotEmptyError(dst_prefix=dst_prefix) |
464 | 565 | await limited_gather( |
465 | 566 | *[ |
|
0 commit comments