pos3 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
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.
- pos3/__init__.py +28 -9
- {pos3-0.2.0.dist-info → pos3-0.2.2.dist-info}/METADATA +1 -1
- pos3-0.2.2.dist-info/RECORD +6 -0
- {pos3-0.2.0.dist-info → pos3-0.2.2.dist-info}/WHEEL +1 -1
- pos3-0.2.0.dist-info/RECORD +0 -6
- {pos3-0.2.0.dist-info → pos3-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {pos3-0.2.0.dist-info → pos3-0.2.2.dist-info}/top_level.txt +0 -0
pos3/__init__.py
CHANGED
|
@@ -142,6 +142,17 @@ def _s3_paths_conflict(left: str, right: str) -> bool:
|
|
|
142
142
|
return left_norm.startswith(right_norm + "/") or right_norm.startswith(left_norm + "/")
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
def _make_s3_key(prefix: str, info: FileInfo) -> str:
|
|
146
|
+
"""Build the canonical S3 key from a prefix and FileInfo, including trailing '/' for directories."""
|
|
147
|
+
if info.relative_path:
|
|
148
|
+
key = prefix + "/" + info.relative_path if prefix else info.relative_path
|
|
149
|
+
else:
|
|
150
|
+
key = prefix
|
|
151
|
+
if info.is_dir and not key.endswith("/"):
|
|
152
|
+
key += "/"
|
|
153
|
+
return key
|
|
154
|
+
|
|
155
|
+
|
|
145
156
|
def _process_futures(futures, operation: str) -> None:
|
|
146
157
|
for future in futures:
|
|
147
158
|
try:
|
|
@@ -618,12 +629,12 @@ class _Mirror:
|
|
|
618
629
|
)
|
|
619
630
|
|
|
620
631
|
for info in to_copy:
|
|
621
|
-
s3_key = prefix
|
|
632
|
+
s3_key = _make_s3_key(prefix, info)
|
|
622
633
|
to_put.append((info, local_path, bucket, s3_key, profile))
|
|
623
634
|
total_bytes += info.size
|
|
624
635
|
|
|
625
636
|
for info in to_delete if delete else []:
|
|
626
|
-
s3_key = prefix
|
|
637
|
+
s3_key = _make_s3_key(prefix, info)
|
|
627
638
|
to_remove.append((bucket, s3_key, profile))
|
|
628
639
|
|
|
629
640
|
if to_put:
|
|
@@ -679,7 +690,7 @@ class _Mirror:
|
|
|
679
690
|
total_bytes = 0
|
|
680
691
|
|
|
681
692
|
for info in to_copy:
|
|
682
|
-
s3_key = prefix
|
|
693
|
+
s3_key = _make_s3_key(prefix, info)
|
|
683
694
|
to_put.append((info, bucket, s3_key, local_path))
|
|
684
695
|
total_bytes += info.size
|
|
685
696
|
|
|
@@ -709,26 +720,35 @@ class _Mirror:
|
|
|
709
720
|
def _list_s3_objects(self, bucket: str, key: str, profile: Profile | None = None) -> Iterator[dict]:
|
|
710
721
|
logger.debug("Listing S3 objects: bucket=%s, key=%s", bucket, key)
|
|
711
722
|
client = self._get_client(profile)
|
|
712
|
-
|
|
713
|
-
#
|
|
714
|
-
|
|
723
|
+
|
|
724
|
+
# Determine the listing prefix - ensure it ends with "/" for directory-like operations
|
|
725
|
+
# This prevents "droid/recovery" from matching "droid/recovery_towels"
|
|
726
|
+
list_prefix = key
|
|
727
|
+
|
|
728
|
+
# If key doesn't end with "/", try to fetch it as a single object first
|
|
729
|
+
if key and not key.endswith("/"):
|
|
715
730
|
try:
|
|
716
731
|
obj = client.head_object(Bucket=bucket, Key=key)
|
|
717
732
|
except ClientError as exc:
|
|
718
733
|
error_code = exc.response["Error"]["Code"]
|
|
719
734
|
if error_code != "404":
|
|
720
735
|
raise
|
|
736
|
+
# Not a single file - treat as directory by adding "/"
|
|
737
|
+
list_prefix = key + "/"
|
|
721
738
|
else:
|
|
739
|
+
# Found single object
|
|
722
740
|
logger.debug("Found single object via head_object: %s", key)
|
|
723
741
|
if "ContentLength" in obj and "Size" not in obj:
|
|
724
742
|
obj["Size"] = obj["ContentLength"]
|
|
725
743
|
yield {**obj, "Key": key}
|
|
726
744
|
return
|
|
745
|
+
# If key already ends with "/", skip head_object - it's clearly a directory prefix
|
|
727
746
|
|
|
747
|
+
# List with the directory prefix (guaranteed to end with "/")
|
|
728
748
|
paginator = client.get_paginator("list_objects_v2")
|
|
729
|
-
for page in paginator.paginate(Bucket=bucket, Prefix=
|
|
749
|
+
for page in paginator.paginate(Bucket=bucket, Prefix=list_prefix):
|
|
730
750
|
objects = page.get("Contents", [])
|
|
731
|
-
logger.debug("Listed %d objects with prefix %s", len(objects),
|
|
751
|
+
logger.debug("Listed %d objects with prefix %s", len(objects), list_prefix)
|
|
732
752
|
yield from objects
|
|
733
753
|
|
|
734
754
|
def _scan_s3(self, bucket: str, prefix: str, profile: Profile | None = None) -> Iterator[FileInfo]:
|
|
@@ -773,7 +793,6 @@ class _Mirror:
|
|
|
773
793
|
try:
|
|
774
794
|
client = self._get_client(profile)
|
|
775
795
|
if info.is_dir:
|
|
776
|
-
key += "/" if not key.endswith("/") else ""
|
|
777
796
|
client.put_object(Bucket=bucket, Key=key, Body=b"")
|
|
778
797
|
else:
|
|
779
798
|
file_path = local_path / info.relative_path if info.relative_path else local_path
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pos3/__init__.py,sha256=clPuf7Qs8GTi8wWxPNEDXMmjxGkYd8on-fYGUPQx5ls,37934
|
|
2
|
+
pos3-0.2.2.dist-info/licenses/LICENSE,sha256=e815_YqPTxHS3WrNI7dotEuLkgHFAgsf9avLhDYBj9s,11354
|
|
3
|
+
pos3-0.2.2.dist-info/METADATA,sha256=ZlcQCpVWamNY-of0jnGibWlgVzS-Bh6ONTQeYxDP1Xo,8637
|
|
4
|
+
pos3-0.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
pos3-0.2.2.dist-info/top_level.txt,sha256=JWOpXHz1F6cbH0nfanGWLaozt8RJFRmv5H3eKkxz7e8,5
|
|
6
|
+
pos3-0.2.2.dist-info/RECORD,,
|
pos3-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
pos3/__init__.py,sha256=ByRzIJ3ggRKFU6j8HtzDv7D3RW_6NZkbjp3etoMilys,37261
|
|
2
|
-
pos3-0.2.0.dist-info/licenses/LICENSE,sha256=e815_YqPTxHS3WrNI7dotEuLkgHFAgsf9avLhDYBj9s,11354
|
|
3
|
-
pos3-0.2.0.dist-info/METADATA,sha256=LF6o86VvogFSbkcsfy39yVmf-37IjbZdqJZ3guem4CA,8637
|
|
4
|
-
pos3-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
pos3-0.2.0.dist-info/top_level.txt,sha256=JWOpXHz1F6cbH0nfanGWLaozt8RJFRmv5H3eKkxz7e8,5
|
|
6
|
-
pos3-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|