Skip to content

Commit 32797f3

Browse files
committed
Add region, extra_headers and extra_query_params
Now all APIs accepts region, extra_headers and extra_query_params additional arguments for more control. Signed-off-by: Bala.FA <bala@minio.io>
1 parent 8a1d738 commit 32797f3

27 files changed

+2334
-870
lines changed

minio/api.py

Lines changed: 1254 additions & 473 deletions
Large diffs are not rendered by default.

minio/checksum.py

Lines changed: 419 additions & 0 deletions
Large diffs are not rendered by default.

minio/commonconfig.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,71 @@ def check_status(status: str):
264264
raise ValueError("status must be 'Enabled' or 'Disabled'")
265265

266266

267+
@dataclass(frozen=True)
268+
class SourceObject:
269+
"""Source object for copy and compose object."""
270+
bucket_name: str
271+
object_name: str
272+
version_id: Optional[str] = None
273+
ssec: Optional[SseCustomerKey] = None
274+
offset: int = 0
275+
length: int = 0
276+
match_etag: Optional[str] = None
277+
not_match_etag: Optional[str] = None
278+
modified_since: Optional[datetime] = None
279+
unmodified_since: Optional[datetime] = None
280+
fetch_checksum: bool = False
281+
region: Optional[str] = None
282+
283+
def __post_init__(self):
284+
if (
285+
self.ssec is not None and
286+
not isinstance(self.ssec, SseCustomerKey)
287+
):
288+
raise ValueError("ssec must be SseCustomerKey type")
289+
if self.offset < 0:
290+
raise ValueError("offset should be zero or greater")
291+
if self.length <= 0:
292+
raise ValueError("length should be greater than zero")
293+
if self.match_etag is not None and self.match_etag == "":
294+
raise ValueError("match_etag must not be empty")
295+
if self.not_match_etag is not None and self.not_match_etag == "":
296+
raise ValueError("not_match_etag must not be empty")
297+
if (
298+
self.modified_since is not None and
299+
not isinstance(self.modified_since, datetime)
300+
):
301+
raise ValueError("modified_since must be datetime type")
302+
if (
303+
self.unmodified_since is not None and
304+
not isinstance(self.unmodified_since, datetime)
305+
):
306+
raise ValueError("unmodified_since must be datetime type")
307+
308+
def gen_copy_headers(self) -> dict[str, str]:
309+
"""Generate copy source headers."""
310+
copy_source = quote("/" + self.bucket_name + "/" + self.object_name)
311+
if self.version_id:
312+
copy_source += "?versionId=" + quote(self.version_id)
313+
314+
headers = {"x-amz-copy-source": copy_source}
315+
if self.ssec:
316+
headers.update(self.ssec.copy_headers())
317+
if self.match_etag:
318+
headers["x-amz-copy-source-if-match"] = self.match_etag
319+
if self.not_match_etag:
320+
headers["x-amz-copy-source-if-none-match"] = self.not_match_etag
321+
if self.modified_since:
322+
headers["x-amz-copy-source-if-modified-since"] = (
323+
to_http_header(self.modified_since)
324+
)
325+
if self.unmodified_since:
326+
headers["x-amz-copy-source-if-unmodified-since"] = (
327+
to_http_header(self.unmodified_since)
328+
)
329+
return headers
330+
331+
267332
@dataclass
268333
class ObjectConditionalReadArgs(ABC):
269334
"""Base argument class holds condition properties for reading object."""

minio/credentials/providers.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from xml.etree import ElementTree as ET
3636

3737
import certifi
38+
from urllib3._collections import HTTPHeaderDict
3839
from urllib3.poolmanager import PoolManager
3940

4041
try:
@@ -75,7 +76,7 @@ def _urlopen(
7576
method: str,
7677
url: str,
7778
body: Optional[str | bytes] = None,
78-
headers: Optional[dict[str, str | list[str] | tuple[str]]] = None,
79+
headers: Optional[HTTPHeaderDict] = None,
7980
) -> BaseHTTPResponse:
8081
"""Wrapper of urlopen() handles HTTP status code."""
8182
res = http_client.urlopen(method, url, body=body, headers=headers)
@@ -167,15 +168,16 @@ def retrieve(self) -> Credentials:
167168
return self._credentials
168169

