miniopy-async 1.23.2__tar.gz → 1.23.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/PKG-INFO +1 -1
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/__init__.py +1 -1
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/api.py +311 -5
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/datatypes.py +98 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/helpers.py +55 -30
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/minioadmin.py +10 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/pyproject.toml +1 -1
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/LICENSE +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/README.md +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/commonconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/credentials/__init__.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/credentials/credentials.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/credentials/providers.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/crypto.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/deleteobjects.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/error.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/legalhold.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/lifecycleconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/notificationconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/objectlockconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/py.typed +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/replicationconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/retention.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/select.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/signer.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/sse.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/sseconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/tagging.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/time.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/versioningconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/xml.py +0 -0
|
@@ -36,6 +36,7 @@ from urllib.parse import urlunsplit
|
|
|
36
36
|
from xml.etree import ElementTree as ET
|
|
37
37
|
|
|
38
38
|
import certifi
|
|
39
|
+
import yarl
|
|
39
40
|
from aiohttp import ClientResponse, ClientSession, ClientTimeout, TCPConnector
|
|
40
41
|
from aiohttp.typedefs import LooseHeaders
|
|
41
42
|
from aiohttp_retry import ExponentialRetry, RetryClient
|
|
@@ -54,6 +55,7 @@ from .datatypes import (
|
|
|
54
55
|
ListMultipartUploadsResult,
|
|
55
56
|
ListObjects,
|
|
56
57
|
ListPartsResult,
|
|
58
|
+
MultipartUploader,
|
|
57
59
|
Object,
|
|
58
60
|
Part,
|
|
59
61
|
PostPolicy,
|
|
@@ -361,9 +363,10 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
361
363
|
self._ensure_session()
|
|
362
364
|
session = cast(ClientSession | RetryClient, self._session)
|
|
363
365
|
|
|
366
|
+
yarl_url = yarl.URL(urlunsplit(url), encoded=True)
|
|
364
367
|
response = await session.request(
|
|
365
368
|
method,
|
|
366
|
-
|
|
369
|
+
yarl_url,
|
|
367
370
|
data=body,
|
|
368
371
|
headers=http_headers,
|
|
369
372
|
)
|
|
@@ -2296,11 +2299,140 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2296
2299
|
element = ET.fromstring(await response.text())
|
|
2297
2300
|
return cast(str, findtext(element, "UploadId", True))
|
|
2298
2301
|
|
|
2302
|
+
def multipart_uploader(
|
|
2303
|
+
self,
|
|
2304
|
+
bucket_name: str,
|
|
2305
|
+
object_name: str,
|
|
2306
|
+
content_type: str = "application/octet-stream",
|
|
2307
|
+
metadata: DictType | None = None,
|
|
2308
|
+
sse: Sse | None = None,
|
|
2309
|
+
tags: Tags | None = None,
|
|
2310
|
+
retention: Retention | None = None,
|
|
2311
|
+
legal_hold: bool = False,
|
|
2312
|
+
extra_headers: DictType | None = None,
|
|
2313
|
+
) -> MultipartUploader:
|
|
2314
|
+
"""
|
|
2315
|
+
Create a multipart uploader for large data.
|
|
2316
|
+
|
|
2317
|
+
:param str bucket_name: Name of the bucket.
|
|
2318
|
+
:param str object_name: Object name in the bucket.
|
|
2319
|
+
:param str content_type: Content type of the object.
|
|
2320
|
+
:param DictType | None metadata: Any additional metadata to be uploaded along
|
|
2321
|
+
with your PUT request.
|
|
2322
|
+
:param Sse | None sse: Server-side encryption.
|
|
2323
|
+
:param Tags | None tags: :class:`Tags` for the object.
|
|
2324
|
+
:param Retention | None retention: :class:`Retention` configuration object.
|
|
2325
|
+
:param bool legal_hold: Flag to set legal hold for the object.
|
|
2326
|
+
:param DictType | None extra_headers: Any additional headers to be added with GET request.
|
|
2327
|
+
:return: :class:`MultipartUploader` object.
|
|
2328
|
+
:rtype: MultipartUploader
|
|
2329
|
+
|
|
2330
|
+
.. code-block:: python
|
|
2331
|
+
|
|
2332
|
+
import asyncio
|
|
2333
|
+
|
|
2334
|
+
from miniopy_async import Minio
|
|
2335
|
+
|
|
2336
|
+
client = Minio(
|
|
2337
|
+
"play.min.io",
|
|
2338
|
+
access_key="Q3AM3UQ867SPQQA43P2F",
|
|
2339
|
+
secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
|
|
2340
|
+
secure=True, # http for False, https for True
|
|
2341
|
+
)
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
async def main():
|
|
2345
|
+
# Use context manager
|
|
2346
|
+
print("example one")
|
|
2347
|
+
async with client.multipart_uploader(
|
|
2348
|
+
"my-bucket",
|
|
2349
|
+
"my-object",
|
|
2350
|
+
) as uploader:
|
|
2351
|
+
await uploader.upload_part(b"hello" * 1024 * 1024, 1)
|
|
2352
|
+
await uploader.upload_part(b"world", 2)
|
|
2353
|
+
result = uploader.result
|
|
2354
|
+
print(
|
|
2355
|
+
"created {0} object; etag: {1}, version-id: {2}".format(
|
|
2356
|
+
result.object_name,
|
|
2357
|
+
result.etag,
|
|
2358
|
+
result.version_id,
|
|
2359
|
+
),
|
|
2360
|
+
)
|
|
2361
|
+
|
|
2362
|
+
# Use directly
|
|
2363
|
+
print("example two")
|
|
2364
|
+
uploader = client.multipart_uploader(
|
|
2365
|
+
"my-bucket",
|
|
2366
|
+
"my-object",
|
|
2367
|
+
)
|
|
2368
|
+
await uploader.upload_part(b"hello" * 1024 * 1024, 1)
|
|
2369
|
+
await uploader.upload_part(b"world", 2)
|
|
2370
|
+
result = await uploader.complete()
|
|
2371
|
+
print(
|
|
2372
|
+
"created {0} object; etag: {1}, version-id: {2}".format(
|
|
2373
|
+
result.object_name,
|
|
2374
|
+
result.etag,
|
|
2375
|
+
result.version_id,
|
|
2376
|
+
),
|
|
2377
|
+
)
|
|
2378
|
+
|
|
2379
|
+
# Abort
|
|
2380
|
+
print("example three")
|
|
2381
|
+
uploader = client.multipart_uploader(
|
|
2382
|
+
"my-bucket",
|
|
2383
|
+
"my-object",
|
|
2384
|
+
)
|
|
2385
|
+
await uploader.upload_part(b"hello", 1)
|
|
2386
|
+
await uploader.abort()
|
|
2387
|
+
|
|
2388
|
+
# Parallel upload
|
|
2389
|
+
print("example four")
|
|
2390
|
+
tasks = []
|
|
2391
|
+
async with client.multipart_uploader(
|
|
2392
|
+
"my-bucket",
|
|
2393
|
+
"my-object",
|
|
2394
|
+
) as uploader:
|
|
2395
|
+
for i in range(1, 11):
|
|
2396
|
+
data = (f"part{i}" * 1024 * 1024).encode("utf-8")
|
|
2397
|
+
tasks.append(uploader.upload_part(data, i))
|
|
2398
|
+
await asyncio.gather(*tasks)
|
|
2399
|
+
result = uploader.result
|
|
2400
|
+
print(
|
|
2401
|
+
"created {0} object; etag: {1}, version-id: {2}".format(
|
|
2402
|
+
result.object_name,
|
|
2403
|
+
result.etag,
|
|
2404
|
+
result.version_id,
|
|
2405
|
+
),
|
|
2406
|
+
)
|
|
2407
|
+
|
|
2408
|
+
|
|
2409
|
+
asyncio.run(main())
|
|
2410
|
+
"""
|
|
2411
|
+
check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host)
|
|
2412
|
+
check_object_name(object_name)
|
|
2413
|
+
check_sse(sse)
|
|
2414
|
+
if tags is not None and not isinstance(tags, Tags):
|
|
2415
|
+
raise ValueError("tags must be Tags type")
|
|
2416
|
+
if retention is not None and not isinstance(retention, Retention):
|
|
2417
|
+
raise ValueError("retention must be Retention type")
|
|
2418
|
+
|
|
2419
|
+
headers = genheaders(metadata, sse, tags, retention, legal_hold)
|
|
2420
|
+
headers["Content-Type"] = content_type or "application/octet-stream"
|
|
2421
|
+
if extra_headers:
|
|
2422
|
+
headers.update(extra_headers)
|
|
2423
|
+
|
|
2424
|
+
return MultipartUploader(
|
|
2425
|
+
self,
|
|
2426
|
+
bucket_name,
|
|
2427
|
+
object_name,
|
|
2428
|
+
headers,
|
|
2429
|
+
)
|
|
2430
|
+
|
|
2299
2431
|
async def _put_object(
|
|
2300
2432
|
self,
|
|
2301
2433
|
bucket_name: str,
|
|
2302
2434
|
object_name: str,
|
|
2303
|
-
data: Substream,
|
|
2435
|
+
data: bytes | BytesIO | Substream,
|
|
2304
2436
|
headers: DictType | None,
|
|
2305
2437
|
query_params: DictType | None = None,
|
|
2306
2438
|
) -> ObjectWriteResult:
|
|
@@ -2326,7 +2458,7 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2326
2458
|
self,
|
|
2327
2459
|
bucket_name: str,
|
|
2328
2460
|
object_name: str,
|
|
2329
|
-
data: Substream,
|
|
2461
|
+
data: bytes | BytesIO | Substream,
|
|
2330
2462
|
headers: DictType | None,
|
|
2331
2463
|
upload_id: str,
|
|
2332
2464
|
part_number: int,
|
|
@@ -2371,6 +2503,7 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2371
2503
|
tags: Tags | None = None,
|
|
2372
2504
|
retention: Retention | None = None,
|
|
2373
2505
|
legal_hold: bool = False,
|
|
2506
|
+
write_offset: int | None = None,
|
|
2374
2507
|
) -> ObjectWriteResult:
|
|
2375
2508
|
"""
|
|
2376
2509
|
Uploads data from a stream to an object in a bucket.
|
|
@@ -2389,6 +2522,7 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2389
2522
|
:param Tags | None tags: :class:`Tags` for the object.
|
|
2390
2523
|
:param Retention | None retention: :class:`Retention` configuration object.
|
|
2391
2524
|
:param bool legal_hold: Flag to set legal hold for the object.
|
|
2525
|
+
:param int | None write_offset: Offset byte for appending data to existing object.
|
|
2392
2526
|
:return: :class:`ObjectWriteResult` object.
|
|
2393
2527
|
:rtype: ObjectWriteResult
|
|
2394
2528
|
:raise ValueError: If tags is not of type :class:`Tags`.
|
|
@@ -2493,6 +2627,12 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2493
2627
|
raise ValueError("retention must be Retention type")
|
|
2494
2628
|
if not callable(getattr(data, "read")):
|
|
2495
2629
|
raise ValueError("input data must have callable read()")
|
|
2630
|
+
if write_offset is not None:
|
|
2631
|
+
if write_offset < 0:
|
|
2632
|
+
raise ValueError("write offset should not be negative")
|
|
2633
|
+
if length < 0:
|
|
2634
|
+
raise ValueError("length must be provided for write offset")
|
|
2635
|
+
part_size = length if length > MIN_PART_SIZE else MIN_PART_SIZE
|
|
2496
2636
|
part_size, part_count = get_part_info(length, part_size)
|
|
2497
2637
|
if progress:
|
|
2498
2638
|
# Set progress bar length and object name before upload
|
|
@@ -2500,6 +2640,8 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2500
2640
|
|
|
2501
2641
|
headers = genheaders(metadata, sse, tags, retention, legal_hold)
|
|
2502
2642
|
headers["Content-Type"] = content_type or "application/octet-stream"
|
|
2643
|
+
if write_offset:
|
|
2644
|
+
headers["x-amz-write-offset-bytes"] = str(write_offset)
|
|
2503
2645
|
|
|
2504
2646
|
object_size = length
|
|
2505
2647
|
uploaded_size = 0
|
|
@@ -2606,6 +2748,169 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2606
2748
|
)
|
|
2607
2749
|
raise exc
|
|
2608
2750
|
|
|
2751
|
+
async def append_object(
|
|
2752
|
+
self,
|
|
2753
|
+
bucket_name: str,
|
|
2754
|
+
object_name: str,
|
|
2755
|
+
data: BinaryIO,
|
|
2756
|
+
length: int,
|
|
2757
|
+
chunk_size: int | None = None,
|
|
2758
|
+
progress: ProgressType | None = None,
|
|
2759
|
+
extra_headers: DictType | None = None,
|
|
2760
|
+
) -> ObjectWriteResult:
|
|
2761
|
+
"""
|
|
2762
|
+
Appends from a stream to existing object in a bucket.
|
|
2763
|
+
|
|
2764
|
+
:param str bucket_name: Name of the bucket.
|
|
2765
|
+
:param str object_name: Object name in the bucket.
|
|
2766
|
+
:param BinaryIO data: An object having callable read() returning bytes object.
|
|
2767
|
+
:param int length: Data size; -1 for unknown size.
|
|
2768
|
+
:param int chunk_size: Chunk size to optimize uploads.
|
|
2769
|
+
:param ProgressType | None progress: A progress object.
|
|
2770
|
+
:param DictType | None extra_headers: Any additional headers to be added with GET request.
|
|
2771
|
+
:return: :class:`ObjectWriteResult` object.
|
|
2772
|
+
:rtype: ObjectWriteResult
|
|
2773
|
+
:raise ValueError: If length is 0.
|
|
2774
|
+
:raise ValueError: If chunk_size is less than 5 MiB or greater than 5 GiB.
|
|
2775
|
+
:raise IOError: If stream doesn't have enough data to read.
|
|
2776
|
+
|
|
2777
|
+
.. code-block:: python
|
|
2778
|
+
|
|
2779
|
+
import asyncio
|
|
2780
|
+
import io
|
|
2781
|
+
from urllib.request import urlopen
|
|
2782
|
+
|
|
2783
|
+
from miniopy_async import Minio
|
|
2784
|
+
|
|
2785
|
+
client = Minio(
|
|
2786
|
+
"play.min.io",
|
|
2787
|
+
access_key="Q3AM3UQ867SPQQA43P2F",
|
|
2788
|
+
secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
|
|
2789
|
+
secure=True, # http for False, https for True
|
|
2790
|
+
)
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
async def main():
|
|
2794
|
+
# Upload data.
|
|
2795
|
+
result = await client.put_object(
|
|
2796
|
+
"my-bucket",
|
|
2797
|
+
"my-object",
|
|
2798
|
+
io.BytesIO(b"hello, "),
|
|
2799
|
+
7,
|
|
2800
|
+
)
|
|
2801
|
+
print(f"created {result.object_name} object; etag: {result.etag}")
|
|
2802
|
+
|
|
2803
|
+
# Append data.
|
|
2804
|
+
result = await client.append_object(
|
|
2805
|
+
"my-bucket",
|
|
2806
|
+
"my-object",
|
|
2807
|
+
io.BytesIO(b"world"),
|
|
2808
|
+
5,
|
|
2809
|
+
)
|
|
2810
|
+
print(f"appended {result.object_name} object; etag: {result.etag}")
|
|
2811
|
+
|
|
2812
|
+
# Append data in chunks.
|
|
2813
|
+
data = urlopen(
|
|
2814
|
+
"https://www.kernel.org/pub/linux/kernel/v6.x/linux-6.13.12.tar.xz",
|
|
2815
|
+
)
|
|
2816
|
+
result = await client.append_object(
|
|
2817
|
+
"my-bucket",
|
|
2818
|
+
"my-object",
|
|
2819
|
+
data,
|
|
2820
|
+
148611164,
|
|
2821
|
+
5 * 1024 * 1024,
|
|
2822
|
+
)
|
|
2823
|
+
print(f"appended {result.object_name} object; etag: {result.etag}")
|
|
2824
|
+
|
|
2825
|
+
# Append unknown sized data.
|
|
2826
|
+
data = urlopen(
|
|
2827
|
+
"https://www.kernel.org/pub/linux/kernel/v6.x/linux-6.14.3.tar.xz",
|
|
2828
|
+
)
|
|
2829
|
+
result = await client.append_object(
|
|
2830
|
+
"my-bucket",
|
|
2831
|
+
"my-object",
|
|
2832
|
+
data,
|
|
2833
|
+
149426584,
|
|
2834
|
+
5 * 1024 * 1024,
|
|
2835
|
+
)
|
|
2836
|
+
print(f"appended {result.object_name} object; etag: {result.etag}")
|
|
2837
|
+
|
|
2838
|
+
|
|
2839
|
+
asyncio.run(main())
|
|
2840
|
+
"""
|
|
2841
|
+
if length == 0:
|
|
2842
|
+
raise ValueError("length should not be zero")
|
|
2843
|
+
if chunk_size is not None:
|
|
2844
|
+
if chunk_size < MIN_PART_SIZE:
|
|
2845
|
+
raise ValueError("chunk size must be minimum of 5 MiB")
|
|
2846
|
+
if chunk_size > MAX_PART_SIZE:
|
|
2847
|
+
raise ValueError("chunk size must be less than 5 GiB")
|
|
2848
|
+
else:
|
|
2849
|
+
chunk_size = length if length > MIN_PART_SIZE else MIN_PART_SIZE
|
|
2850
|
+
|
|
2851
|
+
chunk_count = -1
|
|
2852
|
+
if length > 0:
|
|
2853
|
+
chunk_count = int(length / chunk_size)
|
|
2854
|
+
if (chunk_count * chunk_size) < length:
|
|
2855
|
+
chunk_count += 1
|
|
2856
|
+
chunk_count = chunk_count or 1
|
|
2857
|
+
|
|
2858
|
+
object_size = length
|
|
2859
|
+
uploaded_size = 0
|
|
2860
|
+
chunk_number = 0
|
|
2861
|
+
stop = False
|
|
2862
|
+
|
|
2863
|
+
stat = await self.stat_object(bucket_name, object_name)
|
|
2864
|
+
write_offset = cast(int, stat.size)
|
|
2865
|
+
upload_result = None
|
|
2866
|
+
|
|
2867
|
+
while not stop:
|
|
2868
|
+
chunk_number += 1
|
|
2869
|
+
if chunk_count > 0:
|
|
2870
|
+
if chunk_number == chunk_count:
|
|
2871
|
+
chunk_size = object_size - uploaded_size
|
|
2872
|
+
stop = True
|
|
2873
|
+
chunk_data = Substream(data, uploaded_size, chunk_size)
|
|
2874
|
+
if chunk_data.size != chunk_size:
|
|
2875
|
+
raise IOError(
|
|
2876
|
+
f"stream having not enough data;"
|
|
2877
|
+
f"expected: {chunk_size}, "
|
|
2878
|
+
f"got: {chunk_data.size} bytes"
|
|
2879
|
+
)
|
|
2880
|
+
else:
|
|
2881
|
+
chunk_data = Substream(data, uploaded_size, chunk_size)
|
|
2882
|
+
if chunk_data.size == 0:
|
|
2883
|
+
break
|
|
2884
|
+
if chunk_data.size < chunk_size:
|
|
2885
|
+
chunk_count = chunk_number
|
|
2886
|
+
stop = True
|
|
2887
|
+
chunk_size = chunk_data.size
|
|
2888
|
+
|
|
2889
|
+
if progress:
|
|
2890
|
+
await progress.update(chunk_size)
|
|
2891
|
+
|
|
2892
|
+
uploaded_size += chunk_size
|
|
2893
|
+
|
|
2894
|
+
headers = extra_headers or {}
|
|
2895
|
+
headers["x-amz-write-offset-bytes"] = str(write_offset)
|
|
2896
|
+
upload_result = await self._put_object(
|
|
2897
|
+
bucket_name,
|
|
2898
|
+
object_name,
|
|
2899
|
+
chunk_data,
|
|
2900
|
+
headers=headers,
|
|
2901
|
+
)
|
|
2902
|
+
write_offset += chunk_size
|
|
2903
|
+
|
|
2904
|
+
upload_result = cast(ObjectWriteResult, upload_result)
|
|
2905
|
+
return ObjectWriteResult(
|
|
2906
|
+
cast(str, upload_result.bucket_name),
|
|
2907
|
+
cast(str, upload_result.object_name),
|
|
2908
|
+
upload_result.version_id,
|
|
2909
|
+
upload_result.etag,
|
|
2910
|
+
upload_result.http_headers,
|
|
2911
|
+
location=upload_result.location,
|
|
2912
|
+
)
|
|
2913
|
+
|
|
2609
2914
|
def list_objects(
|
|
2610
2915
|
self,
|
|
2611
2916
|
bucket_name: str,
|
|
@@ -4228,9 +4533,10 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
4228
4533
|
metadata["X-Amz-Meta-Snowball-Auto-Extract"] = "true"
|
|
4229
4534
|
|
|
4230
4535
|
name = staging_filename
|
|
4231
|
-
mode = "w:gz" if compression else "w"
|
|
4232
4536
|
fileobj = None if name else BytesIO()
|
|
4233
|
-
with tarfile.open(
|
|
4537
|
+
with tarfile.open(
|
|
4538
|
+
name=name, mode="w:gz" if compression else "w", fileobj=fileobj
|
|
4539
|
+
) as tar:
|
|
4234
4540
|
for obj in object_list:
|
|
4235
4541
|
if obj.filename:
|
|
4236
4542
|
tar.add(obj.filename, obj.object_name)
|
|
@@ -21,6 +21,7 @@ Response of ListBuckets, ListObjects, ListObjectsV2 and ListObjectVersions API.
|
|
|
21
21
|
from __future__ import absolute_import, annotations
|
|
22
22
|
|
|
23
23
|
import base64
|
|
24
|
+
import io
|
|
24
25
|
import itertools
|
|
25
26
|
import json
|
|
26
27
|
from collections import OrderedDict
|
|
@@ -1286,3 +1287,100 @@ class DeleteErrors:
|
|
|
1286
1287
|
async for error in self:
|
|
1287
1288
|
errors.append(error)
|
|
1288
1289
|
return errors
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
class MultipartUploader:
|
|
1293
|
+
def __init__(
|
|
1294
|
+
self,
|
|
1295
|
+
client: Minio,
|
|
1296
|
+
bucket_name: str,
|
|
1297
|
+
object_name: str,
|
|
1298
|
+
headers: DictType,
|
|
1299
|
+
):
|
|
1300
|
+
self._client = client
|
|
1301
|
+
self._bucket_name = bucket_name
|
|
1302
|
+
self._object_name = object_name
|
|
1303
|
+
self._headers = headers
|
|
1304
|
+
self._upload_id = ""
|
|
1305
|
+
self._parts = []
|
|
1306
|
+
self._result: CompleteMultipartUploadResult | None = None
|
|
1307
|
+
|
|
1308
|
+
@property
|
|
1309
|
+
def result(self) -> CompleteMultipartUploadResult:
|
|
1310
|
+
"""
|
|
1311
|
+
Get multipart upload result.
|
|
1312
|
+
|
|
1313
|
+
:return: Multipart upload result.
|
|
1314
|
+
:rtype: CompleteMultipartUploadResult
|
|
1315
|
+
:raises ValueError: If multipart upload is not completed.
|
|
1316
|
+
"""
|
|
1317
|
+
if not self._result:
|
|
1318
|
+
raise ValueError("Multipart upload not completed")
|
|
1319
|
+
return self._result
|
|
1320
|
+
|
|
1321
|
+
async def _init(self):
|
|
1322
|
+
self._upload_id = await self._client._create_multipart_upload(
|
|
1323
|
+
self._bucket_name, self._object_name, self._headers
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
async def complete(self) -> CompleteMultipartUploadResult:
|
|
1327
|
+
"""
|
|
1328
|
+
Complete multipart upload.
|
|
1329
|
+
|
|
1330
|
+
:return: Multipart upload result.
|
|
1331
|
+
:rtype: CompleteMultipartUploadResult
|
|
1332
|
+
"""
|
|
1333
|
+
self._result = await self._client._complete_multipart_upload(
|
|
1334
|
+
self._bucket_name, self._object_name, self._upload_id, self._parts
|
|
1335
|
+
)
|
|
1336
|
+
return self.result
|
|
1337
|
+
|
|
1338
|
+
async def abort(self):
|
|
1339
|
+
"""Abort multipart upload."""
|
|
1340
|
+
await self._client._abort_multipart_upload(
|
|
1341
|
+
self._bucket_name, self._object_name, self._upload_id
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
async def __aenter__(self):
|
|
1345
|
+
await self._init()
|
|
1346
|
+
return self
|
|
1347
|
+
|
|
1348
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
1349
|
+
await self.complete()
|
|
1350
|
+
|
|
1351
|
+
async def upload_part(self, data: bytes | io.BytesIO, part_number: int) -> Part:
|
|
1352
|
+
"""
|
|
1353
|
+
Upload a part of the object.
|
|
1354
|
+
|
|
1355
|
+
The data size must be larger than 5 MiB and less than 5 GiB except the last part.
|
|
1356
|
+
|
|
1357
|
+
:param bytes | io.BytesIO data: Data to upload as part.
|
|
1358
|
+
:param int part_number: Number of the part.
|
|
1359
|
+
:return: Part information.
|
|
1360
|
+
:rtype: Part
|
|
1361
|
+
:raises ValueError: If part_number is not a positive integer.
|
|
1362
|
+
"""
|
|
1363
|
+
if not self._upload_id:
|
|
1364
|
+
await self._init()
|
|
1365
|
+
try:
|
|
1366
|
+
if not isinstance(part_number, int) or part_number < 1:
|
|
1367
|
+
raise ValueError("part_number must be a positive integer")
|
|
1368
|
+
|
|
1369
|
+
etag = await self._client._upload_part(
|
|
1370
|
+
self._bucket_name,
|
|
1371
|
+
self._object_name,
|
|
1372
|
+
data,
|
|
1373
|
+
self._headers,
|
|
1374
|
+
self._upload_id,
|
|
1375
|
+
part_number,
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
part = Part(part_number, etag)
|
|
1379
|
+
self._parts.append(part)
|
|
1380
|
+
|
|
1381
|
+
return part
|
|
1382
|
+
|
|
1383
|
+
except Exception as e:
|
|
1384
|
+
await self.abort()
|
|
1385
|
+
|
|
1386
|
+
raise e
|
|
@@ -26,9 +26,10 @@ import math
|
|
|
26
26
|
import os
|
|
27
27
|
import platform
|
|
28
28
|
import re
|
|
29
|
+
import threading
|
|
29
30
|
import urllib.parse
|
|
30
31
|
from datetime import datetime
|
|
31
|
-
from typing import BinaryIO, Dict, List, Protocol, Tuple, Union
|
|
32
|
+
from typing import BinaryIO, Dict, List, Protocol, Tuple, Union, cast
|
|
32
33
|
|
|
33
34
|
from aiohttp.typedefs import LooseHeaders
|
|
34
35
|
|
|
@@ -865,36 +866,67 @@ class Substream(io.IOBase):
|
|
|
865
866
|
|
|
866
867
|
self._base = base
|
|
867
868
|
self._start = offset
|
|
868
|
-
self._size = self._get_size(part_size)
|
|
869
869
|
self._pos = 0
|
|
870
870
|
self._closed = False
|
|
871
871
|
|
|
872
|
+
self._lock = threading.RLock()
|
|
873
|
+
self._fd: int | None = None
|
|
874
|
+
self._has_pread: bool = False
|
|
875
|
+
try:
|
|
876
|
+
if hasattr(base, "fileno"):
|
|
877
|
+
self._fd = base.fileno()
|
|
878
|
+
self._has_pread = self._fd is not None and hasattr(os, "pread")
|
|
879
|
+
except Exception:
|
|
880
|
+
self._fd = None
|
|
881
|
+
self._has_pread = False
|
|
882
|
+
|
|
883
|
+
self._size = self._get_size(part_size)
|
|
884
|
+
|
|
885
|
+
def _read_at(self, abs_offset: int, n: int) -> bytes:
|
|
886
|
+
"""Read n bytes from the underlying stream at absolute position abs_offset without changing other readers' pointers."""
|
|
887
|
+
if n <= 0:
|
|
888
|
+
return b""
|
|
889
|
+
if self._has_pread:
|
|
890
|
+
# POSIX: thread-safe, does not affect file pointer
|
|
891
|
+
return os.pread(cast(int, self._fd), n, abs_offset)
|
|
892
|
+
# Fallback: save/restore pointer + lock, ensure atomicity within this object
|
|
893
|
+
with self._lock:
|
|
894
|
+
curr = self._base.tell()
|
|
895
|
+
try:
|
|
896
|
+
self._base.seek(abs_offset)
|
|
897
|
+
return self._base.read(n)
|
|
898
|
+
finally:
|
|
899
|
+
try:
|
|
900
|
+
self._base.seek(curr)
|
|
901
|
+
except Exception:
|
|
902
|
+
pass
|
|
903
|
+
|
|
872
904
|
def _get_size(self, part_size: int) -> int:
|
|
873
905
|
"""Get size of the substream using bisection."""
|
|
874
|
-
|
|
875
|
-
|
|
906
|
+
if part_size == 0:
|
|
907
|
+
return 0
|
|
908
|
+
|
|
909
|
+
start = self._start
|
|
910
|
+
end = start + part_size
|
|
876
911
|
|
|
877
|
-
self.
|
|
878
|
-
data = self._base.read(1)
|
|
879
|
-
if data:
|
|
912
|
+
if self._read_at(end - 1, 1):
|
|
880
913
|
return part_size
|
|
881
914
|
|
|
882
|
-
self.
|
|
883
|
-
data = self._base.read(1)
|
|
884
|
-
if not data:
|
|
915
|
+
if not self._read_at(start, 1):
|
|
885
916
|
return 0
|
|
886
917
|
|
|
887
918
|
# bisection
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
919
|
+
lo = start
|
|
920
|
+
hi = end - 1
|
|
921
|
+
while lo <= hi:
|
|
922
|
+
mid = (lo + hi) // 2
|
|
923
|
+
if self._read_at(mid, 1):
|
|
924
|
+
if self._read_at(mid + 1, 1):
|
|
925
|
+
lo = mid + 1
|
|
926
|
+
else:
|
|
927
|
+
return mid - start + 1
|
|
896
928
|
else:
|
|
897
|
-
|
|
929
|
+
hi = mid - 1
|
|
898
930
|
|
|
899
931
|
raise UnreachableError
|
|
900
932
|
|
|
@@ -906,27 +938,20 @@ class Substream(io.IOBase):
|
|
|
906
938
|
def read(self, size: int = -1) -> bytes:
|
|
907
939
|
"""Read data from the substream."""
|
|
908
940
|
self._check_closed()
|
|
909
|
-
self._base.seek(self._start + self._pos)
|
|
910
|
-
|
|
911
941
|
remaining = self._size - self._pos
|
|
912
942
|
if remaining <= 0:
|
|
913
943
|
return b""
|
|
914
944
|
if size < 0 or size > remaining:
|
|
915
945
|
size = remaining
|
|
916
|
-
|
|
946
|
+
abs_off = self._start + self._pos
|
|
947
|
+
data = self._read_at(abs_off, size)
|
|
948
|
+
|
|
917
949
|
self._pos += len(data)
|
|
918
950
|
return data
|
|
919
951
|
|
|
920
952
|
def is_empty(self) -> bool:
|
|
921
953
|
"""Check if the substream is empty."""
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
self.seek(0)
|
|
925
|
-
if not self.read(1):
|
|
926
|
-
return True
|
|
927
|
-
|
|
928
|
-
self._pos = pos
|
|
929
|
-
return False
|
|
954
|
+
return not bool(self._read_at(self._start, 1))
|
|
930
955
|
|
|
931
956
|
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
|
|
932
957
|
"""Seek to a position in the substream."""
|
|
@@ -55,6 +55,7 @@ from .signer import sign_v4_s3
|
|
|
55
55
|
class _COMMAND(Enum):
|
|
56
56
|
"""Admin Command enumerations."""
|
|
57
57
|
|
|
58
|
+
ACCOUNT_INFO = "accountinfo"
|
|
58
59
|
ADD_USER = "add-user"
|
|
59
60
|
USER_INFO = "user-info"
|
|
60
61
|
LIST_USERS = "list-users"
|
|
@@ -324,6 +325,15 @@ class MinioAdmin:
|
|
|
324
325
|
)
|
|
325
326
|
return await response.text()
|
|
326
327
|
|
|
328
|
+
async def account_info(self, prefix_usage: bool = False) -> str:
|
|
329
|
+
"""Get usage information for the authenticating account"""
|
|
330
|
+
response = await self._url_open(
|
|
331
|
+
"GET",
|
|
332
|
+
_COMMAND.ACCOUNT_INFO,
|
|
333
|
+
query_params={"prefix-usage": "true"} if prefix_usage else None,
|
|
334
|
+
)
|
|
335
|
+
return await response.text()
|
|
336
|
+
|
|
327
337
|
async def user_add(self, access_key: str, secret_key: str) -> str:
|
|
328
338
|
"""Create user with access and secret keys"""
|
|
329
339
|
body = json.dumps({"status": "enabled", "secretKey": secret_key}).encode()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|