rclone-api 1.4.25__py2.py3-none-any.whl → 1.4.28__py2.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 +2 -29
- rclone_api/cmd/copy_large_s3.py +1 -1
- rclone_api/rclone_impl.py +1 -104
- rclone_api-1.4.28.dist-info/METADATA +556 -0
- {rclone_api-1.4.25.dist-info → rclone_api-1.4.28.dist-info}/RECORD +9 -9
- rclone_api-1.4.25.dist-info/METADATA +0 -154
- {rclone_api-1.4.25.dist-info → rclone_api-1.4.28.dist-info}/LICENSE +0 -0
- {rclone_api-1.4.25.dist-info → rclone_api-1.4.28.dist-info}/WHEEL +0 -0
- {rclone_api-1.4.25.dist-info → rclone_api-1.4.28.dist-info}/entry_points.txt +0 -0
- {rclone_api-1.4.25.dist-info → rclone_api-1.4.28.dist-info}/top_level.txt +0 -0
rclone_api/__init__.py
CHANGED
@@ -366,33 +366,6 @@ class Rclone:
|
|
366
366
|
"""Read text from a file."""
|
367
367
|
return self.impl.read_text(src=src)
|
368
368
|
|
369
|
-
def copy_file_resumable_s3(
|
370
|
-
self,
|
371
|
-
src: str,
|
372
|
-
dst: str,
|
373
|
-
save_state_json: Path,
|
374
|
-
chunk_size: SizeSuffix | None = None,
|
375
|
-
read_threads: int = 8,
|
376
|
-
write_threads: int = 8,
|
377
|
-
retries: int = 3,
|
378
|
-
verbose: bool | None = None,
|
379
|
-
max_chunks_before_suspension: int | None = None,
|
380
|
-
backend_log: Path | None = None,
|
381
|
-
) -> MultiUploadResult:
|
382
|
-
"""For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
|
383
|
-
return self.impl.copy_file_resumable_s3(
|
384
|
-
src=src,
|
385
|
-
dst=dst,
|
386
|
-
save_state_json=save_state_json,
|
387
|
-
chunk_size=chunk_size,
|
388
|
-
read_threads=read_threads,
|
389
|
-
write_threads=write_threads,
|
390
|
-
retries=retries,
|
391
|
-
verbose=verbose,
|
392
|
-
max_chunks_before_suspension=max_chunks_before_suspension,
|
393
|
-
backend_log=backend_log,
|
394
|
-
)
|
395
|
-
|
396
369
|
def copy_bytes(
|
397
370
|
self,
|
398
371
|
src: str,
|
@@ -423,7 +396,7 @@ class Rclone:
|
|
423
396
|
"""Copy a remote to another remote."""
|
424
397
|
return self.impl.copy_remote(src=src, dst=dst, args=args)
|
425
398
|
|
426
|
-
def
|
399
|
+
def copy_file_s3_resumable(
|
427
400
|
self,
|
428
401
|
src: str, # src:/Bucket/path/myfile.large.zst
|
429
402
|
dst_dir: str, # dst:/Bucket/path/myfile.large.zst-parts/part.{part_number:05d}.start-end
|
@@ -432,7 +405,7 @@ class Rclone:
|
|
432
405
|
merge_threads: int = 4, # Number of threads to use for merging the parts
|
433
406
|
) -> Exception | None:
|
434
407
|
"""Copy a file in parts."""
|
435
|
-
return self.impl.
|
408
|
+
return self.impl.copy_file_s3_resumable(
|
436
409
|
src=src,
|
437
410
|
dst_dir=dst_dir,
|
438
411
|
part_infos=part_infos,
|
rclone_api/cmd/copy_large_s3.py
CHANGED
rclone_api/rclone_impl.py
CHANGED
@@ -6,7 +6,6 @@ import os
|
|
6
6
|
import random
|
7
7
|
import subprocess
|
8
8
|
import time
|
9
|
-
import traceback
|
10
9
|
import warnings
|
11
10
|
from concurrent.futures import Future, ThreadPoolExecutor
|
12
11
|
from datetime import datetime
|
@@ -34,10 +33,7 @@ from rclone_api.remote import Remote
|
|
34
33
|
from rclone_api.rpath import RPath
|
35
34
|
from rclone_api.s3.create import S3Credentials
|
36
35
|
from rclone_api.s3.types import (
|
37
|
-
MultiUploadResult,
|
38
|
-
S3MutliPartUploadConfig,
|
39
36
|
S3Provider,
|
40
|
-
S3UploadTarget,
|
41
37
|
)
|
42
38
|
from rclone_api.types import (
|
43
39
|
ListingOption,
|
@@ -787,7 +783,7 @@ class RcloneImpl:
|
|
787
783
|
except subprocess.CalledProcessError:
|
788
784
|
return False
|
789
785
|
|
790
|
-
def
|
786
|
+
def copy_file_s3_resumable(
|
791
787
|
self,
|
792
788
|
src: str, # src:/Bucket/path/myfile.large.zst
|
793
789
|
dst_dir: str, # dst:/Bucket/path/myfile.large.zst-parts/
|
@@ -938,105 +934,6 @@ class RcloneImpl:
|
|
938
934
|
)
|
939
935
|
return s3_creds
|
940
936
|
|
941
|
-
def copy_file_resumable_s3(
|
942
|
-
self,
|
943
|
-
src: str,
|
944
|
-
dst: str,
|
945
|
-
save_state_json: Path,
|
946
|
-
chunk_size: SizeSuffix | None = None,
|
947
|
-
read_threads: int = 8,
|
948
|
-
write_threads: int = 8,
|
949
|
-
retries: int = 3,
|
950
|
-
verbose: bool | None = None,
|
951
|
-
max_chunks_before_suspension: int | None = None,
|
952
|
-
backend_log: Path | None = None,
|
953
|
-
) -> MultiUploadResult:
|
954
|
-
"""For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
|
955
|
-
from rclone_api.http_server import HttpFetcher, HttpServer
|
956
|
-
from rclone_api.s3.api import S3Client
|
957
|
-
from rclone_api.util import S3PathInfo, split_s3_path
|
958
|
-
|
959
|
-
src_path = Path(src)
|
960
|
-
name = src_path.name
|
961
|
-
src_parent_path = Path(src).parent.as_posix()
|
962
|
-
|
963
|
-
size_result: SizeResult | Exception = self.size_files(src_parent_path, [name])
|
964
|
-
if isinstance(size_result, Exception):
|
965
|
-
raise size_result
|
966
|
-
target_size = SizeSuffix(size_result.total_size)
|
967
|
-
|
968
|
-
chunk_size = chunk_size or SizeSuffix("64M")
|
969
|
-
MAX_CHUNKS = 10000
|
970
|
-
min_chunk_size = SizeSuffix(size_result.total_size // (MAX_CHUNKS - 1))
|
971
|
-
if min_chunk_size > chunk_size:
|
972
|
-
warnings.warn(
|
973
|
-
f"Chunk size {chunk_size} is too small for file size {size_result.total_size}, setting to {min_chunk_size}"
|
974
|
-
)
|
975
|
-
chunk_size = SizeSuffix(min_chunk_size)
|
976
|
-
|
977
|
-
if target_size < SizeSuffix("5M"):
|
978
|
-
# fallback to normal copy
|
979
|
-
completed_proc = self.copy_to(src, dst, check=True)
|
980
|
-
if completed_proc.ok:
|
981
|
-
return MultiUploadResult.UPLOADED_FRESH
|
982
|
-
|
983
|
-
if size_result.total_size <= 0:
|
984
|
-
raise ValueError(
|
985
|
-
f"File {src} has size {size_result.total_size}, is this a directory?"
|
986
|
-
)
|
987
|
-
|
988
|
-
path_info: S3PathInfo = split_s3_path(dst)
|
989
|
-
# remote = path_info.remote
|
990
|
-
bucket_name = path_info.bucket
|
991
|
-
s3_key = path_info.key
|
992
|
-
s3_creds: S3Credentials = self.get_s3_credentials(dst, verbose=verbose)
|
993
|
-
|
994
|
-
port = random.randint(10000, 20000)
|
995
|
-
http_server: HttpServer = self.serve_http(
|
996
|
-
src=src_path.parent.as_posix(),
|
997
|
-
addr=f"localhost:{port}",
|
998
|
-
serve_http_log=backend_log,
|
999
|
-
)
|
1000
|
-
chunk_fetcher: HttpFetcher = http_server.get_fetcher(
|
1001
|
-
path=src_path.name,
|
1002
|
-
n_threads=read_threads,
|
1003
|
-
)
|
1004
|
-
|
1005
|
-
client = S3Client(s3_creds)
|
1006
|
-
upload_config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
|
1007
|
-
chunk_size=chunk_size.as_int(),
|
1008
|
-
chunk_fetcher=chunk_fetcher.bytes_fetcher,
|
1009
|
-
max_write_threads=write_threads,
|
1010
|
-
retries=retries,
|
1011
|
-
resume_path_json=save_state_json,
|
1012
|
-
max_chunks_before_suspension=max_chunks_before_suspension,
|
1013
|
-
)
|
1014
|
-
|
1015
|
-
print(f"Uploading {name} to {s3_key} in bucket {bucket_name}")
|
1016
|
-
print(f"Source: {src_path}")
|
1017
|
-
print(f"bucket_name: {bucket_name}")
|
1018
|
-
print(f"upload_config: {upload_config}")
|
1019
|
-
|
1020
|
-
upload_target = S3UploadTarget(
|
1021
|
-
src_file=src_path,
|
1022
|
-
src_file_size=size_result.total_size,
|
1023
|
-
bucket_name=bucket_name,
|
1024
|
-
s3_key=s3_key,
|
1025
|
-
)
|
1026
|
-
|
1027
|
-
try:
|
1028
|
-
out: MultiUploadResult = client.upload_file_multipart(
|
1029
|
-
upload_target=upload_target,
|
1030
|
-
upload_config=upload_config,
|
1031
|
-
)
|
1032
|
-
return out
|
1033
|
-
except Exception as e:
|
1034
|
-
print(f"Error uploading file: {e}")
|
1035
|
-
traceback.print_exc()
|
1036
|
-
raise
|
1037
|
-
finally:
|
1038
|
-
chunk_fetcher.shutdown()
|
1039
|
-
|
1040
937
|
def copy_bytes(
|
1041
938
|
self,
|
1042
939
|
src: str,
|
@@ -0,0 +1,556 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: rclone_api
|
3
|
+
Version: 1.4.28
|
4
|
+
Summary: rclone api in python
|
5
|
+
Home-page: https://github.com/zackees/rclone-api
|
6
|
+
License: BSD 3-Clause License
|
7
|
+
Keywords: rclone,api,python,fast,sftp,s3,backblaze
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Requires-Python: >=3.10
|
10
|
+
Description-Content-Type: text/markdown
|
11
|
+
License-File: LICENSE
|
12
|
+
Requires-Dist: pyright>=1.1.393
|
13
|
+
Requires-Dist: python-dotenv>=1.0.0
|
14
|
+
Requires-Dist: certifi>=2025.1.31
|
15
|
+
Requires-Dist: psutil
|
16
|
+
Requires-Dist: boto3<=1.35.99,>=1.20.1
|
17
|
+
Requires-Dist: sqlmodel>=0.0.23
|
18
|
+
Requires-Dist: psycopg2-binary>=2.9.10
|
19
|
+
Requires-Dist: httpx>=0.28.1
|
20
|
+
Dynamic: home-page
|
21
|
+
|
22
|
+
# rclone-api
|
23
|
+
|
24
|
+
|
25
|
+

|
26
|
+
|
27
|
+
|
28
|
+
<!--
|
29
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
|
30
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
|
31
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
|
32
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
|
33
|
+
-->
|
34
|
+
|
35
|
+
|
36
|
+
Got a lot of data to transfer quickly? This package is for you.
|
37
|
+
|
38
|
+
This library was built out of necessity to transfer large amounts of AI training data. Aggressive default means this api will transfer faster than rclone does in stock settings.
|
39
|
+
|
40
|
+
You must have [rclone](https://rclone.org/) in your path to use this library. You'd want this anyway because rclone is still under heavy developement.
|
41
|
+
|
42
|
+
# Install
|
43
|
+
|
44
|
+
`pip install rclone-api`
|
45
|
+
|
46
|
+
# Quick
|
47
|
+
|
48
|
+
In addition to providing easy python use for rclone, this package provides additional features:
|
49
|
+
|
50
|
+
* Resumable multi-part uploads when s3 is the destination.
|
51
|
+
* Diffing src/dst repos as a stream of `list[str]`.
|
52
|
+
* Dumping repo information to an sqlite/postgres/mysql database.
|
53
|
+
* Efficient copying of byte ranges of a file.
|
54
|
+
* Aggressive default settings for copying / syncing operations for extreme performance.
|
55
|
+
* Some platform specific fixes.
|
56
|
+
|
57
|
+
|
58
|
+
## Example
|
59
|
+
|
60
|
+
```python
|
61
|
+
|
62
|
+
from rclone_api import Rclone, DirListing, Config
|
63
|
+
|
64
|
+
RCLONE_CONFIG = Config("""
|
65
|
+
[dst]
|
66
|
+
type = s3
|
67
|
+
account = *********
|
68
|
+
key = ************
|
69
|
+
""")
|
70
|
+
|
71
|
+
|
72
|
+
def test_ls_glob_png(self) -> None:
|
73
|
+
rclone = Rclone(RCLONE_CONFIG)
|
74
|
+
path = f"dst:{BUCKET_NAME}/my_data"
|
75
|
+
listing: DirListing = rclone.ls(path, glob="*.png")
|
76
|
+
self.assertGreater(len(listing.files), 0)
|
77
|
+
for file in listing.files:
|
78
|
+
self.assertIsInstance(file, File)
|
79
|
+
# test that it ends with .png
|
80
|
+
self.assertTrue(file.name.endswith(".png"))
|
81
|
+
# there should be no directories with this glob
|
82
|
+
self.assertEqual(len(listing.dirs), 0)
|
83
|
+
```
|
84
|
+
|
85
|
+
## API
|
86
|
+
|
87
|
+
```python
|
88
|
+
|
89
|
+
# from rclone_api import Rclone
|
90
|
+
# Rclone is the main api entry point.
|
91
|
+
class Rclone:
|
92
|
+
def __init__(
|
93
|
+
self, rclone_conf: Path | Config, rclone_exe: Path | None = None
|
94
|
+
) -> None:
|
95
|
+
from rclone_api.rclone_impl import RcloneImpl
|
96
|
+
|
97
|
+
self.impl: RcloneImpl = RcloneImpl(rclone_conf, rclone_exe)
|
98
|
+
|
99
|
+
def webgui(self, other_args: list[str] | None = None) -> Process:
|
100
|
+
"""Launch the Rclone web GUI."""
|
101
|
+
return self.impl.webgui(other_args=other_args)
|
102
|
+
|
103
|
+
def launch_server(
|
104
|
+
self,
|
105
|
+
addr: str,
|
106
|
+
user: str | None = None,
|
107
|
+
password: str | None = None,
|
108
|
+
other_args: list[str] | None = None,
|
109
|
+
) -> Process:
|
110
|
+
"""Launch the Rclone server so it can receive commands"""
|
111
|
+
return self.impl.launch_server(
|
112
|
+
addr=addr, user=user, password=password, other_args=other_args
|
113
|
+
)
|
114
|
+
|
115
|
+
def remote_control(
|
116
|
+
self,
|
117
|
+
addr: str,
|
118
|
+
user: str | None = None,
|
119
|
+
password: str | None = None,
|
120
|
+
capture: bool | None = None,
|
121
|
+
other_args: list[str] | None = None,
|
122
|
+
) -> CompletedProcess:
|
123
|
+
return self.impl.remote_control(
|
124
|
+
addr=addr,
|
125
|
+
user=user,
|
126
|
+
password=password,
|
127
|
+
capture=capture,
|
128
|
+
other_args=other_args,
|
129
|
+
)
|
130
|
+
|
131
|
+
def obscure(self, password: str) -> str:
|
132
|
+
"""Obscure a password for use in rclone config files."""
|
133
|
+
return self.impl.obscure(password=password)
|
134
|
+
|
135
|
+
def ls_stream(
|
136
|
+
self,
|
137
|
+
path: str,
|
138
|
+
max_depth: int = -1,
|
139
|
+
fast_list: bool = False,
|
140
|
+
) -> FilesStream:
|
141
|
+
"""
|
142
|
+
List files in the given path
|
143
|
+
|
144
|
+
Args:
|
145
|
+
src: Remote path to list
|
146
|
+
max_depth: Maximum recursion depth (-1 for unlimited)
|
147
|
+
fast_list: Use fast list (only use when getting THE entire data repository from the root/bucket, or it's small)
|
148
|
+
"""
|
149
|
+
return self.impl.ls_stream(path=path, max_depth=max_depth, fast_list=fast_list)
|
150
|
+
|
151
|
+
def save_to_db(
|
152
|
+
self,
|
153
|
+
src: str,
|
154
|
+
db_url: str,
|
155
|
+
max_depth: int = -1,
|
156
|
+
fast_list: bool = False,
|
157
|
+
) -> None:
|
158
|
+
"""
|
159
|
+
Save files to a database (sqlite, mysql, postgres)
|
160
|
+
|
161
|
+
Args:
|
162
|
+
src: Remote path to list, this will be used to populate an entire table, so always use the root-most path.
|
163
|
+
db_url: Database URL, like sqlite:///data.db or mysql://user:pass@localhost/db or postgres://user:pass@localhost/db
|
164
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
165
|
+
fast_list: Use fast list (only use when getting THE entire data repository from the root/bucket)
|
166
|
+
|
167
|
+
"""
|
168
|
+
return self.impl.save_to_db(
|
169
|
+
src=src, db_url=db_url, max_depth=max_depth, fast_list=fast_list
|
170
|
+
)
|
171
|
+
|
172
|
+
def ls(
|
173
|
+
self,
|
174
|
+
path: Dir | Remote | str | None = None,
|
175
|
+
max_depth: int | None = None,
|
176
|
+
glob: str | None = None,
|
177
|
+
order: Order = Order.NORMAL,
|
178
|
+
listing_option: ListingOption = ListingOption.ALL,
|
179
|
+
) -> DirListing:
|
180
|
+
return self.impl.ls(
|
181
|
+
path=path,
|
182
|
+
max_depth=max_depth,
|
183
|
+
glob=glob,
|
184
|
+
order=order,
|
185
|
+
listing_option=listing_option,
|
186
|
+
)
|
187
|
+
|
188
|
+
def listremotes(self) -> list[Remote]:
|
189
|
+
return self.impl.listremotes()
|
190
|
+
|
191
|
+
def diff(
|
192
|
+
self,
|
193
|
+
src: str,
|
194
|
+
dst: str,
|
195
|
+
min_size: (
|
196
|
+
str | None
|
197
|
+
) = None, # e. g. "1MB" - see rclone documentation: https://rclone.org/commands/rclone_check/
|
198
|
+
max_size: (
|
199
|
+
str | None
|
200
|
+
) = None, # e. g. "1GB" - see rclone documentation: https://rclone.org/commands/rclone_check/
|
201
|
+
diff_option: DiffOption = DiffOption.COMBINED,
|
202
|
+
fast_list: bool = True,
|
203
|
+
size_only: bool | None = None,
|
204
|
+
checkers: int | None = None,
|
205
|
+
other_args: list[str] | None = None,
|
206
|
+
) -> Generator[DiffItem, None, None]:
|
207
|
+
"""Be extra careful with the src and dst values. If you are off by one
|
208
|
+
parent directory, you will get a huge amount of false diffs."""
|
209
|
+
return self.impl.diff(
|
210
|
+
src=src,
|
211
|
+
dst=dst,
|
212
|
+
min_size=min_size,
|
213
|
+
max_size=max_size,
|
214
|
+
diff_option=diff_option,
|
215
|
+
fast_list=fast_list,
|
216
|
+
size_only=size_only,
|
217
|
+
checkers=checkers,
|
218
|
+
other_args=other_args,
|
219
|
+
)
|
220
|
+
|
221
|
+
def walk(
|
222
|
+
self,
|
223
|
+
path: Dir | Remote | str,
|
224
|
+
max_depth: int = -1,
|
225
|
+
breadth_first: bool = True,
|
226
|
+
order: Order = Order.NORMAL,
|
227
|
+
) -> Generator[DirListing, None, None]:
|
228
|
+
"""Walk through the given path recursively.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
path: Remote path or Remote object to walk through
|
232
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
233
|
+
|
234
|
+
Yields:
|
235
|
+
DirListing: Directory listing for each directory encountered
|
236
|
+
"""
|
237
|
+
return self.impl.walk(
|
238
|
+
path=path, max_depth=max_depth, breadth_first=breadth_first, order=order
|
239
|
+
)
|
240
|
+
|
241
|
+
def scan_missing_folders(
|
242
|
+
self,
|
243
|
+
src: Dir | Remote | str,
|
244
|
+
dst: Dir | Remote | str,
|
245
|
+
max_depth: int = -1,
|
246
|
+
order: Order = Order.NORMAL,
|
247
|
+
) -> Generator[Dir, None, None]:
|
248
|
+
"""Walk through the given path recursively.
|
249
|
+
|
250
|
+
WORK IN PROGRESS!!
|
251
|
+
|
252
|
+
Args:
|
253
|
+
src: Source directory or Remote to walk through
|
254
|
+
dst: Destination directory or Remote to walk through
|
255
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
256
|
+
|
257
|
+
Yields:
|
258
|
+
DirListing: Directory listing for each directory encountered
|
259
|
+
"""
|
260
|
+
return self.impl.scan_missing_folders(
|
261
|
+
src=src, dst=dst, max_depth=max_depth, order=order
|
262
|
+
)
|
263
|
+
|
264
|
+
def cleanup(
|
265
|
+
self, path: str, other_args: list[str] | None = None
|
266
|
+
) -> CompletedProcess:
|
267
|
+
"""Cleanup any resources used by the Rclone instance."""
|
268
|
+
return self.impl.cleanup(path=path, other_args=other_args)
|
269
|
+
|
270
|
+
def copy_to(
|
271
|
+
self,
|
272
|
+
src: File | str,
|
273
|
+
dst: File | str,
|
274
|
+
check: bool | None = None,
|
275
|
+
verbose: bool | None = None,
|
276
|
+
other_args: list[str] | None = None,
|
277
|
+
) -> CompletedProcess:
|
278
|
+
"""Copy one file from source to destination.
|
279
|
+
|
280
|
+
Warning - slow.
|
281
|
+
|
282
|
+
"""
|
283
|
+
return self.impl.copy_to(
|
284
|
+
src=src, dst=dst, check=check, verbose=verbose, other_args=other_args
|
285
|
+
)
|
286
|
+
|
287
|
+
def copy_files(
|
288
|
+
self,
|
289
|
+
src: str,
|
290
|
+
dst: str,
|
291
|
+
files: list[str] | Path,
|
292
|
+
check: bool | None = None,
|
293
|
+
max_backlog: int | None = None,
|
294
|
+
verbose: bool | None = None,
|
295
|
+
checkers: int | None = None,
|
296
|
+
transfers: int | None = None,
|
297
|
+
low_level_retries: int | None = None,
|
298
|
+
retries: int | None = None,
|
299
|
+
retries_sleep: str | None = None,
|
300
|
+
metadata: bool | None = None,
|
301
|
+
timeout: str | None = None,
|
302
|
+
max_partition_workers: int | None = None,
|
303
|
+
multi_thread_streams: int | None = None,
|
304
|
+
other_args: list[str] | None = None,
|
305
|
+
) -> list[CompletedProcess]:
|
306
|
+
"""Copy multiple files from source to destination.
|
307
|
+
|
308
|
+
Args:
|
309
|
+
payload: Dictionary of source and destination file paths
|
310
|
+
"""
|
311
|
+
return self.impl.copy_files(
|
312
|
+
src=src,
|
313
|
+
dst=dst,
|
314
|
+
files=files,
|
315
|
+
check=check,
|
316
|
+
max_backlog=max_backlog,
|
317
|
+
verbose=verbose,
|
318
|
+
checkers=checkers,
|
319
|
+
transfers=transfers,
|
320
|
+
low_level_retries=low_level_retries,
|
321
|
+
retries=retries,
|
322
|
+
retries_sleep=retries_sleep,
|
323
|
+
metadata=metadata,
|
324
|
+
timeout=timeout,
|
325
|
+
max_partition_workers=max_partition_workers,
|
326
|
+
multi_thread_streams=multi_thread_streams,
|
327
|
+
other_args=other_args,
|
328
|
+
)
|
329
|
+
|
330
|
+
def copy(
|
331
|
+
self,
|
332
|
+
src: Dir | str,
|
333
|
+
dst: Dir | str,
|
334
|
+
check: bool | None = None,
|
335
|
+
transfers: int | None = None,
|
336
|
+
checkers: int | None = None,
|
337
|
+
multi_thread_streams: int | None = None,
|
338
|
+
low_level_retries: int | None = None,
|
339
|
+
retries: int | None = None,
|
340
|
+
other_args: list[str] | None = None,
|
341
|
+
) -> CompletedProcess:
|
342
|
+
"""Copy files from source to destination.
|
343
|
+
|
344
|
+
Args:
|
345
|
+
src: Source directory
|
346
|
+
dst: Destination directory
|
347
|
+
"""
|
348
|
+
return self.impl.copy(
|
349
|
+
src=src,
|
350
|
+
dst=dst,
|
351
|
+
check=check,
|
352
|
+
transfers=transfers,
|
353
|
+
checkers=checkers,
|
354
|
+
multi_thread_streams=multi_thread_streams,
|
355
|
+
low_level_retries=low_level_retries,
|
356
|
+
retries=retries,
|
357
|
+
other_args=other_args,
|
358
|
+
)
|
359
|
+
|
360
|
+
def purge(self, path: Dir | str) -> CompletedProcess:
|
361
|
+
"""Purge a directory"""
|
362
|
+
return self.impl.purge(path=path)
|
363
|
+
|
364
|
+
def delete_files(
|
365
|
+
self,
|
366
|
+
files: str | File | list[str] | list[File],
|
367
|
+
check: bool | None = None,
|
368
|
+
rmdirs=False,
|
369
|
+
verbose: bool | None = None,
|
370
|
+
max_partition_workers: int | None = None,
|
371
|
+
other_args: list[str] | None = None,
|
372
|
+
) -> CompletedProcess:
|
373
|
+
"""Delete a directory"""
|
374
|
+
return self.impl.delete_files(
|
375
|
+
files=files,
|
376
|
+
check=check,
|
377
|
+
rmdirs=rmdirs,
|
378
|
+
verbose=verbose,
|
379
|
+
max_partition_workers=max_partition_workers,
|
380
|
+
other_args=other_args,
|
381
|
+
)
|
382
|
+
|
383
|
+
def exists(self, path: Dir | Remote | str | File) -> bool:
|
384
|
+
"""Check if a file or directory exists."""
|
385
|
+
return self.impl.exists(path=path)
|
386
|
+
|
387
|
+
def is_synced(self, src: str | Dir, dst: str | Dir) -> bool:
|
388
|
+
"""Check if two directories are in sync."""
|
389
|
+
return self.impl.is_synced(src=src, dst=dst)
|
390
|
+
|
391
|
+
def modtime(self, src: str) -> str | Exception:
|
392
|
+
"""Get the modification time of a file or directory."""
|
393
|
+
return self.impl.modtime(src=src)
|
394
|
+
|
395
|
+
def modtime_dt(self, src: str) -> datetime | Exception:
|
396
|
+
"""Get the modification time of a file or directory."""
|
397
|
+
return self.impl.modtime_dt(src=src)
|
398
|
+
|
399
|
+
def write_text(
|
400
|
+
self,
|
401
|
+
text: str,
|
402
|
+
dst: str,
|
403
|
+
) -> Exception | None:
|
404
|
+
"""Write text to a file."""
|
405
|
+
return self.impl.write_text(text=text, dst=dst)
|
406
|
+
|
407
|
+
def write_bytes(
|
408
|
+
self,
|
409
|
+
data: bytes,
|
410
|
+
dst: str,
|
411
|
+
) -> Exception | None:
|
412
|
+
"""Write bytes to a file."""
|
413
|
+
return self.impl.write_bytes(data=data, dst=dst)
|
414
|
+
|
415
|
+
def read_bytes(self, src: str) -> bytes | Exception:
|
416
|
+
"""Read bytes from a file."""
|
417
|
+
return self.impl.read_bytes(src=src)
|
418
|
+
|
419
|
+
def read_text(self, src: str) -> str | Exception:
|
420
|
+
"""Read text from a file."""
|
421
|
+
return self.impl.read_text(src=src)
|
422
|
+
|
423
|
+
def copy_bytes(
|
424
|
+
self,
|
425
|
+
src: str,
|
426
|
+
offset: int | SizeSuffix,
|
427
|
+
length: int | SizeSuffix,
|
428
|
+
outfile: Path,
|
429
|
+
other_args: list[str] | None = None,
|
430
|
+
) -> Exception | None:
|
431
|
+
"""Copy a slice of bytes from the src file to dst."""
|
432
|
+
return self.impl.copy_bytes(
|
433
|
+
src=src,
|
434
|
+
offset=offset,
|
435
|
+
length=length,
|
436
|
+
outfile=outfile,
|
437
|
+
other_args=other_args,
|
438
|
+
)
|
439
|
+
|
440
|
+
def copy_dir(
|
441
|
+
self, src: str | Dir, dst: str | Dir, args: list[str] | None = None
|
442
|
+
) -> CompletedProcess:
|
443
|
+
"""Copy a directory from source to destination."""
|
444
|
+
# convert src to str, also dst
|
445
|
+
return self.impl.copy_dir(src=src, dst=dst, args=args)
|
446
|
+
|
447
|
+
def copy_remote(
|
448
|
+
self, src: Remote, dst: Remote, args: list[str] | None = None
|
449
|
+
) -> CompletedProcess:
|
450
|
+
"""Copy a remote to another remote."""
|
451
|
+
return self.impl.copy_remote(src=src, dst=dst, args=args)
|
452
|
+
|
453
|
+
def copy_file_s3_resumable(
|
454
|
+
self,
|
455
|
+
src: str, # src:/Bucket/path/myfile.large.zst
|
456
|
+
dst_dir: str, # dst:/Bucket/path/myfile.large.zst-parts/part.{part_number:05d}.start-end
|
457
|
+
part_infos: list[PartInfo] | None = None,
|
458
|
+
upload_threads: int = 8, # Number of reader and writer threads to use
|
459
|
+
merge_threads: int = 4, # Number of threads to use for merging the parts
|
460
|
+
) -> Exception | None:
|
461
|
+
"""Copy a file in parts."""
|
462
|
+
return self.impl.copy_file_s3_resumable(
|
463
|
+
src=src,
|
464
|
+
dst_dir=dst_dir,
|
465
|
+
part_infos=part_infos,
|
466
|
+
upload_threads=upload_threads,
|
467
|
+
merge_threads=merge_threads,
|
468
|
+
)
|
469
|
+
|
470
|
+
def mount(
|
471
|
+
self,
|
472
|
+
src: Remote | Dir | str,
|
473
|
+
outdir: Path,
|
474
|
+
allow_writes: bool | None = False,
|
475
|
+
use_links: bool | None = None,
|
476
|
+
vfs_cache_mode: str | None = None,
|
477
|
+
verbose: bool | None = None,
|
478
|
+
cache_dir: Path | None = None,
|
479
|
+
cache_dir_delete_on_exit: bool | None = None,
|
480
|
+
log: Path | None = None,
|
481
|
+
other_args: list[str] | None = None,
|
482
|
+
) -> Mount:
|
483
|
+
"""Mount a remote or directory to a local path.
|
484
|
+
|
485
|
+
Args:
|
486
|
+
src: Remote or directory to mount
|
487
|
+
outdir: Local path to mount to
|
488
|
+
|
489
|
+
Returns:
|
490
|
+
CompletedProcess from the mount command execution
|
491
|
+
|
492
|
+
Raises:
|
493
|
+
subprocess.CalledProcessError: If the mount operation fails
|
494
|
+
"""
|
495
|
+
return self.impl.mount(
|
496
|
+
src=src,
|
497
|
+
outdir=outdir,
|
498
|
+
allow_writes=allow_writes,
|
499
|
+
use_links=use_links,
|
500
|
+
vfs_cache_mode=vfs_cache_mode,
|
501
|
+
verbose=verbose,
|
502
|
+
cache_dir=cache_dir,
|
503
|
+
cache_dir_delete_on_exit=cache_dir_delete_on_exit,
|
504
|
+
log=log,
|
505
|
+
other_args=other_args,
|
506
|
+
)
|
507
|
+
|
508
|
+
def serve_http(
|
509
|
+
self,
|
510
|
+
src: str,
|
511
|
+
addr: str = "localhost:8080",
|
512
|
+
other_args: list[str] | None = None,
|
513
|
+
) -> HttpServer:
|
514
|
+
"""Serve a remote or directory via HTTP. The returned HttpServer has a client which can be used to
|
515
|
+
fetch files or parts.
|
516
|
+
|
517
|
+
Args:
|
518
|
+
src: Remote or directory to serve
|
519
|
+
addr: Network address and port to serve on (default: localhost:8080)
|
520
|
+
"""
|
521
|
+
return self.impl.serve_http(src=src, addr=addr, other_args=other_args)
|
522
|
+
|
523
|
+
def size_files(
|
524
|
+
self,
|
525
|
+
src: str,
|
526
|
+
files: list[str],
|
527
|
+
fast_list: bool = False, # Recommend that this is False
|
528
|
+
other_args: list[str] | None = None,
|
529
|
+
check: bool | None = False,
|
530
|
+
verbose: bool | None = None,
|
531
|
+
) -> SizeResult | Exception:
|
532
|
+
"""Get the size of a list of files. Example of files items: "remote:bucket/to/file"."""
|
533
|
+
return self.impl.size_files(
|
534
|
+
src=src,
|
535
|
+
files=files,
|
536
|
+
fast_list=fast_list,
|
537
|
+
other_args=other_args,
|
538
|
+
check=check,
|
539
|
+
verbose=verbose,
|
540
|
+
)
|
541
|
+
|
542
|
+
def size_file(self, src: str) -> SizeSuffix | Exception:
|
543
|
+
"""Get the size of a file."""
|
544
|
+
return self.impl.size_file(src=src)
|
545
|
+
```
|
546
|
+
|
547
|
+
|
548
|
+
To develop software, run `. ./activate`
|
549
|
+
|
550
|
+
# Windows
|
551
|
+
|
552
|
+
This environment requires you to use `git-bash`.
|
553
|
+
|
554
|
+
# Linting
|
555
|
+
|
556
|
+
Run `./lint`
|
@@ -1,4 +1,4 @@
|
|
1
|
-
rclone_api/__init__.py,sha256=
|
1
|
+
rclone_api/__init__.py,sha256=MyVBox2r4zRoMt3mWDTw7My24n9SAxGoCxkvgAo6Nps,16898
|
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=f6jEAxVorGFr31oHfcsu5AJTtOJj2wR5tTSsbGGZuIw,2558
|
@@ -18,7 +18,7 @@ rclone_api/http_server.py,sha256=LhovQu2AI-Z7zQIWflWelCiCDLnWzisL32Rs5350kxE,885
|
|
18
18
|
rclone_api/log.py,sha256=VZHM7pNSXip2ZLBKMP7M1u-rp_F7zoafFDuR8CPUoKI,1271
|
19
19
|
rclone_api/mount.py,sha256=TE_VIBMW7J1UkF_6HRCt8oi_jGdMov4S51bm2OgxFAM,10045
|
20
20
|
rclone_api/process.py,sha256=tGooS5NLdPuqHh7hCH8SfK44A6LGftPQCPQUNgSo0a0,5714
|
21
|
-
rclone_api/rclone_impl.py,sha256=
|
21
|
+
rclone_api/rclone_impl.py,sha256=mFFpU4ngWuZfvtwoIaOl2iTVYrWNRte-TBtbZzVhWaU,46108
|
22
22
|
rclone_api/remote.py,sha256=mTgMTQTwxUmbLjTpr-AGTId2ycXKI9mLX5L7PPpDIoc,520
|
23
23
|
rclone_api/rpath.py,sha256=Y1JjQWcie39EgQrq-UtbfDz5yDLCwwfu27W7AQXllSE,2860
|
24
24
|
rclone_api/scan_missing_folders.py,sha256=-8NCwpCaHeHrX-IepCoAEsX1rl8S-GOCxcIhTr_w3gA,4747
|
@@ -26,7 +26,7 @@ rclone_api/types.py,sha256=2ngxwpdNy88y0teeYJ5Vz5NiLK1rfaFx8Xf99i0J-Js,12155
|
|
26
26
|
rclone_api/util.py,sha256=yY72YKpmpT_ZM7AleVtPpl0YZZYQPTwTdqKn9qPwm8Y,9290
|
27
27
|
rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
28
28
|
rclone_api/cmd/analyze.py,sha256=RHbvk1G5ZUc3qLqlm1AZEyQzd_W_ZjcbCNDvW4YpTKQ,1252
|
29
|
-
rclone_api/cmd/copy_large_s3.py,sha256=
|
29
|
+
rclone_api/cmd/copy_large_s3.py,sha256=O1cAfrgL-M1F-j93qH2w6F9zNoJeXG_4v_pUwL0sXxY,3470
|
30
30
|
rclone_api/cmd/copy_large_s3_finish.py,sha256=SNCqkvu8YtxPKmBp37WVMP876YhxV0kJDoYuOSNPaPY,2309
|
31
31
|
rclone_api/cmd/list_files.py,sha256=x8FHODEilwKqwdiU1jdkeJbLwOqUkUQuDWPo2u_zpf0,741
|
32
32
|
rclone_api/cmd/save_to_db.py,sha256=ylvnhg_yzexM-m6Zr7XDiswvoDVSl56ELuFAdb9gqBY,1957
|
@@ -51,9 +51,9 @@ rclone_api/s3/multipart/upload_parts_inline.py,sha256=V7syKjFyVIe4U9Ahl5XgqVTzt9
|
|
51
51
|
rclone_api/s3/multipart/upload_parts_resumable.py,sha256=diJoUpVYow6No_dNgOZIYVsv43k4evb6zixqpzWJaUk,9771
|
52
52
|
rclone_api/s3/multipart/upload_parts_server_side_merge.py,sha256=Fp2pdrs5dONQI9LkfNolgAGj1-Z2V1SsRd0r0sreuXI,18040
|
53
53
|
rclone_api/s3/multipart/upload_state.py,sha256=f-Aq2NqtAaMUMhYitlICSNIxCKurWAl2gDEUVizLIqw,6019
|
54
|
-
rclone_api-1.4.
|
55
|
-
rclone_api-1.4.
|
56
|
-
rclone_api-1.4.
|
57
|
-
rclone_api-1.4.
|
58
|
-
rclone_api-1.4.
|
59
|
-
rclone_api-1.4.
|
54
|
+
rclone_api-1.4.28.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
55
|
+
rclone_api-1.4.28.dist-info/METADATA,sha256=sDgVDw-tZ4GUfd_JbHZXb5wDoqKOXoAuJt9uo8uR_W8,18592
|
56
|
+
rclone_api-1.4.28.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
|
57
|
+
rclone_api-1.4.28.dist-info/entry_points.txt,sha256=fJteOlYVwgX3UbNuL9jJ0zUTuX2O79JFAeNgK7Sw7EQ,255
|
58
|
+
rclone_api-1.4.28.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
59
|
+
rclone_api-1.4.28.dist-info/RECORD,,
|
@@ -1,154 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: rclone_api
|
3
|
-
Version: 1.4.25
|
4
|
-
Summary: rclone api in python
|
5
|
-
Home-page: https://github.com/zackees/rclone-api
|
6
|
-
License: BSD 3-Clause License
|
7
|
-
Keywords: template-python-cmd
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Requires-Python: >=3.10
|
10
|
-
Description-Content-Type: text/markdown
|
11
|
-
License-File: LICENSE
|
12
|
-
Requires-Dist: pyright>=1.1.393
|
13
|
-
Requires-Dist: python-dotenv>=1.0.0
|
14
|
-
Requires-Dist: certifi>=2025.1.31
|
15
|
-
Requires-Dist: psutil
|
16
|
-
Requires-Dist: boto3<=1.35.99,>=1.20.1
|
17
|
-
Requires-Dist: sqlmodel>=0.0.23
|
18
|
-
Requires-Dist: psycopg2-binary>=2.9.10
|
19
|
-
Requires-Dist: httpx>=0.28.1
|
20
|
-
Dynamic: home-page
|
21
|
-
|
22
|
-
# rclone-api
|
23
|
-
|
24
|
-
[](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
|
25
|
-
[](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
|
26
|
-
[](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
|
27
|
-
[](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
|
28
|
-
|
29
|
-
Started off as just python bindings to rclone, but this project is now adding features to fill in the gaps of the rclone app. The big features is streaming file diffs, stream file listings.
|
30
|
-
|
31
|
-
You will need to have rclone installed and on your path.
|
32
|
-
|
33
|
-
One of the benefits of this api is that it does not use 'shell=True' so therefore ctrl-c will work well in gracefully shutting down
|
34
|
-
|
35
|
-
# Install
|
36
|
-
|
37
|
-
`pip install rclone-api`
|
38
|
-
|
39
|
-
|
40
|
-
# Examples
|
41
|
-
|
42
|
-
You can use env variables or use a `.env` file to store your secrets.
|
43
|
-
|
44
|
-
|
45
|
-
# Rclone API Usage Examples
|
46
|
-
|
47
|
-
This script demonstrates how to interact with DigitalOcean Spaces using `rclone_api`.
|
48
|
-
|
49
|
-
## Setup & Usage
|
50
|
-
|
51
|
-
Ensure you have set the required environment variables:
|
52
|
-
|
53
|
-
- `BUCKET_NAME`
|
54
|
-
- `BUCKET_KEY_PUBLIC`
|
55
|
-
- `BUCKET_KEY_SECRET`
|
56
|
-
- `BUCKET_URL`
|
57
|
-
|
58
|
-
Then, run the following Python script:
|
59
|
-
|
60
|
-
```python
|
61
|
-
import os
|
62
|
-
from rclone_api import Config, DirListing, File, Rclone, Remote
|
63
|
-
|
64
|
-
# Load environment variables
|
65
|
-
BUCKET_NAME = os.getenv("BUCKET_NAME")
|
66
|
-
BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
|
67
|
-
BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
|
68
|
-
BUCKET_URL = "sfo3.digitaloceanspaces.com"
|
69
|
-
|
70
|
-
# Generate Rclone Configuration
|
71
|
-
def generate_rclone_config() -> Config:
|
72
|
-
config_text = f"""
|
73
|
-
[dst]
|
74
|
-
type = s3
|
75
|
-
provider = DigitalOcean
|
76
|
-
access_key_id = {BUCKET_KEY_PUBLIC}
|
77
|
-
secret_access_key = {BUCKET_KEY_SECRET}
|
78
|
-
endpoint = {BUCKET_URL}
|
79
|
-
"""
|
80
|
-
return Config(config_text)
|
81
|
-
|
82
|
-
rclone = Rclone(generate_rclone_config())
|
83
|
-
|
84
|
-
# List Available Remotes
|
85
|
-
print("\n=== Available Remotes ===")
|
86
|
-
remotes = rclone.listremotes()
|
87
|
-
for remote in remotes:
|
88
|
-
print(remote)
|
89
|
-
|
90
|
-
# List Contents of the Root Bucket
|
91
|
-
print("\n=== Listing Root Bucket ===")
|
92
|
-
listing = rclone.ls(f"dst:{BUCKET_NAME}", max_depth=-1)
|
93
|
-
|
94
|
-
print("\nDirectories:")
|
95
|
-
for dir in listing.dirs:
|
96
|
-
print(dir)
|
97
|
-
|
98
|
-
print("\nFiles:")
|
99
|
-
for file in listing.files:
|
100
|
-
print(file)
|
101
|
-
|
102
|
-
# List a Specific Subdirectory
|
103
|
-
print("\n=== Listing 'zachs_video' Subdirectory ===")
|
104
|
-
path = f"dst:{BUCKET_NAME}/zachs_video"
|
105
|
-
listing = rclone.ls(path)
|
106
|
-
print(listing)
|
107
|
-
|
108
|
-
# List PNG Files in a Subdirectory
|
109
|
-
print("\n=== Listing PNG Files ===")
|
110
|
-
listing = rclone.ls(path, glob="*.png")
|
111
|
-
|
112
|
-
if listing.files:
|
113
|
-
for file in listing.files:
|
114
|
-
print(file)
|
115
|
-
|
116
|
-
# Copy a File
|
117
|
-
print("\n=== Copying a File ===")
|
118
|
-
if listing.files:
|
119
|
-
file = listing.files[0]
|
120
|
-
new_path = f"dst:{BUCKET_NAME}/zachs_video/{file.name}_copy"
|
121
|
-
rclone.copyfile(file, new_path)
|
122
|
-
print(f"Copied {file.name} to {new_path}")
|
123
|
-
|
124
|
-
# Copy Multiple Files
|
125
|
-
print("\n=== Copying Multiple Files ===")
|
126
|
-
if listing.files:
|
127
|
-
file_mapping = {file.name: file.name + "_copy" for file in listing.files[:2]}
|
128
|
-
rclone.copyfiles(file_mapping)
|
129
|
-
print(f"Copied files: {file_mapping}")
|
130
|
-
|
131
|
-
# Delete a File
|
132
|
-
print("\n=== Deleting a File ===")
|
133
|
-
file_to_delete = f"dst:{BUCKET_NAME}/zachs_video/sample.png_copy"
|
134
|
-
rclone.delete_files([file_to_delete])
|
135
|
-
print(f"Deleted {file_to_delete}")
|
136
|
-
|
137
|
-
# Walk Through a Directory
|
138
|
-
print("\n=== Walking Through a Directory ===")
|
139
|
-
for dirlisting in rclone.walk(f"dst:{BUCKET_NAME}", max_depth=1):
|
140
|
-
print(dirlisting)
|
141
|
-
|
142
|
-
print("Done.")
|
143
|
-
```
|
144
|
-
|
145
|
-
|
146
|
-
To develop software, run `. ./activate`
|
147
|
-
|
148
|
-
# Windows
|
149
|
-
|
150
|
-
This environment requires you to use `git-bash`.
|
151
|
-
|
152
|
-
# Linting
|
153
|
-
|
154
|
-
Run `./lint`
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|