169170
utctime = utcnow()
171+
headers = HTTPHeaderDict({
172+
"Content-Type": "application/x-www-form-urlencoded",
173+
"Host": self._host,
174+
"X-Amz-Date": to_amz_date(utctime),
175+
})
170176
headers = sign_v4_sts(
171177
method="POST",
172178
url=self._url,
173179
region=self._region,
174-
headers={
175-
"Content-Type": "application/x-www-form-urlencoded",
176-
"Host": self._host,
177-
"X-Amz-Date": to_amz_date(utctime),
178-
},
180+
headers=headers,
179181
credentials=Credentials(
180182
access_key=self._access_key,
181183
secret_key=self._secret_key,
@@ -438,7 +440,7 @@ def __init__(
438440
def fetch(
439441
self,
440442
url: str,
441-
headers: Optional[dict[str, str | list[str] | tuple[str]]] = None,
443+
headers: Optional[HTTPHeaderDict] = None,
442444
) -> Credentials:
443445
"""Fetch credentials from EC2/ECS."""
444446
res = _urlopen(self._http_client, "GET", url, headers=headers)
@@ -484,11 +486,14 @@ def retrieve(self) -> Credentials:
484486
self._credentials = provider.retrieve()
485487
return cast(Credentials, self._credentials)
486488

487-
headers: Optional[dict[str, str | list[str] | tuple[str]]] = None
489+
headers: Optional[HTTPHeaderDict] = None
488490
if self._relative_uri:
489491
if not url:
490492
url = "http://169.254.170.2" + self._relative_uri
491-
headers = {"Authorization": self._token} if self._token else None
493+
headers = (
494+
HTTPHeaderDict({"Authorization": self._token})
495+
if self._token else None
496+
)
492497
elif self._full_uri:
493498
token = self._token
494499
if self._token_file:
@@ -499,20 +504,28 @@ def retrieve(self) -> Credentials:
499504
if not url:
500505
url = self._full_uri
501506
_check_loopback_host(url)
502-
headers = {"Authorization": token} if token else None
507+
headers = (
508+
HTTPHeaderDict({"Authorization": token}) if token else None
509+
)
503510
else:
504511
if not url:
505512
url = "http://169.254.169.254"
506513

507514
# Get IMDS Token
515+
headers = HTTPHeaderDict(
516+
{"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
517+
)
508518
res = _urlopen(
509519
self._http_client,
510520
"PUT",
511521
url+"/latest/api/token",
512-
headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
522+
headers=headers,
513523
)
514524
token = res.data.decode("utf-8")
515-
headers = {"X-aws-ec2-metadata-token": token} if token else None
525+
headers = (
526+
HTTPHeaderDict({"X-aws-ec2-metadata-token": token})
527+
if token else None
528+
)
516529

517530
# Get role name
518531
url = urlunsplit(

minio/datatypes.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141

4242
from .commonconfig import Tags
4343
from .credentials import Credentials
44-
from .helpers import check_bucket_name
44+
from .helpers import HTTPQueryDict, check_bucket_name
4545
from .signer import get_credential_string, post_presign_v4
4646
from .time import from_iso8601utc, to_amz_date, to_iso8601utc
4747
from .xml import find, findall, findtext
@@ -67,10 +67,19 @@ class Bucket:
6767
class ListAllMyBucketsResult:
6868
"""LissBuckets API result."""
6969
buckets: list[Bucket]
70+
prefix: Optional[str]
71+
continuation_token: Optional[str]
72+
owner_id: Optional[str] = None
73+
owner_name: Optional[str] = None
7074

7175
@classmethod
7276
def fromxml(cls: Type[A], element: ET.Element) -> A:
7377
"""Create new object with values from XML element."""
78+
prefix = findtext(element, "Prefix")
79+
continuation_token = findtext(element, "ContinuationToken")
80+
owner = find(element, "Owner")
81+
owner_id = None if owner is None else findtext(owner, "ID")
82+
owner_name = None if owner is None else findtext(owner, "DisplayName")
7483
element = cast(ET.Element, find(element, "Buckets", True))
7584
buckets = []
7685
elements = findall(element, "Bucket")
@@ -81,7 +90,13 @@ def fromxml(cls: Type[A], element: ET.Element) -> A:
8190
name,
8291
from_iso8601utc(creation_date) if creation_date else None,
8392
))
84-
return cls(buckets)
93+
return cls(
94+
buckets=buckets,
95+
prefix=prefix,
96+
continuation_token=continuation_token,
97+
owner_id=owner_id,
98+
owner_name=owner_name,
99+
)
85100

86101

87102
B = TypeVar("B", bound="Object")
@@ -232,15 +247,15 @@ def parse_list_objects(
232247
class CompleteMultipartUploadResult:
233248
"""CompleteMultipartUpload API result."""
234249

235-
http_headers: HTTPHeaderDict
250+
headers: HTTPHeaderDict
236251
bucket_name: Optional[str] = None
237252
object_name: Optional[str] = None
238253
location: Optional[str] = None
239254
etag: Optional[str] = None
240255
version_id: Optional[str] = None
241256

242257
def __init__(self, response: BaseHTTPResponse):
243-
object.__setattr__(self, "http_headers", response.headers)
258+
object.__setattr__(self, "headers", response.headers)
244259
element = ET.fromstring(response.data.decode())
245260
object.__setattr__(self, "bucket_name", findtext(element, "Bucket"))
246261
object.__setattr__(self, "object_name", findtext(element, "Key"))
@@ -751,16 +766,15 @@ class SiteReplicationStatusOptions:
751766
entity: Optional[str] = None
752767
entity_value: Optional[str] = None
753768

754-
def to_query_params(self) -> dict[str, str]:
769+
def to_query_params(self) -> HTTPQueryDict:
755770
"""Convert this options to query parameters."""
756-
params = {
757-
"buckets": str(self.buckets).lower(),
758-
"policies": str(self.policies).lower(),
759-
"users": str(self.users).lower(),
760-
"groups": str(self.groups).lower(),
761-
"metrics": str(self.metrics).lower(),
762-
"showDeleted": str(self.show_deleted).lower(),
763-
}
771+
params = HTTPQueryDict()
772+
params["buckets"] = str(self.buckets).lower()
773+
params["policies"] = str(self.policies).lower()
774+
params["users"] = str(self.users).lower()
775+
params["groups"] = str(self.groups).lower()
776+
params["metrics"] = str(self.metrics).lower()
777+
params["showDeleted"] = str(self.show_deleted).lower()
764778
if self.entity and self.entity_value:
765779
params["entity"] = self.entity
766780
params["entityvalue"] = self.entity_value

0 commit comments

Comments
 (0)