rclone-api 1.3.28__py2.py3-none-any.whl → 1.4.2__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.
Files changed (34) hide show
  1. rclone_api/__init__.py +491 -4
  2. rclone_api/cmd/copy_large_s3.py +18 -10
  3. rclone_api/db/db.py +3 -3
  4. rclone_api/detail/copy_file_parts.py +382 -0
  5. rclone_api/dir.py +1 -1
  6. rclone_api/dir_listing.py +1 -1
  7. rclone_api/file.py +8 -0
  8. rclone_api/file_part.py +198 -0
  9. rclone_api/file_stream.py +52 -0
  10. rclone_api/http_server.py +15 -21
  11. rclone_api/{rclone.py → rclone_impl.py} +153 -321
  12. rclone_api/remote.py +3 -3
  13. rclone_api/rpath.py +11 -4
  14. rclone_api/s3/chunk_task.py +3 -19
  15. rclone_api/s3/multipart/file_info.py +7 -0
  16. rclone_api/s3/multipart/finished_piece.py +38 -0
  17. rclone_api/s3/multipart/upload_info.py +62 -0
  18. rclone_api/s3/{chunk_types.py → multipart/upload_state.py} +3 -99
  19. rclone_api/s3/s3_multipart_uploader.py +138 -28
  20. rclone_api/s3/types.py +1 -1
  21. rclone_api/s3/upload_file_multipart.py +6 -13
  22. rclone_api/scan_missing_folders.py +1 -1
  23. rclone_api/types.py +136 -165
  24. rclone_api/util.py +22 -2
  25. {rclone_api-1.3.28.dist-info → rclone_api-1.4.2.dist-info}/METADATA +1 -1
  26. rclone_api-1.4.2.dist-info/RECORD +55 -0
  27. rclone_api/mount_read_chunker.py +0 -130
  28. rclone_api/profile/mount_copy_bytes.py +0 -311
  29. rclone_api-1.3.28.dist-info/RECORD +0 -51
  30. /rclone_api/{walk.py → detail/walk.py} +0 -0
  31. {rclone_api-1.3.28.dist-info → rclone_api-1.4.2.dist-info}/LICENSE +0 -0
  32. {rclone_api-1.3.28.dist-info → rclone_api-1.4.2.dist-info}/WHEEL +0 -0
  33. {rclone_api-1.3.28.dist-info → rclone_api-1.4.2.dist-info}/entry_points.txt +0 -0
  34. {rclone_api-1.3.28.dist-info → rclone_api-1.4.2.dist-info}/top_level.txt +0 -0
rclone_api/types.py CHANGED
@@ -1,14 +1,11 @@
1
- import atexit
2
1
  import os
3
2
  import re
4
- import threading
5
3
  import time
6
4
  import warnings
7
5
  from dataclasses import dataclass
8
6
  from enum import Enum
9
7
  from pathlib import Path
10
8
  from threading import Lock
11
- from typing import Any
12
9
 
13
10
 
14
11
  class ModTimeStrategy(Enum):
@@ -164,14 +161,25 @@ class SizeSuffix:
164
161
  other_int = SizeSuffix(other)
165
162
  return SizeSuffix(self._size * other_int._size)
166
163
 
164
+ # multiply when int is on the left
165
+ def __rmul__(self, other: "int | SizeSuffix") -> "SizeSuffix":
166
+ return self.__mul__(other)
167
+
167
168
  def __add__(self, other: "int | SizeSuffix") -> "SizeSuffix":
168
169
  other_int = SizeSuffix(other)
169
170
  return SizeSuffix(self._size + other_int._size)
170
171
 
172
+ def __radd__(self, other: "int | SizeSuffix") -> "SizeSuffix":
173
+ return self.__add__(other)
174
+
171
175
  def __sub__(self, other: "int | SizeSuffix") -> "SizeSuffix":
172
176
  other_int = SizeSuffix(other)
173
177
  return SizeSuffix(self._size - other_int._size)
174
178
 
