rclone-api 1.3.28__py2.py3-none-any.whl → 1.4.1__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 +491 -4
- rclone_api/cmd/copy_large_s3.py +17 -10
- rclone_api/db/db.py +3 -3
- rclone_api/detail/copy_file_parts.py +382 -0
- rclone_api/dir.py +1 -1
- rclone_api/dir_listing.py +1 -1
- rclone_api/file.py +8 -0
- rclone_api/file_part.py +198 -0
- rclone_api/file_stream.py +52 -0
- rclone_api/http_server.py +15 -21
- rclone_api/{rclone.py → rclone_impl.py} +153 -321
- rclone_api/remote.py +3 -3
- rclone_api/rpath.py +11 -4
- rclone_api/s3/chunk_task.py +3 -19
- rclone_api/s3/multipart/file_info.py +7 -0
- rclone_api/s3/multipart/finished_piece.py +38 -0
- rclone_api/s3/multipart/upload_info.py +62 -0
- rclone_api/s3/{chunk_types.py → multipart/upload_state.py} +3 -99
- rclone_api/s3/s3_multipart_uploader.py +138 -28
- rclone_api/s3/types.py +1 -1
- rclone_api/s3/upload_file_multipart.py +6 -13
- rclone_api/scan_missing_folders.py +1 -1
- rclone_api/types.py +136 -165
- rclone_api/util.py +22 -2
- {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/METADATA +1 -1
- rclone_api-1.4.1.dist-info/RECORD +55 -0
- rclone_api/mount_read_chunker.py +0 -130
- rclone_api/profile/mount_copy_bytes.py +0 -311
- rclone_api-1.3.28.dist-info/RECORD +0 -51
- /rclone_api/{walk.py → detail/walk.py} +0 -0
- {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/LICENSE +0 -0
- {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/WHEEL +0 -0
- {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/entry_points.txt +0 -0
- {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.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
|
-
|
197
|
-
return True
|
198
|
-
return self._size != other._size
|
218
|
+
return not self.__ne__(other)
|
199
219
|
|
200
|
-
def __lt__(self, other:
|
201
|
-
if not isinstance(other, SizeSuffix):
|
202
|
-
|
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:
|
206
|
-
if not isinstance(other, SizeSuffix):
|
207
|
-
|
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:
|
211
|
-
|
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:
|
216
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
def
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
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.
|
47
|
+
from rclone_api.rclone_impl import RcloneImpl
|
28
48
|
|
29
|
-
assert isinstance(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)
|
@@ -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=imz9lGMRz5Xt3xIMlokkQvLjejknz0OqNkUEvAzAPdA,3769
|
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.1.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
51
|
+
rclone_api-1.4.1.dist-info/METADATA,sha256=Mn2BS9-AcPzDKJ9aH0OxFYSuB7D5TFQS-yDGpUkLpvw,4627
|
52
|
+
rclone_api-1.4.1.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
|
53
|
+
rclone_api-1.4.1.dist-info/entry_points.txt,sha256=fJteOlYVwgX3UbNuL9jJ0zUTuX2O79JFAeNgK7Sw7EQ,255
|
54
|
+
rclone_api-1.4.1.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
55
|
+
rclone_api-1.4.1.dist-info/RECORD,,
|
rclone_api/mount_read_chunker.py
DELETED
@@ -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)
|