rclone-api 1.5.50__py3-none-any.whl → 1.5.52__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.
- rclone_api/__init__.py +31 -0
- rclone_api/fs/filesystem.py +32 -8
- rclone_api/rclone_impl.py +72 -9
- rclone_api/s3/api.py +4 -0
- rclone_api/types.py +6 -0
- {rclone_api-1.5.50.dist-info → rclone_api-1.5.52.dist-info}/METADATA +1 -1
- {rclone_api-1.5.50.dist-info → rclone_api-1.5.52.dist-info}/RECORD +11 -11
- {rclone_api-1.5.50.dist-info → rclone_api-1.5.52.dist-info}/WHEEL +0 -0
- {rclone_api-1.5.50.dist-info → rclone_api-1.5.52.dist-info}/entry_points.txt +0 -0
- {rclone_api-1.5.50.dist-info → rclone_api-1.5.52.dist-info}/licenses/LICENSE +0 -0
- {rclone_api-1.5.50.dist-info → rclone_api-1.5.52.dist-info}/top_level.txt +0 -0
rclone_api/__init__.py
CHANGED
@@ -438,6 +438,37 @@ class Rclone:
|
|
438
438
|
"""
|
439
439
|
return self.impl.cleanup(src=src, other_args=other_args)
|
440
440
|
|
441
|
+
def copy_file_s3(
|
442
|
+
self,
|
443
|
+
src: Path,
|
444
|
+
dst: str,
|
445
|
+
verbose: bool | None = None,
|
446
|
+
) -> Exception | None:
|
447
|
+
"""
|
448
|
+
Copy a file to S3.
|
449
|
+
|
450
|
+
Args:
|
451
|
+
src: Local file path
|
452
|
+
dst: Destination path in S3
|
453
|
+
verbose: Whether to show detailed progress
|
454
|
+
|
455
|
+
Returns:
|
456
|
+
None if successful, Exception if an error occurred
|
457
|
+
"""
|
458
|
+
return self.impl.copy_file_s3(src=src, dst=dst, verbose=verbose)
|
459
|
+
|
460
|
+
def is_s3(self, dst: str) -> bool:
|
461
|
+
"""
|
462
|
+
Check if a path is an S3 bucket.
|
463
|
+
|
464
|
+
Args:
|
465
|
+
dst: Path to check
|
466
|
+
|
467
|
+
Returns:
|
468
|
+
True if the path is an S3 bucket, False otherwise
|
469
|
+
"""
|
470
|
+
return self.impl.is_s3(dst=dst)
|
471
|
+
|
441
472
|
def copy_to(
|
442
473
|
self,
|
443
474
|
src: File | str,
|
rclone_api/fs/filesystem.py
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
import abc
|
2
|
+
import logging
|
2
3
|
import shutil
|
3
4
|
import warnings
|
4
5
|
from pathlib import Path
|
5
6
|
|
6
7
|
from rclone_api.config import Config
|
7
8
|
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
8
11
|
|
9
12
|
class FS(abc.ABC):
|
10
13
|
def __init__(self) -> None:
|
11
14
|
pass
|
12
15
|
|
13
16
|
@abc.abstractmethod
|
14
|
-
def copy(self, src: Path | str,
|
17
|
+
def copy(self, src: Path | str, dst: Path | str) -> None:
|
15
18
|
pass
|
16
19
|
|
17
20
|
@abc.abstractmethod
|
@@ -67,8 +70,8 @@ class RealFS(FS):
|
|
67
70
|
def cwd(self) -> "FSPath":
|
68
71
|
return RealFS.from_path(Path.cwd())
|
69
72
|
|
70
|
-
def copy(self, src: Path | str,
|
71
|
-
shutil.copy(str(src), str(
|
73
|
+
def copy(self, src: Path | str, dst: Path | str) -> None:
|
74
|
+
shutil.copy(str(src), str(dst))
|
72
75
|
|
73
76
|
def read_bytes(self, path: Path | str) -> bytes:
|
74
77
|
with open(path, "rb") as f:
|
@@ -132,10 +135,31 @@ class RemoteFS(FS):
|
|
132
135
|
def _to_remote_path(self, path: str | Path) -> str:
|
133
136
|
return Path(path).relative_to(self.src).as_posix()
|
134
137
|
|
135
|
-
def copy(self, src: Path | str,
|
136
|
-
|
137
|
-
|
138
|
-
|
138
|
+
def copy(self, src: Path | str, dst: Path | str) -> None:
|
139
|
+
from rclone_api.completed_process import CompletedProcess
|
140
|
+
|
141
|
+
src = src if isinstance(src, Path) else Path(src)
|
142
|
+
if not src.is_file():
|
143
|
+
raise FileNotFoundError(f"File not found: {src}")
|
144
|
+
dst = self._to_remote_path(dst)
|
145
|
+
|
146
|
+
is_s3 = self.rclone.is_s3(dst)
|
147
|
+
if is_s3:
|
148
|
+
filesize = src.stat().st_size
|
149
|
+
if filesize < 512 * 1024 * 1024:
|
150
|
+
logger.info(f"S3 OPTIMIZED: Copying {src} -> {dst}")
|
151
|
+
err = self.rclone.copy_file_s3(src, dst)
|
152
|
+
if isinstance(err, Exception):
|
153
|
+
raise FileNotFoundError(
|
154
|
+
f"File not found: {src}, specified by {err}"
|
155
|
+
)
|
156
|
+
return
|
157
|
+
# Fallback.
|
158
|
+
logging.info(f"Copying {src} -> {dst}")
|
159
|
+
src_path = src.as_posix()
|
160
|
+
cp: CompletedProcess = self.rclone.copy(src_path, dst)
|
161
|
+
if cp.returncode != 0:
|
162
|
+
raise FileNotFoundError(f"File not found: {src}, specified by {cp.stderr}")
|
139
163
|
|
140
164
|
def read_bytes(self, path: Path | str) -> bytes:
|
141
165
|
path = self._to_str(path)
|
@@ -146,7 +170,7 @@ class RemoteFS(FS):
|
|
146
170
|
|
147
171
|
def write_binary(self, path: Path | str, data: bytes) -> None:
|
148
172
|
path = self._to_str(path)
|
149
|
-
self.rclone.write_bytes(data, path)
|
173
|
+
self.rclone.write_bytes(data, path) # Already optimized for s3.
|
150
174
|
|
151
175
|
def exists(self, path: Path | str) -> bool:
|
152
176
|
from rclone_api.http_server import HttpServer
|
rclone_api/rclone_impl.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
Unit test file.
|
3
3
|
"""
|
4
4
|
|
5
|
+
import logging
|
5
6
|
import os
|
6
7
|
import random
|
7
8
|
import subprocess
|
@@ -33,6 +34,7 @@ from rclone_api.mount import Mount
|
|
33
34
|
from rclone_api.process import Process
|
34
35
|
from rclone_api.remote import Remote
|
35
36
|
from rclone_api.rpath import RPath
|
37
|
+
from rclone_api.s3.api import S3Client
|
36
38
|
from rclone_api.s3.create import S3Credentials
|
37
39
|
from rclone_api.s3.types import (
|
38
40
|
S3Provider,
|
@@ -56,6 +58,8 @@ from rclone_api.util import (
|
|
56
58
|
# Enable tracing memory usage always
|
57
59
|
tracemalloc.start()
|
58
60
|
|
61
|
+
logger = logging.getLogger(__name__)
|
62
|
+
|
59
63
|
|
60
64
|
def rclone_verbose(verbose: bool | None) -> bool:
|
61
65
|
if verbose is not None:
|
@@ -825,6 +829,52 @@ class RcloneImpl:
|
|
825
829
|
except subprocess.CalledProcessError:
|
826
830
|
return False
|
827
831
|
|
832
|
+
def _s3_client(self, src: str, verbose: bool | None = None) -> S3Client:
|
833
|
+
"""Get an S3 client."""
|
834
|
+
verbose = get_verbose(verbose)
|
835
|
+
s3_creds = self.get_s3_credentials(remote=src, verbose=verbose)
|
836
|
+
s3_client = S3Client(s3_creds=s3_creds, verbose=verbose)
|
837
|
+
return s3_client
|
838
|
+
|
839
|
+
def copy_file_s3(
|
840
|
+
self,
|
841
|
+
src: Path,
|
842
|
+
dst: str,
|
843
|
+
verbose: bool | None = None,
|
844
|
+
) -> Exception | None:
|
845
|
+
"""Copy a file to S3."""
|
846
|
+
from rclone_api.s3.types import S3UploadTarget
|
847
|
+
from rclone_api.util import S3PathInfo
|
848
|
+
|
849
|
+
dst_is_s3 = self.is_s3(dst)
|
850
|
+
if not dst_is_s3:
|
851
|
+
return ValueError(f"Destination is not an S3 remote: {dst}")
|
852
|
+
s3_client = self._s3_client(dst, verbose=verbose)
|
853
|
+
|
854
|
+
path_info: S3PathInfo = S3PathInfo.from_str(dst)
|
855
|
+
target: S3UploadTarget = S3UploadTarget(
|
856
|
+
src_file=src,
|
857
|
+
src_file_size=src.stat().st_size,
|
858
|
+
bucket_name=path_info.bucket,
|
859
|
+
s3_key=path_info.key,
|
860
|
+
)
|
861
|
+
out = s3_client.upload_file(target=target)
|
862
|
+
return out
|
863
|
+
|
864
|
+
def is_s3(self, dst: str) -> bool:
|
865
|
+
"""Check if a remote is an S3 remote."""
|
866
|
+
from rclone_api.util import S3PathInfo
|
867
|
+
|
868
|
+
path_info: S3PathInfo = S3PathInfo.from_str(dst)
|
869
|
+
remote = path_info.remote
|
870
|
+
parsed: Parsed = self.config.parse()
|
871
|
+
sections: dict[str, Section] = parsed.sections
|
872
|
+
if remote not in sections:
|
873
|
+
return False
|
874
|
+
section: Section = sections[remote]
|
875
|
+
t = section.type()
|
876
|
+
return t in ["s3", "b2"]
|
877
|
+
|
828
878
|
def copy_file_s3_resumable(
|
829
879
|
self,
|
830
880
|
src: str, # src:/Bucket/path/myfile.large.zst
|
@@ -864,15 +914,28 @@ class RcloneImpl:
|
|
864
914
|
def write_bytes(
|
865
915
|
self,
|
866
916
|
dst: str,
|
867
|
-
data: bytes,
|
917
|
+
data: bytes | Path,
|
918
|
+
verbose: bool | None = None,
|
868
919
|
) -> Exception | None:
|
869
920
|
"""Write bytes to a file."""
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
921
|
+
|
922
|
+
try:
|
923
|
+
if isinstance(data, Path):
|
924
|
+
data = data.read_bytes()
|
925
|
+
|
926
|
+
with TemporaryDirectory() as tmpdir:
|
927
|
+
tmpfile = Path(tmpdir) / "file.bin"
|
928
|
+
tmpfile.write_bytes(data)
|
929
|
+
dst_is_s3 = self.is_s3(dst)
|
930
|
+
if dst_is_s3:
|
931
|
+
return self.copy_file_s3(tmpfile, dst, verbose=verbose)
|
932
|
+
|
933
|
+
completed_proc = self.copy_to(str(tmpfile), dst, check=True)
|
934
|
+
if completed_proc.returncode != 0:
|
935
|
+
return Exception(f"Failed to write bytes to {dst}", completed_proc)
|
936
|
+
except Exception as e:
|
937
|
+
logging.error(f"Failed to write bytes to {dst}")
|
938
|
+
return e
|
876
939
|
return None
|
877
940
|
|
878
941
|
def read_bytes(self, src: str) -> bytes | Exception:
|
@@ -925,10 +988,10 @@ class RcloneImpl:
|
|
925
988
|
def get_s3_credentials(
|
926
989
|
self, remote: str, verbose: bool | None = None
|
927
990
|
) -> S3Credentials:
|
928
|
-
from rclone_api.util import S3PathInfo
|
991
|
+
from rclone_api.util import S3PathInfo
|
929
992
|
|
930
993
|
verbose = get_verbose(verbose)
|
931
|
-
path_info: S3PathInfo =
|
994
|
+
path_info: S3PathInfo = S3PathInfo.from_str(remote)
|
932
995
|
|
933
996
|
# path_info: S3PathInfo = split_s3_path(remote)
|
934
997
|
remote = path_info.remote
|
rclone_api/s3/api.py
CHANGED
@@ -27,6 +27,10 @@ class S3Client:
|
|
27
27
|
s3_creds=s3_creds, s3_config=S3Config(verbose=verbose)
|
28
28
|
)
|
29
29
|
|
30
|
+
@property
|
31
|
+
def bucket_name(self) -> str:
|
32
|
+
return self.credentials.bucket_name
|
33
|
+
|
30
34
|
def list_bucket_contents(self, bucket_name: str) -> None:
|
31
35
|
list_bucket_contents(self.client, bucket_name)
|
32
36
|
|
rclone_api/types.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
rclone_api/__init__.py,sha256=
|
1
|
+
rclone_api/__init__.py,sha256=M3-jlyxhO7Md3FGMO8qBJ1Z7oLcbOjWshCHh2-CqKG4,34885
|
2
2
|
rclone_api/cli.py,sha256=dibfAZIh0kXWsBbfp3onKLjyZXo54mTzDjUdzJlDlWo,231
|
3
3
|
rclone_api/completed_process.py,sha256=_IZ8IWK7DM1_tsbDEkH6wPZ-bbcrgf7A7smls854pmg,1775
|
4
4
|
rclone_api/config.py,sha256=DEBubN0ziUfAFYe6cmDn7YoTqRJPdGronFyRHZvWUso,6070
|
@@ -20,11 +20,11 @@ rclone_api/log.py,sha256=VZHM7pNSXip2ZLBKMP7M1u-rp_F7zoafFDuR8CPUoKI,1271
|
|
20
20
|
rclone_api/mount.py,sha256=LZqEhuKZunbWVqmsOIqkkCotaxWJpdFRS1InXveoU5E,1428
|
21
21
|
rclone_api/mount_util.py,sha256=jqhJEVTHV6c6lOOzUYb4FLMbqDMHdz7-QRcdH-IobFc,10154
|
22
22
|
rclone_api/process.py,sha256=MeWiN-TrrN0HmtWexBPxGwf84z6f-_E5yaXE-YtLYpY,5879
|
23
|
-
rclone_api/rclone_impl.py,sha256=
|
23
|
+
rclone_api/rclone_impl.py,sha256=s00SHQGcKskbauWiIZQBQKSR0NOJ6SSiXhMf0fwkl9U,52328
|
24
24
|
rclone_api/remote.py,sha256=mTgMTQTwxUmbLjTpr-AGTId2ycXKI9mLX5L7PPpDIoc,520
|
25
25
|
rclone_api/rpath.py,sha256=Y1JjQWcie39EgQrq-UtbfDz5yDLCwwfu27W7AQXllSE,2860
|
26
26
|
rclone_api/scan_missing_folders.py,sha256=-8NCwpCaHeHrX-IepCoAEsX1rl8S-GOCxcIhTr_w3gA,4747
|
27
|
-
rclone_api/types.py,sha256=
|
27
|
+
rclone_api/types.py,sha256=59Rw7NdHw35X6iiWMemO61IzCbrjgfNHUlzY3Dq0wdM,12303
|
28
28
|
rclone_api/util.py,sha256=Xa_VifjNT_h9IMkqbnAmgSkEqqwczkGH_TcUUJHx-no,9546
|
29
29
|
rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
30
30
|
rclone_api/cmd/analyze.py,sha256=RHbvk1G5ZUc3qLqlm1AZEyQzd_W_ZjcbCNDvW4YpTKQ,1252
|
@@ -40,8 +40,8 @@ rclone_api/detail/copy_file_parts_resumable.py,sha256=RoUWV2eBWEvuuTfsvrz5BhtvX3
|
|
40
40
|
rclone_api/detail/walk.py,sha256=-54NVE8EJcCstwDoaC_UtHm73R2HrZwVwQmsnv55xNU,3369
|
41
41
|
rclone_api/experimental/flags.py,sha256=qCVD--fSTmzlk9hloRLr0q9elzAOFzPsvVpKM3aB1Mk,2739
|
42
42
|
rclone_api/experimental/flags_base.py,sha256=ajU_czkTcAxXYU-SlmiCfHY7aCQGHvpCLqJ-Z8uZLk0,2102
|
43
|
-
rclone_api/fs/filesystem.py,sha256=
|
44
|
-
rclone_api/s3/api.py,sha256=
|
43
|
+
rclone_api/fs/filesystem.py,sha256=KgaLqk6OCePcWWtPgh9NDf3dMJzYpHdhQspvzgQ0r1s,9919
|
44
|
+
rclone_api/s3/api.py,sha256=HpheMOGHcCc0CyNdv0zvB0S92g312urjRdssRrkdpb8,4162
|
45
45
|
rclone_api/s3/basic_ops.py,sha256=hK3366xhVEzEcjz9Gk_8lFx6MRceAk72cax6mUrr6ko,2104
|
46
46
|
rclone_api/s3/chunk_task.py,sha256=waEYe-iYQ1_BR3NCS4BrzVrK9UANvH1EcbXx2I6Z_NM,6839
|
47
47
|
rclone_api/s3/create.py,sha256=_Q-faQ4Zl8XKTB28gireRxVXWP-YNxoAK4bligxDtiI,3998
|
@@ -55,9 +55,9 @@ rclone_api/s3/multipart/upload_parts_inline.py,sha256=V7syKjFyVIe4U9Ahl5XgqVTzt9
|
|
55
55
|
rclone_api/s3/multipart/upload_parts_resumable.py,sha256=6-nlMclS8jyVvMvFbQDcZOX9MY1WbCcKA_s9bwuYxnk,9793
|
56
56
|
rclone_api/s3/multipart/upload_parts_server_side_merge.py,sha256=Fp2pdrs5dONQI9LkfNolgAGj1-Z2V1SsRd0r0sreuXI,18040
|
57
57
|
rclone_api/s3/multipart/upload_state.py,sha256=f-Aq2NqtAaMUMhYitlICSNIxCKurWAl2gDEUVizLIqw,6019
|
58
|
-
rclone_api-1.5.
|
59
|
-
rclone_api-1.5.
|
60
|
-
rclone_api-1.5.
|
61
|
-
rclone_api-1.5.
|
62
|
-
rclone_api-1.5.
|
63
|
-
rclone_api-1.5.
|
58
|
+
rclone_api-1.5.52.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
59
|
+
rclone_api-1.5.52.dist-info/METADATA,sha256=pzUkl7zLuD22M8z-gOpbEkACDpT0tZ6r62N3hJ-LwMc,37305
|
60
|
+
rclone_api-1.5.52.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
61
|
+
rclone_api-1.5.52.dist-info/entry_points.txt,sha256=ognh2e11HTjn73_KL5MWI67pBKS2jekBi-QTiRXySXA,316
|
62
|
+
rclone_api-1.5.52.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
63
|
+
rclone_api-1.5.52.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|