rclone-api 1.1.94__py2.py3-none-any.whl → 1.1.96__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/profile/mount_copy_bytes.py +14 -16
- rclone_api/rclone.py +19 -17
- rclone_api/s3/api.py +5 -1
- rclone_api/s3/chunk_file.py +52 -24
- rclone_api/s3/types.py +3 -0
- rclone_api/s3/upload_file_multipart.py +5 -1
- {rclone_api-1.1.94.dist-info → rclone_api-1.1.96.dist-info}/METADATA +1 -1
- {rclone_api-1.1.94.dist-info → rclone_api-1.1.96.dist-info}/RECORD +12 -12
- {rclone_api-1.1.94.dist-info → rclone_api-1.1.96.dist-info}/LICENSE +0 -0
- {rclone_api-1.1.94.dist-info → rclone_api-1.1.96.dist-info}/WHEEL +0 -0
- {rclone_api-1.1.94.dist-info → rclone_api-1.1.96.dist-info}/entry_points.txt +0 -0
- {rclone_api-1.1.94.dist-info → rclone_api-1.1.96.dist-info}/top_level.txt +0 -0
|
@@ -130,27 +130,23 @@ def _run_profile(
|
|
|
130
130
|
)
|
|
131
131
|
bytes_count = 0
|
|
132
132
|
|
|
133
|
+
futures: list[Future[bytes | Exception]] = []
|
|
134
|
+
for i in range(num):
|
|
135
|
+
offset = SizeSuffix(i * chunk_size.as_int()) + offset
|
|
136
|
+
future = filechunker.fetch(offset.as_int(), size.as_int())
|
|
137
|
+
futures.append(future)
|
|
138
|
+
|
|
139
|
+
for future in futures:
|
|
140
|
+
bytes_or_err = future.result()
|
|
141
|
+
if isinstance(bytes_or_err, Exception):
|
|
142
|
+
assert False, f"Error: {bytes_or_err}"
|
|
143
|
+
futures.clear()
|
|
133
144
|
|
|
134
145
|
start = time.time()
|
|
135
146
|
net_io_start = psutil.net_io_counters()
|
|
136
147
|
|
|
137
|
-
# bytes_or_err: bytes | Exception = rclone.copy_bytes(
|
|
138
|
-
# src=src_file,
|
|
139
|
-
# offset=offset.as_int(),
|
|
140
|
-
# length=size.as_int(),
|
|
141
|
-
# chunk_size=chunk_size,
|
|
142
|
-
# direct_io=direct_io,
|
|
143
|
-
# max_threads=transfers,
|
|
144
|
-
# mount_log=mount_log,
|
|
145
|
-
# )
|
|
146
|
-
|
|
147
|
-
# bytes_or_err = filechunker.fetch(
|
|
148
|
-
# (offset + SizeSuffix("1G")).as_int(), size.as_int()
|
|
149
|
-
# )
|
|
150
|
-
|
|
151
148
|
offset = SizeSuffix("1G")
|
|
152
149
|
|
|
153
|
-
futures: list[Future[bytes | Exception]] = []
|
|
154
150
|
for i in range(num):
|
|
155
151
|
offset = SizeSuffix(i * chunk_size.as_int()) + offset
|
|
156
152
|
future = filechunker.fetch(offset.as_int(), size.as_int())
|
|
@@ -165,7 +161,9 @@ def _run_profile(
|
|
|
165
161
|
diff = (time.time() - start) / num
|
|
166
162
|
net_io_end = psutil.net_io_counters()
|
|
167
163
|
# self.assertEqual(len(bytes_or_err), size)
|
|
168
|
-
assert
|
|
164
|
+
assert (
|
|
165
|
+
bytes_count == SizeSuffix(size * num).as_int()
|
|
166
|
+
), f"Length: {SizeSuffix(bytes_count)} != {SizeSuffix(size* num)}"
|
|
169
167
|
|
|
170
168
|
# print io stats
|
|
171
169
|
bytes_sent = (net_io_end.bytes_sent - net_io_start.bytes_sent) // num
|
rclone_api/rclone.py
CHANGED
|
@@ -777,9 +777,18 @@ class Rclone:
|
|
|
777
777
|
endpoint_url=section.endpoint(),
|
|
778
778
|
)
|
|
779
779
|
|
|
780
|
+
chunk_fetcher: MultiMountFileChunker = self.get_multi_mount_file_chunker(
|
|
781
|
+
src=src_path.parent.as_posix(),
|
|
782
|
+
chunk_size=chunk_size,
|
|
783
|
+
threads=read_threads,
|
|
784
|
+
mount_log=mount_log,
|
|
785
|
+
direct_io=True,
|
|
786
|
+
)
|
|
787
|
+
|
|
780
788
|
client = S3Client(s3_creds)
|
|
781
|
-
|
|
789
|
+
upload_config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
|
|
782
790
|
chunk_size=chunk_size.as_int(),
|
|
791
|
+
chunk_fetcher=chunk_fetcher.fetch,
|
|
783
792
|
max_write_threads=write_threads,
|
|
784
793
|
retries=retries,
|
|
785
794
|
resume_path_json=save_state_json,
|
|
@@ -791,10 +800,7 @@ class Rclone:
|
|
|
791
800
|
print(f"Uploading {name} to {s3_key} in bucket {bucket_name}")
|
|
792
801
|
print(f"Source: {src_path}")
|
|
793
802
|
print(f"bucket_name: {bucket_name}")
|
|
794
|
-
print(f"upload_config: {
|
|
795
|
-
|
|
796
|
-
upload_target: S3UploadTarget
|
|
797
|
-
upload_config: S3MutliPartUploadConfig
|
|
803
|
+
print(f"upload_config: {upload_config}")
|
|
798
804
|
|
|
799
805
|
upload_target = S3UploadTarget(
|
|
800
806
|
bucket_name=bucket_name,
|
|
@@ -802,18 +808,14 @@ class Rclone:
|
|
|
802
808
|
s3_key=s3_key,
|
|
803
809
|
)
|
|
804
810
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
out: MultiUploadResult = client.upload_file_multipart(
|
|
814
|
-
upload_target=upload_target, upload_config=upload_config
|
|
815
|
-
)
|
|
816
|
-
return out
|
|
811
|
+
try:
|
|
812
|
+
out: MultiUploadResult = client.upload_file_multipart(
|
|
813
|
+
upload_target=upload_target,
|
|
814
|
+
upload_config=upload_config,
|
|
815
|
+
)
|
|
816
|
+
return out
|
|
817
|
+
finally:
|
|
818
|
+
chunk_fetcher.close()
|
|
817
819
|
|
|
818
820
|
def _copy_bytes(
|
|
819
821
|
self,
|
rclone_api/s3/api.py
CHANGED
|
@@ -11,7 +11,10 @@ from rclone_api.s3.basic_ops import (
|
|
|
11
11
|
)
|
|
12
12
|
from rclone_api.s3.create import create_s3_client
|
|
13
13
|
from rclone_api.s3.types import S3Credentials, S3MutliPartUploadConfig, S3UploadTarget
|
|
14
|
-
from rclone_api.s3.upload_file_multipart import
|
|
14
|
+
from rclone_api.s3.upload_file_multipart import (
|
|
15
|
+
MultiUploadResult,
|
|
16
|
+
upload_file_multipart,
|
|
17
|
+
)
|
|
15
18
|
|
|
16
19
|
_MIN_THRESHOLD_FOR_CHUNKING = 5 * 1024 * 1024
|
|
17
20
|
|
|
@@ -67,6 +70,7 @@ class S3Client:
|
|
|
67
70
|
|
|
68
71
|
out = upload_file_multipart(
|
|
69
72
|
s3_client=self.client,
|
|
73
|
+
chunk_fetcher=upload_config.chunk_fetcher,
|
|
70
74
|
bucket_name=bucket_name,
|
|
71
75
|
file_path=upload_target.src_file,
|
|
72
76
|
object_name=upload_target.s3_key,
|
rclone_api/s3/chunk_file.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import time
|
|
2
2
|
import warnings
|
|
3
|
+
from concurrent.futures import Future
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from queue import Queue
|
|
5
6
|
from threading import Event
|
|
7
|
+
from typing import Callable
|
|
6
8
|
|
|
7
9
|
from rclone_api.s3.chunk_types import FileChunk, UploadState
|
|
8
10
|
from rclone_api.util import locked_print
|
|
@@ -27,12 +29,16 @@ def _get_file_size(file_path: Path, timeout: int = 60) -> int:
|
|
|
27
29
|
|
|
28
30
|
def file_chunker(
|
|
29
31
|
upload_state: UploadState,
|
|
32
|
+
chunk_fetcher: Callable[[int, int], Future[bytes | Exception]],
|
|
30
33
|
max_chunks: int | None,
|
|
31
34
|
cancel_signal: Event,
|
|
32
35
|
output: Queue[FileChunk | None],
|
|
33
36
|
) -> None:
|
|
34
37
|
count = 0
|
|
35
38
|
|
|
39
|
+
if False:
|
|
40
|
+
print(chunk_fetcher)
|
|
41
|
+
|
|
36
42
|
def should_stop() -> bool:
|
|
37
43
|
nonlocal count
|
|
38
44
|
|
|
@@ -41,14 +47,6 @@ def file_chunker(
|
|
|
41
47
|
if count >= max_chunks:
|
|
42
48
|
return True
|
|
43
49
|
count += 1
|
|
44
|
-
if count > 10 and count % 10 == 0:
|
|
45
|
-
# recheck that the file size has not changed
|
|
46
|
-
file_size = _get_file_size(upload_state.upload_info.src_file_path)
|
|
47
|
-
if file_size != upload_state.upload_info.file_size:
|
|
48
|
-
locked_print(
|
|
49
|
-
f"File size changed, cannot resume, expected {upload_state.upload_info.file_size}, got {file_size}"
|
|
50
|
-
)
|
|
51
|
-
raise ValueError("File size changed, cannot resume")
|
|
52
50
|
return False
|
|
53
51
|
|
|
54
52
|
upload_info = upload_state.upload_info
|
|
@@ -58,7 +56,6 @@ def file_chunker(
|
|
|
58
56
|
# Mounted files may take a while to appear, so keep retrying.
|
|
59
57
|
|
|
60
58
|
try:
|
|
61
|
-
file_size = _get_file_size(src, timeout=60)
|
|
62
59
|
part_number = 1
|
|
63
60
|
done_part_numbers: set[int] = {
|
|
64
61
|
p.part_number for p in upload_state.parts if p is not None
|
|
@@ -86,25 +83,56 @@ def file_chunker(
|
|
|
86
83
|
break
|
|
87
84
|
assert curr_parth_num is not None
|
|
88
85
|
offset = (curr_parth_num - 1) * chunk_size
|
|
86
|
+
file_size = upload_info.file_size
|
|
89
87
|
|
|
90
88
|
assert offset < file_size, f"Offset {offset} is greater than file size"
|
|
91
89
|
|
|
92
90
|
# Open the file, seek, read the chunk, and close immediately.
|
|
93
|
-
with open(file_path, "rb") as f:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
# with open(file_path, "rb") as f:
|
|
92
|
+
# f.seek(offset)
|
|
93
|
+
# data = f.read(chunk_size)
|
|
94
|
+
|
|
95
|
+
# data = chunk_fetcher(offset, chunk_size).result()
|
|
96
|
+
|
|
97
|
+
assert curr_parth_num is not None
|
|
98
|
+
cpn: int = curr_parth_num
|
|
99
|
+
|
|
100
|
+
def on_complete(
|
|
101
|
+
fut: Future[bytes | Exception],
|
|
102
|
+
part_number: int = cpn,
|
|
103
|
+
file_path: Path = file_path,
|
|
104
|
+
) -> None:
|
|
105
|
+
data: bytes | Exception = fut.result()
|
|
106
|
+
if isinstance(data, Exception):
|
|
107
|
+
warnings.warn(
|
|
108
|
+
f"Error reading file: {data}, skipping part {part_number}"
|
|
109
|
+
)
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
if isinstance(data, Exception):
|
|
113
|
+
err: Exception = data
|
|
114
|
+
warnings.warn(
|
|
115
|
+
f"Error reading file: {err}, skipping part {part_number}"
|
|
116
|
+
)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if not data:
|
|
120
|
+
warnings.warn(f"Empty data for part {part_number} of {file_path}")
|
|
121
|
+
|
|
122
|
+
file_chunk = FileChunk(
|
|
123
|
+
src,
|
|
124
|
+
upload_id=upload_info.upload_id,
|
|
125
|
+
part_number=part_number,
|
|
126
|
+
data=data, # After this, data should not be reused.
|
|
127
|
+
)
|
|
128
|
+
done_part_numbers.add(part_number)
|
|
129
|
+
output.put(file_chunk)
|
|
130
|
+
|
|
131
|
+
fut = chunk_fetcher(curr_parth_num, chunk_size)
|
|
132
|
+
fut.add_done_callback(on_complete)
|
|
133
|
+
# wait until the output queue can accept the next chunk
|
|
134
|
+
while output.full():
|
|
135
|
+
time.sleep(0.1)
|
|
108
136
|
except Exception as e:
|
|
109
137
|
|
|
110
138
|
warnings.warn(f"Error reading file: {e}")
|
rclone_api/s3/types.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from concurrent.futures import Future
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from pathlib import Path
|
|
5
|
+
from typing import Callable
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class S3Provider(Enum):
|
|
@@ -45,6 +47,7 @@ class S3MutliPartUploadConfig:
|
|
|
45
47
|
|
|
46
48
|
chunk_size: int
|
|
47
49
|
retries: int
|
|
50
|
+
chunk_fetcher: Callable[[int, int], Future[bytes | Exception]]
|
|
48
51
|
resume_path_json: Path
|
|
49
52
|
max_write_threads: int
|
|
50
53
|
max_chunks_before_suspension: int | None = None
|
|
@@ -2,10 +2,11 @@ import _thread
|
|
|
2
2
|
import os
|
|
3
3
|
import traceback
|
|
4
4
|
import warnings
|
|
5
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from queue import Queue
|
|
8
8
|
from threading import Event, Thread
|
|
9
|
+
from typing import Callable
|
|
9
10
|
|
|
10
11
|
from botocore.client import BaseClient
|
|
11
12
|
|
|
@@ -117,6 +118,7 @@ def _abort_previous_upload(upload_state: UploadState) -> None:
|
|
|
117
118
|
|
|
118
119
|
def upload_file_multipart(
|
|
119
120
|
s3_client: BaseClient,
|
|
121
|
+
chunk_fetcher: Callable[[int, int], Future[bytes | Exception]],
|
|
120
122
|
bucket_name: str,
|
|
121
123
|
file_path: Path,
|
|
122
124
|
object_name: str,
|
|
@@ -211,6 +213,7 @@ def upload_file_multipart(
|
|
|
211
213
|
|
|
212
214
|
def chunker_task(
|
|
213
215
|
upload_state=upload_state,
|
|
216
|
+
chunk_fetcher=chunk_fetcher,
|
|
214
217
|
output=filechunks,
|
|
215
218
|
max_chunks=max_chunks_before_suspension,
|
|
216
219
|
cancel_signal=cancel_chunker_event,
|
|
@@ -219,6 +222,7 @@ def upload_file_multipart(
|
|
|
219
222
|
try:
|
|
220
223
|
file_chunker(
|
|
221
224
|
upload_state=upload_state,
|
|
225
|
+
chunk_fetcher=chunk_fetcher,
|
|
222
226
|
output=output,
|
|
223
227
|
max_chunks=max_chunks,
|
|
224
228
|
cancel_signal=cancel_signal,
|
|
@@ -13,7 +13,7 @@ rclone_api/filelist.py,sha256=xbiusvNgaB_b_kQOZoHMJJxn6TWGtPrWd2J042BI28o,767
|
|
|
13
13
|
rclone_api/group_files.py,sha256=H92xPW9lQnbNw5KbtZCl00bD6iRh9yRbCuxku4j_3dg,8036
|
|
14
14
|
rclone_api/mount.py,sha256=pU-YTHTLYMXuNHVkT9ACS1wdZKazXXAhVWQUD6ZvP40,14535
|
|
15
15
|
rclone_api/process.py,sha256=rBj_S86jC6nqCYop-jq8r9eMSteKeObxUrJMgH8LZvI,5084
|
|
16
|
-
rclone_api/rclone.py,sha256=
|
|
16
|
+
rclone_api/rclone.py,sha256=0UW3gQ93DgZjvNspxyoi_ae_PfbNq32tHb3wVpa6Eq4,48893
|
|
17
17
|
rclone_api/remote.py,sha256=O9WDUFQy9f6oT1HdUbTixK2eg0xtBBm8k4Xl6aa6K00,431
|
|
18
18
|
rclone_api/rpath.py,sha256=8ZA_1wxWtskwcy0I8V2VbjKDmzPkiWd8Q2JQSvh-sYE,2586
|
|
19
19
|
rclone_api/scan_missing_folders.py,sha256=Kulca2Q6WZodt00ATFHkmqqInuoPvBkhTcS9703y6po,4740
|
|
@@ -25,17 +25,17 @@ rclone_api/cmd/copy_large_s3.py,sha256=a-NtpIhy3gj1umIyS0JG1qQMCbsLQoi6VP6u2NjBG
|
|
|
25
25
|
rclone_api/cmd/list_files.py,sha256=x8FHODEilwKqwdiU1jdkeJbLwOqUkUQuDWPo2u_zpf0,741
|
|
26
26
|
rclone_api/experimental/flags.py,sha256=qCVD--fSTmzlk9hloRLr0q9elzAOFzPsvVpKM3aB1Mk,2739
|
|
27
27
|
rclone_api/experimental/flags_base.py,sha256=ajU_czkTcAxXYU-SlmiCfHY7aCQGHvpCLqJ-Z8uZLk0,2102
|
|
28
|
-
rclone_api/profile/mount_copy_bytes.py,sha256
|
|
29
|
-
rclone_api/s3/api.py,sha256=
|
|
28
|
+
rclone_api/profile/mount_copy_bytes.py,sha256=_bd9oergTuCdj1P6AVh_pC6GmtCpLFQYwiTdhwXeYZE,8404
|
|
29
|
+
rclone_api/s3/api.py,sha256=Plhtw_R9wrhN1fNzMI14ZFkHlvKdehUpdJ07P5cw9HI,3842
|
|
30
30
|
rclone_api/s3/basic_ops.py,sha256=hK3366xhVEzEcjz9Gk_8lFx6MRceAk72cax6mUrr6ko,2104
|
|
31
|
-
rclone_api/s3/chunk_file.py,sha256=
|
|
31
|
+
rclone_api/s3/chunk_file.py,sha256=Gro77c-mHQUvW41fw3aFtSrc6ByOcdGt0_padnip6yc,4568
|
|
32
32
|
rclone_api/s3/chunk_types.py,sha256=LbXayXY1KgVU1LkdbASD_BQ7TpVpwVnzMjtz--8LBaE,10316
|
|
33
33
|
rclone_api/s3/create.py,sha256=wgfkapv_j904CfKuWyiBIWJVxfAx_ftemFSUV14aT68,3149
|
|
34
|
-
rclone_api/s3/types.py,sha256=
|
|
35
|
-
rclone_api/s3/upload_file_multipart.py,sha256=
|
|
36
|
-
rclone_api-1.1.
|
|
37
|
-
rclone_api-1.1.
|
|
38
|
-
rclone_api-1.1.
|
|
39
|
-
rclone_api-1.1.
|
|
40
|
-
rclone_api-1.1.
|
|
41
|
-
rclone_api-1.1.
|
|
34
|
+
rclone_api/s3/types.py,sha256=MFZkVXB-rUHa-d3b5gAt5qKPP1tv3FrUmy7Wq4HQgjc,1521
|
|
35
|
+
rclone_api/s3/upload_file_multipart.py,sha256=frfz8CDW2x2DCPKW0fcZCaABq-vA8gcv26bO8mYNEf8,10550
|
|
36
|
+
rclone_api-1.1.96.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
|
37
|
+
rclone_api-1.1.96.dist-info/METADATA,sha256=CbTS1pfpa01wYwzt-47lFLHlEaz7R92EGIbE6YsXkc8,4537
|
|
38
|
+
rclone_api-1.1.96.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
|
|
39
|
+
rclone_api-1.1.96.dist-info/entry_points.txt,sha256=TV8kwP3FRzYwUEr0RLC7aJh0W03SAefIJNXTJ-FdMIQ,200
|
|
40
|
+
rclone_api-1.1.96.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
|
41
|
+
rclone_api-1.1.96.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|