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 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
- tmp = self.size_file(files[0])
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
- msg = "\n#########################################\n"
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 += "#########################################\n"
52
- print(msg)
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(chunk=outchunk, dst_part="", exception=err)
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(chunk=outchunk, dst_part=part_dst)
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(chunk=outchunk, dst_part=part_dst, exception=e)
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
- return Exception(f"Failed to copy parts: {exceptions}", exceptions)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rclone_api
3
- Version: 1.5.61
3
+ Version: 1.5.64
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -1,4 +1,4 @@
1
- rclone_api/__init__.py,sha256=M3-jlyxhO7Md3FGMO8qBJ1Z7oLcbOjWshCHh2-CqKG4,34885
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=FXIkDOGnqp7zN9pks3WXoruIJki9M2sJd0-QX5cMQgg,52603
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=6-nlMclS8jyVvMvFbQDcZOX9MY1WbCcKA_s9bwuYxnk,9793
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.61.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
62
- rclone_api-1.5.61.dist-info/METADATA,sha256=6euBxBpkAYSnNXNCu_JDaYOiTIDW6vzyzHxpx09u-0g,37305
63
- rclone_api-1.5.61.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
64
- rclone_api-1.5.61.dist-info/entry_points.txt,sha256=ognh2e11HTjn73_KL5MWI67pBKS2jekBi-QTiRXySXA,316
65
- rclone_api-1.5.61.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
66
- rclone_api-1.5.61.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5