rclone-api 1.5.61__py3-none-any.whl → 1.5.64__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 +49 -35
- rclone_api/rclone_impl.py +2 -1
- rclone_api/s3/multipart/upload_parts_resumable.py +65 -8
- {rclone_api-1.5.61.dist-info → rclone_api-1.5.64.dist-info}/METADATA +1 -1
- {rclone_api-1.5.61.dist-info → rclone_api-1.5.64.dist-info}/RECORD +9 -9
- {rclone_api-1.5.61.dist-info → rclone_api-1.5.64.dist-info}/WHEEL +1 -1
- {rclone_api-1.5.61.dist-info → rclone_api-1.5.64.dist-info}/entry_points.txt +0 -0
- {rclone_api-1.5.61.dist-info → rclone_api-1.5.64.dist-info}/licenses/LICENSE +0 -0
- {rclone_api-1.5.61.dist-info → rclone_api-1.5.64.dist-info}/top_level.txt +0 -0
rclone_api/__init__.py
CHANGED
@@ -7,6 +7,8 @@ The API wraps the rclone command-line tool, providing a Pythonic interface
|
|
7
7
|
for common operations like copying, listing, and managing remote storage.
|
8
8
|
"""
|
9
9
|
|
10
|
+
import os
|
11
|
+
|
10
12
|
# Import core components and utilities
|
11
13
|
from datetime import datetime
|
12
14
|
from pathlib import Path
|
@@ -64,6 +66,18 @@ def rclone_verbose(val: bool | None) -> bool:
|
|
64
66
|
return _rclone_verbose(val)
|
65
67
|
|
66
68
|
|
69
|
+
class Logging:
|
70
|
+
@staticmethod
|
71
|
+
def enable_upload_parts_logging(value: bool) -> None:
|
72
|
+
"""
|
73
|
+
Enable or disable logging of upload parts.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
value: If True, enables upload parts logging; otherwise disables it.
|
77
|
+
"""
|
78
|
+
os.environ["LOG_UPLOAD_S3_RESUMABLE"] = "1" if value else "0"
|
79
|
+
|
80
|
+
|
67
81
|
class Rclone:
|
68
82
|
"""
|
69
83
|
Main interface for interacting with Rclone.
|
@@ -469,6 +483,41 @@ class Rclone:
|
|
469
483
|
"""
|
470
484
|
return self.impl.is_s3(dst=dst)
|
471
485
|
|
486
|
+
def copy_file_s3_resumable(
|
487
|
+
self,
|
488
|
+
src: str, # src:/Bucket/path/myfile.large.zst
|
489
|
+
dst: str, # dst:/Bucket/path/myfile.large.zst
|
490
|
+
part_infos: list[PartInfo] | None = None,
|
491
|
+
upload_threads: int = 8, # Number of reader and writer threads to use
|
492
|
+
merge_threads: int = 4, # Number of threads to use for merging the parts
|
493
|
+
) -> Exception | None:
|
494
|
+
"""
|
495
|
+
Copy a large file to S3 with resumable upload capability.
|
496
|
+
|
497
|
+
This method splits the file into parts for parallel upload and can
|
498
|
+
resume interrupted transfers using a custom algorithm in python.
|
499
|
+
|
500
|
+
Particularly useful for very large files where network interruptions
|
501
|
+
are likely.
|
502
|
+
|
503
|
+
Args:
|
504
|
+
src: Source file path (format: remote:bucket/path/file)
|
505
|
+
dst: Destination file path (format: remote:bucket/path/file)
|
506
|
+
part_infos: Optional list of part information for resuming uploads
|
507
|
+
upload_threads: Number of parallel upload threads
|
508
|
+
merge_threads: Number of threads for merging uploaded parts
|
509
|
+
|
510
|
+
Returns:
|
511
|
+
None if successful, Exception if an error occurred
|
512
|
+
"""
|
513
|
+
return self.impl.copy_file_s3_resumable(
|
514
|
+
src=src,
|
515
|
+
dst=dst,
|
516
|
+
part_infos=part_infos,
|
517
|
+
upload_threads=upload_threads,
|
518
|
+
merge_threads=merge_threads,
|
519
|
+
)
|
520
|
+
|
472
521
|
def copy_to(
|
473
522
|
self,
|
474
523
|
src: File | str,
|
@@ -831,41 +880,6 @@ class Rclone:
|
|
831
880
|
"""
|
832
881
|
return self.impl.copy_remote(src=src, dst=dst, args=args)
|
833
882
|
|
834
|
-
def copy_file_s3_resumable(
|
835
|
-
self,
|
836
|
-
src: str, # src:/Bucket/path/myfile.large.zst
|
837
|
-
dst: str, # dst:/Bucket/path/myfile.large.zst
|
838
|
-
part_infos: list[PartInfo] | None = None,
|
839
|
-
upload_threads: int = 8, # Number of reader and writer threads to use
|
840
|
-
merge_threads: int = 4, # Number of threads to use for merging the parts
|
841
|
-
) -> Exception | None:
|
842
|
-
"""
|
843
|
-
Copy a large file to S3 with resumable upload capability.
|
844
|
-
|
845
|
-
This method splits the file into parts for parallel upload and can
|
846
|
-
resume interrupted transfers using a custom algorithm in python.
|
847
|
-
|
848
|
-
Particularly useful for very large files where network interruptions
|
849
|
-
are likely.
|
850
|
-
|
851
|
-
Args:
|
852
|
-
src: Source file path (format: remote:bucket/path/file)
|
853
|
-
dst: Destination file path (format: remote:bucket/path/file)
|
854
|
-
part_infos: Optional list of part information for resuming uploads
|
855
|
-
upload_threads: Number of parallel upload threads
|
856
|
-
merge_threads: Number of threads for merging uploaded parts
|
857
|
-
|
858
|
-
Returns:
|
859
|
-
None if successful, Exception if an error occurred
|
860
|
-
"""
|
861
|
-
return self.impl.copy_file_s3_resumable(
|
862
|
-
src=src,
|
863
|
-
dst=dst,
|
864
|
-
part_infos=part_infos,
|
865
|
-
upload_threads=upload_threads,
|
866
|
-
merge_threads=merge_threads,
|
867
|
-
)
|
868
|
-
|
869
883
|
def mount(
|
870
884
|
self,
|
871
885
|
src: Remote | Dir | str,
|
rclone_api/rclone_impl.py
CHANGED
@@ -1363,7 +1363,8 @@ class RcloneImpl:
|
|
1363
1363
|
verbose = get_verbose(verbose)
|
1364
1364
|
check = get_check(check)
|
1365
1365
|
if len(files) < 2:
|
1366
|
-
|
1366
|
+
full_path = f"{src}/{files[0]}"
|
1367
|
+
tmp = self.size_file(full_path)
|
1367
1368
|
if isinstance(tmp, Exception):
|
1368
1369
|
return tmp
|
1369
1370
|
assert isinstance(tmp, SizeSuffix)
|
@@ -17,11 +17,28 @@ from rclone_api.types import (
|
|
17
17
|
SizeSuffix,
|
18
18
|
)
|
19
19
|
|
20
|
+
_LOCK = threading.Lock()
|
21
|
+
|
22
|
+
|
23
|
+
def _maybe_log(msg: str) -> None:
|
24
|
+
print(msg)
|
25
|
+
if os.getenv("LOG_UPLOAD_S3_RESUMABLE") == "1":
|
26
|
+
log_path = Path("log") / "upload.log"
|
27
|
+
with _LOCK:
|
28
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
29
|
+
# log_path.write_text(msg, append=True)
|
30
|
+
with open(log_path, mode="a", encoding="utf-8") as f:
|
31
|
+
f.write(msg)
|
32
|
+
f.write("\n")
|
33
|
+
|
20
34
|
|
21
35
|
@dataclass
|
22
36
|
class UploadPart:
|
23
37
|
chunk: Path
|
24
38
|
dst_part: str
|
39
|
+
part_num: int
|
40
|
+
total_parts: int
|
41
|
+
total_size: SizeSuffix
|
25
42
|
exception: Exception | None = None
|
26
43
|
finished: bool = False
|
27
44
|
|
@@ -46,10 +63,18 @@ def upload_task(self: RcloneImpl, upload_part: UploadPart) -> UploadPart:
|
|
46
63
|
if upload_part.exception is not None:
|
47
64
|
return upload_part
|
48
65
|
# print(f"Uploading {upload_part.chunk} to {upload_part.dst_part}")
|
49
|
-
|
66
|
+
num_parts = upload_part.total_parts
|
67
|
+
total_size = upload_part.total_size
|
68
|
+
part_num = upload_part.part_num
|
69
|
+
msg = "\n#############################################################\n"
|
50
70
|
msg += f"# Uploading {upload_part.chunk} to {upload_part.dst_part}\n"
|
51
|
-
msg += "
|
52
|
-
|
71
|
+
msg += f"# Part number: {part_num} / {num_parts}\n"
|
72
|
+
msg += f"# Total parts: {num_parts}\n"
|
73
|
+
msg += f"# Total size: {total_size.as_int()} bytes\n"
|
74
|
+
msg += f"# Chunk size: {upload_part.chunk.stat().st_size} bytes\n"
|
75
|
+
msg += f"# Range: {upload_part.chunk.name}\n"
|
76
|
+
msg += "##############################################################\n"
|
77
|
+
_maybe_log(msg)
|
53
78
|
self.copy_to(upload_part.chunk.as_posix(), upload_part.dst_part)
|
54
79
|
return upload_part
|
55
80
|
except Exception as e:
|
@@ -66,6 +91,9 @@ def read_task(
|
|
66
91
|
offset: SizeSuffix,
|
67
92
|
length: SizeSuffix,
|
68
93
|
part_dst: str,
|
94
|
+
part_number: int,
|
95
|
+
total_parts: int,
|
96
|
+
total_size: SizeSuffix,
|
69
97
|
) -> UploadPart:
|
70
98
|
outchunk: Path = tmpdir / f"{offset.as_int()}-{(offset + length).as_int()}.chunk"
|
71
99
|
range = Range(offset.as_int(), (offset + length).as_int())
|
@@ -77,10 +105,23 @@ def read_task(
|
|
77
105
|
dst=outchunk,
|
78
106
|
)
|
79
107
|
if isinstance(err, Exception):
|
80
|
-
out = UploadPart(
|
108
|
+
out = UploadPart(
|
109
|
+
chunk=outchunk,
|
110
|
+
dst_part="",
|
111
|
+
part_num=-1,
|
112
|
+
total_parts=total_parts,
|
113
|
+
total_size=SizeSuffix(0),
|
114
|
+
exception=err,
|
115
|
+
)
|
81
116
|
out.dispose()
|
82
117
|
return out
|
83
|
-
return UploadPart(
|
118
|
+
return UploadPart(
|
119
|
+
chunk=outchunk,
|
120
|
+
dst_part=part_dst,
|
121
|
+
part_num=part_number,
|
122
|
+
total_parts=total_parts,
|
123
|
+
total_size=total_size,
|
124
|
+
)
|
84
125
|
except KeyboardInterrupt as ke:
|
85
126
|
_thread.interrupt_main()
|
86
127
|
raise ke
|
@@ -88,7 +129,14 @@ def read_task(
|
|
88
129
|
_thread.interrupt_main()
|
89
130
|
raise se
|
90
131
|
except Exception as e:
|
91
|
-
return UploadPart(
|
132
|
+
return UploadPart(
|
133
|
+
chunk=outchunk,
|
134
|
+
dst_part=part_dst,
|
135
|
+
part_num=part_number,
|
136
|
+
total_parts=total_parts,
|
137
|
+
total_size=total_size,
|
138
|
+
exception=e,
|
139
|
+
)
|
92
140
|
|
93
141
|
|
94
142
|
def collapse_runs(numbers: list[int]) -> list[str]:
|
@@ -199,6 +247,7 @@ def upload_parts_resumable(
|
|
199
247
|
f"all_numbers_already_done: {collapse_runs(sorted(list(all_numbers_already_done)))}"
|
200
248
|
)
|
201
249
|
|
250
|
+
total_parts = len(part_infos)
|
202
251
|
filtered_part_infos: list[PartInfo] = []
|
203
252
|
for part_info in part_infos:
|
204
253
|
if part_info.part_number not in all_numbers_already_done:
|
@@ -261,6 +310,9 @@ def upload_parts_resumable(
|
|
261
310
|
offset=offset,
|
262
311
|
length=length,
|
263
312
|
part_dst=part_dst,
|
313
|
+
part_number=part_number,
|
314
|
+
total_parts=total_parts,
|
315
|
+
total_size=src_size,
|
264
316
|
)
|
265
317
|
|
266
318
|
read_fut: Future[UploadPart] = read_executor.submit(_read_task)
|
@@ -293,12 +345,17 @@ def upload_parts_resumable(
|
|
293
345
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
294
346
|
|
295
347
|
if len(exceptions) > 0:
|
296
|
-
|
348
|
+
msg = f"Failed to copy parts: {exceptions}"
|
349
|
+
_maybe_log(msg)
|
350
|
+
return Exception(msg, exceptions)
|
297
351
|
|
298
352
|
finished_parts: list[int] = info_json.fetch_all_finished_part_numbers()
|
299
353
|
print(f"finished_names: {finished_parts}")
|
300
354
|
|
301
355
|
diff_set = set(all_part_numbers).symmetric_difference(set(finished_parts))
|
302
356
|
all_part_numbers_done = len(diff_set) == 0
|
303
|
-
print(f"all_part_numbers_done: {all_part_numbers_done}")
|
357
|
+
# print(f"all_part_numbers_done: {all_part_numbers_done}")
|
358
|
+
msg = f"all_part_numbers_done: {all_part_numbers_done}"
|
359
|
+
_maybe_log(msg)
|
360
|
+
|
304
361
|
return None
|
@@ -1,4 +1,4 @@
|
|
1
|
-
rclone_api/__init__.py,sha256=
|
1
|
+
rclone_api/__init__.py,sha256=AH0_iCsWfPCphaXgN0eV7zeZ5WvinUYpyizwPIuUqmQ,35244
|
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=URZwMME01f0EZymprCESuZ5dk4IuUSKbHhwIeTHrn7A,6131
|
@@ -20,7 +20,7 @@ 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=V4Ax9AyNdC3m4O6gjWSbIJyCQCMhT-t0f-K8z6xux7Q,5946
|
23
|
-
rclone_api/rclone_impl.py,sha256=
|
23
|
+
rclone_api/rclone_impl.py,sha256=PppFWk4qo7gg6p_k3K7am3_6pytsLG6qTAKo6dBT-tI,52649
|
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
|
@@ -55,12 +55,12 @@ rclone_api/s3/multipart/info_json.py,sha256=-e8UCwrqjAP64U8PmH-o2ciJ6TN48DwHktJf
|
|
55
55
|
rclone_api/s3/multipart/merge_state.py,sha256=ziTB9CYV-OWaky5C1fOT9hifSY2zgUrk5HmX1Xeu2UA,4978
|
56
56
|
rclone_api/s3/multipart/upload_info.py,sha256=d6_OfzFR_vtDzCEegFfzCfWi2kUBUV4aXZzqAEVp1c4,1874
|
57
57
|
rclone_api/s3/multipart/upload_parts_inline.py,sha256=V7syKjFyVIe4U9Ahl5XgqVTzt9akiew3MFjGmufLo2w,12503
|
58
|
-
rclone_api/s3/multipart/upload_parts_resumable.py,sha256
|
58
|
+
rclone_api/s3/multipart/upload_parts_resumable.py,sha256=-KKrgXhCeDWp80JHBsFMEauQyxy6_X6LK0dWS7XSoYE,11621
|
59
59
|
rclone_api/s3/multipart/upload_parts_server_side_merge.py,sha256=Fp2pdrs5dONQI9LkfNolgAGj1-Z2V1SsRd0r0sreuXI,18040
|
60
60
|
rclone_api/s3/multipart/upload_state.py,sha256=f-Aq2NqtAaMUMhYitlICSNIxCKurWAl2gDEUVizLIqw,6019
|
61
|
-
rclone_api-1.5.
|
62
|
-
rclone_api-1.5.
|
63
|
-
rclone_api-1.5.
|
64
|
-
rclone_api-1.5.
|
65
|
-
rclone_api-1.5.
|
66
|
-
rclone_api-1.5.
|
61
|
+
rclone_api-1.5.64.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
62
|
+
rclone_api-1.5.64.dist-info/METADATA,sha256=vFhk8ddv9OgccApXBXfIPQYesY2lnNE3cThIdl1H91s,37305
|
63
|
+
rclone_api-1.5.64.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
64
|
+
rclone_api-1.5.64.dist-info/entry_points.txt,sha256=ognh2e11HTjn73_KL5MWI67pBKS2jekBi-QTiRXySXA,316
|
65
|
+
rclone_api-1.5.64.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
66
|
+
rclone_api-1.5.64.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|