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 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 + ("/" + info.relative_path if info.relative_path else "")
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 + ("/" + info.relative_path if info.relative_path else "")
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 + ("/" + info.relative_path if info.relative_path else "")
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
- # Skip head_object for directory-like keys ending with '/'
713
- # as we want to list contents, not check if the directory marker exists
714
- if not key.endswith("/"):
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=key):
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), key)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pos3
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: S3 Simple Sync - Make using S3 as simple as using local files
5
5
  Author-email: Positronic Robotics <hi@positronic.ro>
6
6
  License: Apache-2.0
@@ -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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,