179
+ def __rsub__(self, other: "int | SizeSuffix") -> "SizeSuffix":
180
+ other_int = SizeSuffix(other)
181
+ return SizeSuffix(other_int._size - self._size)
182
+
175
183
  def __truediv__(self, other: "int | SizeSuffix") -> "SizeSuffix":
176
184
  other_int = SizeSuffix(other)
177
185
  if other_int._size == 0:
@@ -179,6 +187,13 @@ class SizeSuffix:
179
187
  # Use floor division to maintain integer arithmetic.
180
188
  return SizeSuffix(self._size // other_int._size)
181
189
 
190
+ def __rtruediv__(self, other: "int | SizeSuffix") -> "SizeSuffix":
191
+ other_int = SizeSuffix(other)
192
+ if self._size == 0:
193
+ raise ZeroDivisionError("Division by zero is undefined")
194
+ # Use floor division to maintain integer arithmetic.
195
+ return SizeSuffix(other_int._size // self._size)
196
+
182
197
  # support / division
183
198
  def __floordiv__(self, other: "int | SizeSuffix") -> "SizeSuffix":
184
199
  other_int = SizeSuffix(other)
@@ -187,35 +202,38 @@ class SizeSuffix:
187
202
  # Use floor division to maintain integer arithmetic.
188
203
  return SizeSuffix(self._size // other_int._size)
189
204
 
205
+ def __rfloordiv__(self, other: "int | SizeSuffix") -> "SizeSuffix":
206
+ other_int = SizeSuffix(other)
207
+ if self._size == 0:
208
+ raise ZeroDivisionError("Division by zero is undefined")
209
+ # Use floor division to maintain integer arithmetic.
210
+ return SizeSuffix(other_int._size // self._size)
211
+
190
212
  def __eq__(self, other: object) -> bool:
191
- if not isinstance(other, SizeSuffix):
213
+ if not isinstance(other, SizeSuffix) and not isinstance(other, int):
192
214
  return False
193
- return self._size == other._size
215
+ return self._size == SizeSuffix(other)._size
194
216
 
195
217
  def __ne__(self, other: object) -> bool:
196
- if not isinstance(other, SizeSuffix):
197
- return True
198
- return self._size != other._size
218
+ return not self.__ne__(other)
199
219
 
200
- def __lt__(self, other: object) -> bool:
201
- if not isinstance(other, SizeSuffix):
202
- return False
203
- return self._size < other._size
220
+ def __lt__(self, other: "int | SizeSuffix") -> bool:
221
+ # if not isinstance(other, SizeSuffix):
222
+ # return False
223
+ # return self._size < other._size
224
+ return self._size < SizeSuffix(other)._size
204
225
 
205
- def __le__(self, other: object) -> bool:
206
- if not isinstance(other, SizeSuffix):
207
- return False
208
- return self._size <= other._size
226
+ def __le__(self, other: "int | SizeSuffix") -> bool:
227
+ # if not isinstance(other, SizeSuffix):
228
+ # return False
229
+ # return self._size <= other._size
230
+ return self._size < SizeSuffix(other)._size
209
231
 
210
- def __gt__(self, other: object) -> bool:
211
- if not isinstance(other, SizeSuffix):
212
- return False
213
- return self._size > other._size
232
+ def __gt__(self, other: "int | SizeSuffix") -> bool:
233
+ return self._size > SizeSuffix(other)._size
214
234
 
215
- def __ge__(self, other: object) -> bool:
216
- if not isinstance(other, SizeSuffix):
217
- return False
218
- return self._size >= other._size
235
+ def __ge__(self, other: "int | SizeSuffix") -> bool:
236
+ return self._size >= SizeSuffix(other)._size
219
237
 
220
238
  def __hash__(self) -> int:
221
239
  return hash(self._size)
@@ -223,6 +241,16 @@ class SizeSuffix:
223
241
  def __int__(self) -> int:
224
242
  return self._size
225
243
 
244
+ def __iadd__(self, other: "int | SizeSuffix") -> "SizeSuffix":
245
+ other_int = SizeSuffix(other)
246
+ self._size += other_int._size
247
+ return self
248
+
249
+ def __isub__(self, other: "int | SizeSuffix") -> "SizeSuffix":
250
+ other_int = SizeSuffix(other)
251
+ self._size -= other_int._size
252
+ return self
253
+
226
254
 
227
255
  _TMP_DIR_ACCESS_LOCK = Lock()
228
256
 
@@ -269,150 +297,93 @@ class EndOfStream:
269
297
  pass
270
298
 
271
299
 
272
- _CLEANUP_LIST: list[Path] = []
273
-
274
-
275
- def _add_for_cleanup(path: Path) -> None:
276
- _CLEANUP_LIST.append(path)
277
-
278
-
279
- def _on_exit_cleanup() -> None:
280
- paths = list(_CLEANUP_LIST)
281
- for path in paths:
282
- try:
283
- if path.exists():
284
- path.unlink()
285
- except Exception as e:
286
- warnings.warn(f"Cannot cleanup {path}: {e}")
287
-
288
-
289
- atexit.register(_on_exit_cleanup)
290
-
291
-
292
- _FILEPARTS: list["FilePart"] = []
293
-
294
- _FILEPARTS_LOCK = Lock()
295
-
296
-
297
- def _add_filepart(part: "FilePart") -> None:
298
- with _FILEPARTS_LOCK:
299
- if part not in _FILEPARTS:
300
- _FILEPARTS.append(part)
300
+ class Range:
301
+ def __init__(self, start: int | SizeSuffix, end: int | SizeSuffix):
302
+ self.start: SizeSuffix = SizeSuffix(start) # inclusive
303
+ self.end: SizeSuffix = SizeSuffix(
304
+ end
305
+ ) # exclusive (not like http byte range which is inclusive)
306
+
307
+ def to_header(self) -> dict[str, str]:
308
+ last = self.end - 1
309
+ val = f"bytes={self.start.as_int()}-{last.as_int()}"
310
+ return {"Range": val}
311
+
312
+
313
+ _MAX_PART_NUMBER = 10000
314
+
315
+
316
+ def _get_chunk_size(
317
+ src_size: int | SizeSuffix, target_chunk_size: int | SizeSuffix
318
+ ) -> SizeSuffix:
319
+ src_size = SizeSuffix(src_size)
320
+ target_chunk_size = SizeSuffix(target_chunk_size)
321
+ min_chunk_size = src_size // (_MAX_PART_NUMBER - 1) # overriden
322
+ # chunk_size = max(min_chunk_size, target_chunk_size)
323
+ if min_chunk_size > target_chunk_size:
324
+ warnings.warn(
325
+ f"min_chunk_size: {min_chunk_size} is greater than target_chunk_size: {target_chunk_size}, adjusting target_chunk_size to min_chunk_size"
326
+ )
327
+ chunk_size = SizeSuffix(min_chunk_size)
328
+ else:
329
+ chunk_size = SizeSuffix(target_chunk_size)
330
+ return chunk_size
331
+
332
+
333
+ def _create_part_infos(
334
+ src_size: int | SizeSuffix, target_chunk_size: int | SizeSuffix
335
+ ) -> list["PartInfo"]:
336
+ # now break it up into 10 parts
337
+ target_chunk_size = SizeSuffix(target_chunk_size)
338
+ src_size = SizeSuffix(src_size)
339
+ chunk_size = _get_chunk_size(src_size=src_size, target_chunk_size=target_chunk_size)
340
+
341
+ part_infos: list[PartInfo] = []
342
+ curr_offset: int = 0
343
+ part_number: int = 0
344
+ while True:
345
+ part_number += 1
346
+ done = False
347
+ end = curr_offset + chunk_size
348
+ if end > src_size:
349
+ done = True
350
+ chunk_size = src_size - curr_offset
351
+ range: Range = Range(start=curr_offset, end=curr_offset + chunk_size)
352
+ part_info = PartInfo(
353
+ part_number=part_number,
354
+ range=range,
355
+ )
356
+ part_infos.append(part_info)
357
+ curr_offset += chunk_size.as_int()
358
+ if curr_offset >= src_size:
359
+ break
360
+ if done:
361
+ break
362
+ return part_infos
301
363
 
302
364
 
303
- def _remove_filepart(part: "FilePart") -> None:
304
- with _FILEPARTS_LOCK:
305
- if part in _FILEPARTS:
306
- _FILEPARTS.remove(part)
365
+ @dataclass
366
+ class PartInfo:
367
+ part_number: int
368
+ range: Range
307
369
 
370
+ @staticmethod
371
+ def split_parts(
372
+ size: int | SizeSuffix, target_chunk_size: int | SizeSuffix
373
+ ) -> list["PartInfo"]:
374
+ out = _create_part_infos(size, target_chunk_size)
375
+ return out
308
376
 
309
- def run_debug_parts():
310
- while True:
311
- print("\nAlive file parts:")
312
- for part in list(_FILEPARTS):
313
- print(part)
314
- # print(part.stacktrace)
315
- print("\n\n")
316
- time.sleep(60)
317
-
318
-
319
- dbg_thread = threading.Thread(target=run_debug_parts)
320
- dbg_thread.start()
321
-
322
-
323
- class FilePart:
324
- def __init__(self, payload: Path | bytes | Exception, extra: Any) -> None:
325
- import traceback
326
-
327
- from rclone_api.util import random_str
328
-
329
- stacktrace = traceback.format_stack()
330
- stacktrace_str = "".join(stacktrace)
331
- self.stacktrace = stacktrace_str
332
- # _FILEPARTS.append(self)
333
- _add_filepart(self)
334
-
335
- self.extra = extra
336
- self._lock = Lock()
337
- self.payload: Path | Exception
338
- if isinstance(payload, Exception):
339
- self.payload = payload
340
- return
341
- if isinstance(payload, bytes):
342
- print(f"Creating file part with payload: {len(payload)}")
343
- self.payload = get_chunk_tmpdir() / f"{random_str(12)}.chunk"
344
- with _TMP_DIR_ACCESS_LOCK:
345
- if not self.payload.parent.exists():
346
- self.payload.parent.mkdir(parents=True, exist_ok=True)
347
- self.payload.write_bytes(payload)
348
- _add_for_cleanup(self.payload)
349
- if isinstance(payload, Path):
350
- print("Adopting payload: ", payload)
351
- self.payload = payload
352
- _add_for_cleanup(self.payload)
353
-
354
- def get_file(self) -> Path | Exception:
355
- return self.payload
377
+ def __post_init__(self):
378
+ assert self.part_number >= 0
379
+ assert self.part_number <= 10000
380
+ assert self.range.start >= 0
381
+ assert self.range.end > self.range.start
356
382
 
357
383
  @property
358
- def size(self) -> int:
359
- with self._lock:
360
- if isinstance(self.payload, Path):
361
- return self.payload.stat().st_size
362
- return -1
363
-
364
- def n_bytes(self) -> int:
365
- with self._lock:
366
- if isinstance(self.payload, Path):
367
- return self.payload.stat().st_size
368
- return -1
369
-
370
- def load(self) -> bytes:
371
- with self._lock:
372
- if isinstance(self.payload, Path):
373
- with open(self.payload, "rb") as f:
374
- return f.read()
375
- raise ValueError("Cannot load from error")
376
-
377
- def __post_init__(self):
378
- if isinstance(self.payload, Path):
379
- assert self.payload.exists(), f"File part {self.payload} does not exist"
380
- assert self.payload.is_file(), f"File part {self.payload} is not a file"
381
- assert self.payload.stat().st_size > 0, f"File part {self.payload} is empty"
382
- elif isinstance(self.payload, Exception):
383
- warnings.warn(f"File part error: {self.payload}")
384
- print(f"File part created with payload: {self.payload}")
385
-
386
- def is_error(self) -> bool:
387
- return isinstance(self.payload, Exception)
388
-
389
- def dispose(self) -> None:
390
- # _FILEPARTS.remove(self)
391
- _remove_filepart(self)
392
- print("Disposing file part")
393
- with self._lock:
394
- if isinstance(self.payload, Exception):
395
- warnings.warn(
396
- f"Cannot close file part because the payload represents an error: {self.payload}"
397
- )
398
- print("Cannot close file part because the payload represents an error")
399
- return
400
- if self.payload.exists():
401
- print(f"File part {self.payload} exists")
402
- try:
403
- print(f"Unlinking file part {self.payload}")
404
- self.payload.unlink()
405
- print(f"File part {self.payload} deleted")
406
- except Exception as e:
407
- warnings.warn(f"Cannot close file part because of error: {e}")
408
- else:
409
- warnings.warn(
410
- f"Cannot close file part because it does not exist: {self.payload}"
411
- )
412
-
413
- def __del__(self):
414
- self.dispose()
415
-
416
- def __repr__(self):
417
- payload_str = "err" if self.is_error() else f"{SizeSuffix(self.n_bytes())}"
418
- return f"FilePart(payload={payload_str}, extra={self.extra})"
384
+ def name(self) -> str:
385
+ partnumber = f"{self.part_number:05d}"
386
+ offset = self.range.start.as_int()
387
+ end = SizeSuffix(self.range.end._size).as_int()
388
+ dst_name = f"part.{partnumber}_{offset}-{end}"
389
+ return dst_name
rclone_api/util.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import random
2
3
  import shutil
3
4
  import subprocess
4
5
  import warnings
@@ -23,10 +24,29 @@ def locked_print(*args, **kwargs):
23
24
  print(*args, **kwargs)
24
25
 
25
26
 
27
+ def port_is_free(port: int) -> bool:
28
+ import socket
29
+
30
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
31
+ return s.connect_ex(("localhost", port)) != 0
32
+
33
+
34
+ def find_free_port() -> int:
35
+ tries = 20
36
+ port = random.randint(10000, 20000)
37
+ while tries > 0:
38
+ if port_is_free(port):
39
+ return port
40
+ tries -= 1
41
+ port = random.randint(10000, 20000)
42
+ warnings.warn(f"Failed to find a free port, so using {port}")
43
+ return port
44
+
45
+
26
46
  def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
27
- from rclone_api.rclone import Rclone
47
+ from rclone_api.rclone_impl import RcloneImpl
28
48
 
29
- assert isinstance(rclone, Rclone)
49
+ assert isinstance(rclone, RcloneImpl)
30
50
  # if str then it will be remote:path
31
51
  if isinstance(item, str):
32
52
  # return RPath(item)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.3.28
3
+ Version: 1.4.2
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -0,0 +1,55 @@
1
+ rclone_api/__init__.py,sha256=cJpn62AmlcCXdbRIMIls4yEek6SLWJ-ONlr2mDnkIFk,17530
2
+ rclone_api/cli.py,sha256=dibfAZIh0kXWsBbfp3onKLjyZXo54mTzDjUdzJlDlWo,231
3
+ rclone_api/completed_process.py,sha256=_IZ8IWK7DM1_tsbDEkH6wPZ-bbcrgf7A7smls854pmg,1775
4
+ rclone_api/config.py,sha256=f6jEAxVorGFr31oHfcsu5AJTtOJj2wR5tTSsbGGZuIw,2558
5
+ rclone_api/convert.py,sha256=Mx9Qo7zhkOedJd8LdhPvNGHp8znJzOk4f_2KWnoGc78,1012
6
+ rclone_api/deprecated.py,sha256=qWKpnZdYcBK7YQZKuVoWWXDwi-uqiAtbjgPcci_efow,590
7
+ rclone_api/diff.py,sha256=tMoJMAGmLSE6Q_7QhPf6PnCzb840djxMZtDmhc2GlGQ,5227
8
+ rclone_api/dir.py,sha256=_9o-_5tbWVJkL1Vf_Yb8aiQV3xAqvUq5bk3zoJblvEg,3547
9
+ rclone_api/dir_listing.py,sha256=bSmd8yZtSeyVaDRw2JPB4bCpbCeDzhoa_pomgdJp44c,1884
10
+ rclone_api/exec.py,sha256=Bq0gkyZ10mEY0FRyzNZgdN4FaWP9vpeCk1kjpg-gN_8,1083
11
+ rclone_api/file.py,sha256=JLPqjUcW_YVb4UQjX6FQ7PABJqhchFUXVy1W-X9rJLk,5659
12
+ rclone_api/file_item.py,sha256=cH-AQYsxedhNPp4c8NHY1ad4Z7St4yf_VGbmiGD59no,1770
13
+ rclone_api/file_part.py,sha256=i6ByS5_sae8Eba-4imBVTxd-xKC8ExWy7NR8QGr0ors,6155
14
+ rclone_api/file_stream.py,sha256=_W3qnwCuigqA0hzXl2q5pAxSZDRaUSwet4BkT0lpnzs,1431
15
+ rclone_api/filelist.py,sha256=xbiusvNgaB_b_kQOZoHMJJxn6TWGtPrWd2J042BI28o,767
16
+ rclone_api/group_files.py,sha256=H92xPW9lQnbNw5KbtZCl00bD6iRh9yRbCuxku4j_3dg,8036
17
+ rclone_api/http_server.py,sha256=YrILbxK0Xpnpkm8l9uGEXH_AOCGPUX8Cx33jKVtu7ZM,8207
18
+ rclone_api/log.py,sha256=VZHM7pNSXip2ZLBKMP7M1u-rp_F7zoafFDuR8CPUoKI,1271
19
+ rclone_api/mount.py,sha256=TE_VIBMW7J1UkF_6HRCt8oi_jGdMov4S51bm2OgxFAM,10045
20
+ rclone_api/process.py,sha256=BGXJTZVT__jeaDyjN8_kRycliOhkBErMPdHO1hKRvJE,5271
21
+ rclone_api/rclone_impl.py,sha256=BAnRi4jB_TLRHwZE_mQfX7b1L1SODn3XhdHL7hozTAs,47960
22
+ rclone_api/remote.py,sha256=mTgMTQTwxUmbLjTpr-AGTId2ycXKI9mLX5L7PPpDIoc,520
23
+ rclone_api/rpath.py,sha256=Y1JjQWcie39EgQrq-UtbfDz5yDLCwwfu27W7AQXllSE,2860
24
+ rclone_api/scan_missing_folders.py,sha256=-8NCwpCaHeHrX-IepCoAEsX1rl8S-GOCxcIhTr_w3gA,4747
25
+ rclone_api/types.py,sha256=gpEYVHNkxB7X3p_B4nEc1ls1kF_ykjNuUyZXCsvU-Cs,11788
26
+ rclone_api/util.py,sha256=j252WPB-UMiz6zQcLvsZe9XvMq-Z2FIeZrjb-y01eL4,5947
27
+ rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
28
+ rclone_api/cmd/analyze.py,sha256=RHbvk1G5ZUc3qLqlm1AZEyQzd_W_ZjcbCNDvW4YpTKQ,1252
29
+ rclone_api/cmd/copy_large_s3.py,sha256=w1aCaq9EQ84aRoMj9mHtdr0Svjflkx6KTKcoldzRSo8,3788
30
+ rclone_api/cmd/list_files.py,sha256=x8FHODEilwKqwdiU1jdkeJbLwOqUkUQuDWPo2u_zpf0,741
31
+ rclone_api/cmd/save_to_db.py,sha256=ylvnhg_yzexM-m6Zr7XDiswvoDVSl56ELuFAdb9gqBY,1957
32
+ rclone_api/db/__init__.py,sha256=OSRUdnSWUlDTOHmjdjVmxYTUNpTbtaJ5Ll9sl-PfZg0,40
33
+ rclone_api/db/db.py,sha256=YRnYrCaXHwytQt07uEZ_mMpvPHo9-0IWcOb95fVOOfs,10086
34
+ rclone_api/db/models.py,sha256=v7qaXUehvsDvU51uk69JI23fSIs9JFGcOa-Tv1c_wVs,1600
35
+ rclone_api/detail/copy_file_parts.py,sha256=KgLl5vVF67RLqZ1aUmRR98BvoqTrbcFzPY9oZd0yyI0,12316
36
+ rclone_api/detail/walk.py,sha256=-54NVE8EJcCstwDoaC_UtHm73R2HrZwVwQmsnv55xNU,3369
37
+ rclone_api/experimental/flags.py,sha256=qCVD--fSTmzlk9hloRLr0q9elzAOFzPsvVpKM3aB1Mk,2739
38
+ rclone_api/experimental/flags_base.py,sha256=ajU_czkTcAxXYU-SlmiCfHY7aCQGHvpCLqJ-Z8uZLk0,2102
39
+ rclone_api/s3/api.py,sha256=PafsIEyWDpLWAXsZAjFm9CY14vJpsDr9lOsn0kGRLZ0,4009
40
+ rclone_api/s3/basic_ops.py,sha256=hK3366xhVEzEcjz9Gk_8lFx6MRceAk72cax6mUrr6ko,2104
41
+ rclone_api/s3/chunk_task.py,sha256=waEYe-iYQ1_BR3NCS4BrzVrK9UANvH1EcbXx2I6Z_NM,6839
42
+ rclone_api/s3/create.py,sha256=wgfkapv_j904CfKuWyiBIWJVxfAx_ftemFSUV14aT68,3149
43
+ rclone_api/s3/s3_multipart_uploader.py,sha256=KY9k585Us0VW8uqgS5jdSVrYynxlnO8Wzb52pcvR06M,4570
44
+ rclone_api/s3/types.py,sha256=VqnvH0qhvb3_4wngqk0vSSStH-TgVQxNNxo9slz9-p8,1595
45
+ rclone_api/s3/upload_file_multipart.py,sha256=V7syKjFyVIe4U9Ahl5XgqVTzt9akiew3MFjGmufLo2w,12503
46
+ rclone_api/s3/multipart/file_info.py,sha256=8v_07_eADo0K-Nsv7F0Ac1wcv3lkIsrR3MaRCmkYLTQ,105
47
+ rclone_api/s3/multipart/finished_piece.py,sha256=TcwA58-qgKBiskfHrePoCWaSSep6Za9psZEpzrLUUhE,1199
48
+ rclone_api/s3/multipart/upload_info.py,sha256=d6_OfzFR_vtDzCEegFfzCfWi2kUBUV4aXZzqAEVp1c4,1874
49
+ rclone_api/s3/multipart/upload_state.py,sha256=f-Aq2NqtAaMUMhYitlICSNIxCKurWAl2gDEUVizLIqw,6019
50
+ rclone_api-1.4.2.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
51
+ rclone_api-1.4.2.dist-info/METADATA,sha256=oRC3WBMs6KdWFK8NOJOaa2LjKC4ze4tKy7fQlU63uIA,4627
52
+ rclone_api-1.4.2.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
53
+ rclone_api-1.4.2.dist-info/entry_points.txt,sha256=fJteOlYVwgX3UbNuL9jJ0zUTuX2O79JFAeNgK7Sw7EQ,255
54
+ rclone_api-1.4.2.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
55
+ rclone_api-1.4.2.dist-info/RECORD,,
@@ -1,130 +0,0 @@
1
- import logging
2
- import traceback
3
- from concurrent.futures import Future, ThreadPoolExecutor
4
- from pathlib import Path
5
- from threading import Lock, Semaphore
6
- from typing import Any
7
-
8
- from rclone_api.mount import Mount
9
- from rclone_api.types import FilePart, SizeSuffix
10
-
11
- # Create a logger for this module
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- def _read_from_mount_task(
16
- offset: int, size: int, path: Path, verbose: bool
17
- ) -> bytes | Exception:
18
- if verbose:
19
- logger.debug(f"Fetching chunk: offset={offset}, size={size}, path={path}")
20
- try:
21
- with path.open("rb") as f:
22
- f.seek(offset)
23
- payload = f.read(size)
24
- assert len(payload) == size, f"Invalid read size: {len(payload)}"
25
- return payload
26
-
27
- except KeyboardInterrupt as e:
28
- import _thread
29
-
30
- logger.error("KeyboardInterrupt received during chunk read")
31
- _thread.interrupt_main()
32
- return Exception(e)
33
- except Exception as e:
34
- stack_trace = traceback.format_exc()
35
- logger.error(
36
- f"Error fetching file chunk at offset {offset} + {size}: {e}\n{stack_trace}"
37
- )
38
- return e
39
-
40
-
41
- class MultiMountFileChunker:
42
- def __init__(
43
- self,
44
- filename: str,
45
- filesize: int,
46
- mounts: list[Mount],
47
- executor: ThreadPoolExecutor,
48
- verbose: bool | None,
49
- ) -> None:
50
- from rclone_api.util import get_verbose
51
-
52
- self.filename = filename
53
- self.filesize = filesize
54
- self.executor = executor
55
- self.mounts_processing: list[Mount] = []
56
- self.mounts_availabe: list[Mount] = mounts
57
- self.semaphore = Semaphore(len(mounts))
58
- self.lock = Lock()
59
- self.verbose = get_verbose(verbose)
60
- logger.info(
61
- f"Initialized MultiMountFileChunker for {filename} ({filesize} bytes)"
62
- )
63
-
64
- def shutdown(self) -> None:
65
- logger.info("Shutting down MultiMountFileChunker")
66
- self.executor.shutdown(wait=True, cancel_futures=True)
67
- with ThreadPoolExecutor() as executor:
68
- for mount in self.mounts_processing:
69
- executor.submit(lambda: mount.close())
70
- logger.debug("MultiMountFileChunker shutdown complete")
71
-
72
- def _acquire_mount(self) -> Mount:
73
- logger.debug("Acquiring mount")
74
- self.semaphore.acquire()
75
- with self.lock:
76
- mount = self.mounts_availabe.pop()
77
- self.mounts_processing.append(mount)
78
- logger.debug(f"Mount acquired: {mount}")
79
- return mount
80
-
81
- def _release_mount(self, mount: Mount) -> None:
82
- logger.debug(f"Releasing mount: {mount}")
83
- with self.lock:
84
- self.mounts_processing.remove(mount)
85
- self.mounts_availabe.append(mount)
86
- self.semaphore.release()
87
- logger.debug("Mount released")
88
-
89
- def fetch(
90
- self, offset: int | SizeSuffix, size: int | SizeSuffix, extra: Any
91
- ) -> Future[FilePart]:
92
- offset = SizeSuffix(offset).as_int()
93
- size = SizeSuffix(size).as_int()
94
- if isinstance(size, SizeSuffix):
95
- size = size.as_int()
96
- if self.verbose:
97
- logger.debug(f"Fetching data range: offset={offset}, size={size}")
98
-
99
- assert size > 0, f"Invalid size: {size}"
100
- assert offset >= 0, f"Invalid offset: {offset}"
101
- assert (
102
- offset + size <= self.filesize
103
- ), f"Invalid offset + size: {offset} + {size} ({offset+size}) > {self.filesize}"
104
-
105
- try:
106
- mount = self._acquire_mount()
107
- path = mount.mount_path / self.filename
108
-
109
- def task_fetch_file_range(
110
- size=size, path=path, mount=mount, verbose=self.verbose
111
- ) -> FilePart:
112
- bytes_or_err = _read_from_mount_task(
113
- offset=offset, size=size, path=path, verbose=verbose
114
- )
115
- self._release_mount(mount)
116
-
117
- if isinstance(bytes_or_err, Exception):
118
- err: Exception = bytes_or_err
119
- logger.warning(f"Fetch task returned exception: {bytes_or_err}")
120
- return FilePart(payload=err, extra=extra)
121
- logger.debug(f"Successfully fetched {size} bytes from offset {offset}")
122
- out = FilePart(payload=bytes_or_err, extra=extra)
123
- return out
124
-
125
- fut = self.executor.submit(task_fetch_file_range)
126
- return fut
127
- except Exception as e:
128
- logger.error(f"Error setting up file chunk fetch: {e}", exc_info=True)
129
- fp = FilePart(payload=e, extra=extra)
130
- return self.executor.submit(lambda: fp)