rclone-api 1.3.0__tar.gz → 1.3.2__tar.gz
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-1.3.0 → rclone_api-1.3.2}/PKG-INFO +1 -1
- {rclone_api-1.3.0 → rclone_api-1.3.2}/pyproject.toml +1 -1
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/__init__.py +5 -3
- rclone_api-1.3.0/src/rclone_api/logging.py → rclone_api-1.3.2/src/rclone_api/log.py +5 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/profile/mount_copy_bytes.py +13 -5
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/rclone.py +15 -79
- rclone_api-1.3.2/src/rclone_api/s3/chunk_task.py +196 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/s3/chunk_types.py +6 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/s3/upload_file_multipart.py +1 -1
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api.egg-info/SOURCES.txt +2 -2
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_copy_bytes.py +2 -2
- rclone_api-1.3.0/src/rclone_api/s3/chunk_file.py +0 -146
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.aiderignore +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.gitignore +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.pylintrc +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.vscode/launch.json +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.vscode/settings.json +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/.vscode/tasks.json +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/LICENSE +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/MANIFEST.in +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/README.md +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/clean +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/install +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/lint +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/requirements.testing.txt +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/setup.cfg +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/setup.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/cmd/copy_large_s3.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/config.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/experimental/flags.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/experimental/flags_base.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/file.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/group_files.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/mount.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/mount_read_chunker.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/process.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/s3/api.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/s3/basic_ops.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/s3/create.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/s3/types.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/scan_missing_folders.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/types.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/util.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api/walk.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/test +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/archive/test_paramiko.py.disabled +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_copy.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_copy_file_resumable_s3.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_copy_files.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_diff.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_group_files.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_is_synced.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_ls.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_mount.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_obscure.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_rclone_config.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_remote_control.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_remotes.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_s3.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_scan_missing_folders.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_size_files.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_size_suffix.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tests/test_walk.py +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/tox.ini +0 -0
- {rclone_api-1.3.0 → rclone_api-1.3.2}/upload_package.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Import logging module to activate default configuration
|
|
2
2
|
|
|
3
|
-
from rclone_api import
|
|
3
|
+
from rclone_api import log
|
|
4
4
|
|
|
5
5
|
from .completed_process import CompletedProcess
|
|
6
6
|
from .config import Config, Parsed, Section
|
|
@@ -11,7 +11,7 @@ from .file import File
|
|
|
11
11
|
from .filelist import FileList
|
|
12
12
|
|
|
13
13
|
# Import the configure_logging function to make it available at package level
|
|
14
|
-
from .
|
|
14
|
+
from .log import configure_logging, setup_default_logging
|
|
15
15
|
from .process import Process
|
|
16
16
|
from .rclone import Rclone, rclone_verbose
|
|
17
17
|
from .remote import Remote
|
|
@@ -43,5 +43,7 @@ __all__ = [
|
|
|
43
43
|
"MultiUploadResult",
|
|
44
44
|
"SizeSuffix",
|
|
45
45
|
"configure_logging",
|
|
46
|
-
"
|
|
46
|
+
"log",
|
|
47
47
|
]
|
|
48
|
+
|
|
49
|
+
setup_default_logging()
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
|
+
_INITIALISED = False
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
def setup_default_logging():
|
|
6
8
|
"""Set up default logging configuration if none exists."""
|
|
9
|
+
global _INITIALISED
|
|
10
|
+
if _INITIALISED:
|
|
11
|
+
return
|
|
7
12
|
if not logging.root.handlers:
|
|
8
13
|
logging.basicConfig(
|
|
9
14
|
level=logging.INFO,
|
|
@@ -164,9 +164,17 @@ def _run_profile(
|
|
|
164
164
|
diff = (time.time() - start) / num
|
|
165
165
|
net_io_end = psutil.net_io_counters()
|
|
166
166
|
# self.assertEqual(len(bytes_or_err), size)
|
|
167
|
-
assert (
|
|
168
|
-
|
|
169
|
-
), f"Length: {SizeSuffix(bytes_count)} != {SizeSuffix(size* num)}"
|
|
167
|
+
# assert (
|
|
168
|
+
# bytes_count == SizeSuffix(size * num).as_int()
|
|
169
|
+
# ), f"Length: {SizeSuffix(bytes_count)} != {SizeSuffix(size* num)}"
|
|
170
|
+
|
|
171
|
+
if bytes_count != SizeSuffix(size * num).as_int():
|
|
172
|
+
print("\n######################## ERROR ########################")
|
|
173
|
+
print(f"Error: Length: {SizeSuffix(bytes_count)} != {SizeSuffix(size* num)}")
|
|
174
|
+
print(f" Bytes count: {bytes_count}")
|
|
175
|
+
print(f" Size: {SizeSuffix(size * num)}")
|
|
176
|
+
print(f" num: {num}")
|
|
177
|
+
print("########################################################\n")
|
|
170
178
|
|
|
171
179
|
# print io stats
|
|
172
180
|
bytes_sent = (net_io_end.bytes_sent - net_io_start.bytes_sent) // num
|
|
@@ -203,11 +211,11 @@ def test_profile_copy_bytes(
|
|
|
203
211
|
sizes = [size.as_int()]
|
|
204
212
|
else:
|
|
205
213
|
sizes = [
|
|
206
|
-
|
|
214
|
+
1024 * 1024 * 1,
|
|
207
215
|
# 1024 * 1024 * 2,
|
|
208
216
|
# 1024 * 1024 * 4,
|
|
209
217
|
# 1024 * 1024 * 8,
|
|
210
|
-
1024 * 1024 * 16,
|
|
218
|
+
# 1024 * 1024 * 16,
|
|
211
219
|
# 1024 * 1024 * 32,
|
|
212
220
|
1024 * 1024 * 64,
|
|
213
221
|
1024 * 1024 * 128,
|
|
@@ -682,7 +682,7 @@ class Rclone:
|
|
|
682
682
|
save_state_json: Path,
|
|
683
683
|
chunk_size: SizeSuffix | None = None,
|
|
684
684
|
read_threads: int = 8,
|
|
685
|
-
write_threads: int =
|
|
685
|
+
write_threads: int = 8,
|
|
686
686
|
retries: int = 3,
|
|
687
687
|
verbose: bool | None = None,
|
|
688
688
|
max_chunks_before_suspension: int | None = None,
|
|
@@ -829,74 +829,6 @@ class Rclone:
|
|
|
829
829
|
finally:
|
|
830
830
|
chunk_fetcher.shutdown()
|
|
831
831
|
|
|
832
|
-
def _copy_bytes(
|
|
833
|
-
self,
|
|
834
|
-
src: str,
|
|
835
|
-
offset: int,
|
|
836
|
-
length: int,
|
|
837
|
-
transfers: int = 1, # Note, increasing transfers can result in devestating drop in performance.
|
|
838
|
-
# If outfile is supplied then bytes are written to this file and success returns bytes(0)
|
|
839
|
-
outfile: Path | None = None,
|
|
840
|
-
mount_log: Path | None = None,
|
|
841
|
-
direct_io: bool = True,
|
|
842
|
-
) -> bytes | Exception:
|
|
843
|
-
"""Copy bytes from a file to another file."""
|
|
844
|
-
from rclone_api.util import random_str
|
|
845
|
-
|
|
846
|
-
tmp_mnts = Path("tmp_mnts") / random_str(12)
|
|
847
|
-
src_parent_path = Path(src).parent.as_posix()
|
|
848
|
-
src_file = Path(src).name
|
|
849
|
-
other_args: list[str] = [
|
|
850
|
-
"--no-modtime",
|
|
851
|
-
# "--vfs-read-wait", "1s"
|
|
852
|
-
]
|
|
853
|
-
vfs_read_chunk_size = SizeSuffix(length // transfers)
|
|
854
|
-
vfs_read_chunk_size_limit = SizeSuffix(length)
|
|
855
|
-
vfs_read_chunk_streams = transfers
|
|
856
|
-
vfs_disk_space_total_size = SizeSuffix(length)
|
|
857
|
-
# --max-read-ahead SizeSuffix
|
|
858
|
-
max_read_ahead = SizeSuffix(vfs_read_chunk_size.as_int())
|
|
859
|
-
|
|
860
|
-
# other_args += ["--vfs-read-chunk-size", str(vfs_read_chunk_size)]
|
|
861
|
-
other_args += ["--vfs-read-chunk-size", str(vfs_read_chunk_size)]
|
|
862
|
-
other_args += ["--vfs-read-chunk-size-limit", str(vfs_read_chunk_size_limit)]
|
|
863
|
-
other_args += ["--vfs-read-chunk-streams", str(vfs_read_chunk_streams)]
|
|
864
|
-
other_args += ["--vfs-disk-space-total-size", str(vfs_disk_space_total_size)]
|
|
865
|
-
other_args += ["--max-read-ahead", str(max_read_ahead)]
|
|
866
|
-
other_args += ["--read-only"]
|
|
867
|
-
if direct_io:
|
|
868
|
-
other_args += ["--direct-io"]
|
|
869
|
-
|
|
870
|
-
with TemporaryDirectory() as tmpdir:
|
|
871
|
-
cache_dir = Path(tmpdir) / "cache"
|
|
872
|
-
other_args += ["--cache-dir", str(cache_dir.absolute())]
|
|
873
|
-
try:
|
|
874
|
-
# use scoped mount to do the read, then write the bytes to the destination
|
|
875
|
-
with self.scoped_mount(
|
|
876
|
-
src_parent_path,
|
|
877
|
-
tmp_mnts,
|
|
878
|
-
use_links=True,
|
|
879
|
-
verbose=mount_log is not None,
|
|
880
|
-
vfs_cache_mode="minimal",
|
|
881
|
-
other_args=other_args,
|
|
882
|
-
log=mount_log,
|
|
883
|
-
cache_dir=cache_dir,
|
|
884
|
-
cache_dir_delete_on_exit=True,
|
|
885
|
-
):
|
|
886
|
-
src_file_mnt = tmp_mnts / src_file
|
|
887
|
-
with open(src_file_mnt, "rb") as f:
|
|
888
|
-
f.seek(offset)
|
|
889
|
-
data = f.read(length)
|
|
890
|
-
if outfile is None:
|
|
891
|
-
return data
|
|
892
|
-
with open(outfile, "wb") as out:
|
|
893
|
-
out.write(data)
|
|
894
|
-
del data
|
|
895
|
-
return bytes(0)
|
|
896
|
-
|
|
897
|
-
except Exception as e:
|
|
898
|
-
return e
|
|
899
|
-
|
|
900
832
|
def get_multi_mount_file_chunker(
|
|
901
833
|
self,
|
|
902
834
|
src: str,
|
|
@@ -928,16 +860,19 @@ class Rclone:
|
|
|
928
860
|
if direct_io:
|
|
929
861
|
other_args += ["--direct-io"]
|
|
930
862
|
|
|
863
|
+
base_mount_dir = Path("tmp_mnts")
|
|
864
|
+
base_cache_dir = Path("cache")
|
|
865
|
+
|
|
931
866
|
filename = Path(src).name
|
|
932
867
|
with ThreadPoolExecutor(max_workers=threads) as executor:
|
|
933
868
|
futures: list[Future] = []
|
|
934
869
|
try:
|
|
935
870
|
for i in range(threads):
|
|
936
|
-
tmp_mnts =
|
|
871
|
+
tmp_mnts = base_mount_dir / random_str(12)
|
|
937
872
|
verbose = mount_log is not None
|
|
938
873
|
|
|
939
874
|
src_parent_path = Path(src).parent.as_posix()
|
|
940
|
-
cache_dir =
|
|
875
|
+
cache_dir = base_cache_dir / random_str(12)
|
|
941
876
|
|
|
942
877
|
def task(
|
|
943
878
|
src_parent_path=src_parent_path,
|
|
@@ -992,7 +927,7 @@ class Rclone:
|
|
|
992
927
|
)
|
|
993
928
|
return filechunker
|
|
994
929
|
|
|
995
|
-
def
|
|
930
|
+
def copy_bytes_multimount(
|
|
996
931
|
self,
|
|
997
932
|
src: str,
|
|
998
933
|
offset: int,
|
|
@@ -1004,7 +939,7 @@ class Rclone:
|
|
|
1004
939
|
mount_log: Path | None = None,
|
|
1005
940
|
direct_io: bool = True,
|
|
1006
941
|
) -> bytes | Exception:
|
|
1007
|
-
"""Copy bytes from
|
|
942
|
+
"""Copy a slice of bytes from the src file to dst. Parallelism is achieved through multiple mounted files."""
|
|
1008
943
|
from rclone_api.types import FilePart
|
|
1009
944
|
|
|
1010
945
|
# determine number of threads from chunk size
|
|
@@ -1022,12 +957,13 @@ class Rclone:
|
|
|
1022
957
|
payload = fp.payload
|
|
1023
958
|
if isinstance(payload, Exception):
|
|
1024
959
|
return payload
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
960
|
+
try:
|
|
961
|
+
if outfile is None:
|
|
962
|
+
return payload.read_bytes()
|
|
963
|
+
shutil.move(payload, outfile)
|
|
964
|
+
return bytes(0)
|
|
965
|
+
finally:
|
|
966
|
+
fp.close()
|
|
1031
967
|
|
|
1032
968
|
except Exception as e:
|
|
1033
969
|
warnings.warn(f"Error copying bytes: {e}")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from concurrent.futures import Future
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from queue import Queue
|
|
6
|
+
from threading import Event, Lock
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from rclone_api.mount_read_chunker import FilePart
|
|
10
|
+
from rclone_api.s3.chunk_types import S3FileInfo, UploadState
|
|
11
|
+
from rclone_api.types import EndOfStream
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__) # noqa
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# def _get_file_size(file_path: Path, timeout: int = 60) -> int:
|
|
17
|
+
# sleep_time = timeout / 60 if timeout > 0 else 1
|
|
18
|
+
# start = time.time()
|
|
19
|
+
# while True:
|
|
20
|
+
# expired = time.time() - start > timeout
|
|
21
|
+
# try:
|
|
22
|
+
# time.sleep(sleep_time)
|
|
23
|
+
# if file_path.exists():
|
|
24
|
+
# return file_path.stat().st_size
|
|
25
|
+
# except FileNotFoundError as e:
|
|
26
|
+
# if expired:
|
|
27
|
+
# print(f"File not found: {file_path}, exception is {e}")
|
|
28
|
+
# raise
|
|
29
|
+
# if expired:
|
|
30
|
+
# raise TimeoutError(f"File {file_path} not found after {timeout} seconds")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _ShouldStopChecker:
|
|
34
|
+
def __init__(self, max_chunks: int | None) -> None:
|
|
35
|
+
self.count = 0
|
|
36
|
+
self.max_chunks = max_chunks
|
|
37
|
+
|
|
38
|
+
def should_stop(self) -> bool:
|
|
39
|
+
if self.max_chunks is None:
|
|
40
|
+
return False
|
|
41
|
+
if self.count >= self.max_chunks:
|
|
42
|
+
logger.info(
|
|
43
|
+
f"Stopping file chunker after {self.count} chunks because it exceeded max_chunks {self.max_chunks}"
|
|
44
|
+
)
|
|
45
|
+
return True
|
|
46
|
+
# self.count += 1
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def increment(self):
|
|
50
|
+
self.count += 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _PartNumberTracker:
|
|
54
|
+
def __init__(
|
|
55
|
+
self, start_part_value: int, last_part_value: int, done_parts: set[int]
|
|
56
|
+
) -> None:
|
|
57
|
+
# self._num_parts = (last_part_value - start_part_value) + 1
|
|
58
|
+
self._start_part_value = start_part_value
|
|
59
|
+
self._last_part_value = last_part_value
|
|
60
|
+
self._done_part_numbers: set[int] = done_parts
|
|
61
|
+
self._curr_part_number = start_part_value
|
|
62
|
+
self._finished = False
|
|
63
|
+
self._lock = Lock()
|
|
64
|
+
|
|
65
|
+
def next_part_number(self) -> int | None:
|
|
66
|
+
with self._lock:
|
|
67
|
+
while self._curr_part_number in self._done_part_numbers:
|
|
68
|
+
self._curr_part_number += 1
|
|
69
|
+
if self._curr_part_number > self._last_part_value:
|
|
70
|
+
self._finished = True
|
|
71
|
+
return None
|
|
72
|
+
curr_part_number = self._curr_part_number
|
|
73
|
+
self._curr_part_number += (
|
|
74
|
+
1 # prevent a second thread from getting the same part number
|
|
75
|
+
)
|
|
76
|
+
return curr_part_number
|
|
77
|
+
|
|
78
|
+
def is_finished(self) -> bool:
|
|
79
|
+
with self._lock:
|
|
80
|
+
return self._finished
|
|
81
|
+
|
|
82
|
+
def add_finished_part_number(self, part_number: int) -> None:
|
|
83
|
+
with self._lock:
|
|
84
|
+
self._done_part_numbers.add(part_number)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _OnCompleteHandler:
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
part_number_tracker: _PartNumberTracker,
|
|
91
|
+
file_path: Path,
|
|
92
|
+
queue_upload: Queue[FilePart | EndOfStream],
|
|
93
|
+
) -> None:
|
|
94
|
+
self.part_number_tracker = part_number_tracker
|
|
95
|
+
self.file_path = file_path
|
|
96
|
+
self.queue_upload = queue_upload
|
|
97
|
+
|
|
98
|
+
def on_complete(self, fut: Future[FilePart]) -> None:
|
|
99
|
+
logger.debug("Chunk read complete")
|
|
100
|
+
fp: FilePart = fut.result()
|
|
101
|
+
extra: S3FileInfo = fp.extra
|
|
102
|
+
assert isinstance(extra, S3FileInfo)
|
|
103
|
+
part_number = extra.part_number
|
|
104
|
+
if fp.is_error():
|
|
105
|
+
logger.warning(f"Error reading file: {fp}, skipping part {part_number}")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
if fp.n_bytes() == 0:
|
|
109
|
+
logger.warning(f"Empty data for part {part_number} of {self.file_path}")
|
|
110
|
+
raise ValueError(f"Empty data for part {part_number} of {self.file_path}")
|
|
111
|
+
|
|
112
|
+
if isinstance(fp.payload, Exception):
|
|
113
|
+
logger.warning(f"Error reading file because of error: {fp.payload}")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# done_part_numbers.add(part_number)
|
|
117
|
+
# queue_upload.put(fp)
|
|
118
|
+
self.part_number_tracker.add_finished_part_number(part_number)
|
|
119
|
+
self.queue_upload.put(fp)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def file_chunker(
|
|
123
|
+
upload_state: UploadState,
|
|
124
|
+
fetcher: Callable[[int, int, Any], Future[FilePart]],
|
|
125
|
+
max_chunks: int | None,
|
|
126
|
+
cancel_signal: Event,
|
|
127
|
+
queue_upload: Queue[FilePart | EndOfStream],
|
|
128
|
+
) -> None:
|
|
129
|
+
final_part_number = upload_state.upload_info.total_chunks() + 1
|
|
130
|
+
should_stop_checker = _ShouldStopChecker(max_chunks)
|
|
131
|
+
|
|
132
|
+
upload_info = upload_state.upload_info
|
|
133
|
+
file_path = upload_info.src_file_path
|
|
134
|
+
chunk_size = upload_info.chunk_size
|
|
135
|
+
|
|
136
|
+
done_part_numbers: set[int] = {
|
|
137
|
+
p.part_number for p in upload_state.parts if not isinstance(p, EndOfStream)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
part_tracker = _PartNumberTracker(
|
|
141
|
+
start_part_value=1,
|
|
142
|
+
last_part_value=final_part_number,
|
|
143
|
+
done_parts=done_part_numbers,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
callback = _OnCompleteHandler(part_tracker, file_path, queue_upload)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
num_parts = upload_info.total_chunks()
|
|
150
|
+
|
|
151
|
+
if cancel_signal.is_set():
|
|
152
|
+
logger.info(
|
|
153
|
+
f"Cancel signal is set for file chunker while processing {file_path}, returning"
|
|
154
|
+
)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
while not should_stop_checker.should_stop():
|
|
158
|
+
should_stop_checker.increment()
|
|
159
|
+
logger.debug("Processing next chunk")
|
|
160
|
+
curr_part_number = part_tracker.next_part_number()
|
|
161
|
+
if curr_part_number is None:
|
|
162
|
+
logger.info(f"File {file_path} has completed chunking all parts")
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
assert curr_part_number is not None
|
|
166
|
+
offset = (curr_part_number - 1) * chunk_size
|
|
167
|
+
file_size = upload_info.file_size
|
|
168
|
+
|
|
169
|
+
assert offset < file_size, f"Offset {offset} is greater than file size"
|
|
170
|
+
fetch_size = max(0, min(chunk_size, file_size - offset))
|
|
171
|
+
if fetch_size == 0:
|
|
172
|
+
logger.error(
|
|
173
|
+
f"Empty data for part {curr_part_number} of {file_path}, is this the last chunk?"
|
|
174
|
+
)
|
|
175
|
+
# assert final_part_number == curr_part_number, f"Final part number is {final_part_number} but current part number is {curr_part_number}"
|
|
176
|
+
if final_part_number != curr_part_number:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"This should have been the last part, but it is not: {final_part_number} != {curr_part_number}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
assert curr_part_number is not None
|
|
182
|
+
logger.info(
|
|
183
|
+
f"Reading chunk {curr_part_number} of {num_parts} for {file_path}"
|
|
184
|
+
)
|
|
185
|
+
fut = fetcher(
|
|
186
|
+
offset, fetch_size, S3FileInfo(upload_info.upload_id, curr_part_number)
|
|
187
|
+
)
|
|
188
|
+
fut.add_done_callback(callback.on_complete)
|
|
189
|
+
# wait until the queue_upload queue can accept the next chunk
|
|
190
|
+
while queue_upload.full():
|
|
191
|
+
time.sleep(0.1)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Error reading file: {e}", exc_info=True)
|
|
194
|
+
finally:
|
|
195
|
+
logger.info(f"Finishing FILE CHUNKER for {file_path} and adding EndOfStream")
|
|
196
|
+
queue_upload.put(EndOfStream())
|
|
@@ -11,7 +11,7 @@ from typing import Any, Callable
|
|
|
11
11
|
from botocore.client import BaseClient
|
|
12
12
|
|
|
13
13
|
from rclone_api.mount_read_chunker import FilePart
|
|
14
|
-
from rclone_api.s3.
|
|
14
|
+
from rclone_api.s3.chunk_task import S3FileInfo, file_chunker
|
|
15
15
|
from rclone_api.s3.chunk_types import (
|
|
16
16
|
FinishedPiece,
|
|
17
17
|
UploadInfo,
|
|
@@ -33,7 +33,7 @@ src/rclone_api/exec.py
|
|
|
33
33
|
src/rclone_api/file.py
|
|
34
34
|
src/rclone_api/filelist.py
|
|
35
35
|
src/rclone_api/group_files.py
|
|
36
|
-
src/rclone_api/
|
|
36
|
+
src/rclone_api/log.py
|
|
37
37
|
src/rclone_api/mount.py
|
|
38
38
|
src/rclone_api/mount_read_chunker.py
|
|
39
39
|
src/rclone_api/process.py
|
|
@@ -58,7 +58,7 @@ src/rclone_api/experimental/flags_base.py
|
|
|
58
58
|
src/rclone_api/profile/mount_copy_bytes.py
|
|
59
59
|
src/rclone_api/s3/api.py
|
|
60
60
|
src/rclone_api/s3/basic_ops.py
|
|
61
|
-
src/rclone_api/s3/
|
|
61
|
+
src/rclone_api/s3/chunk_task.py
|
|
62
62
|
src/rclone_api/s3/chunk_types.py
|
|
63
63
|
src/rclone_api/s3/create.py
|
|
64
64
|
src/rclone_api/s3/types.py
|
|
@@ -72,7 +72,7 @@ class RcloneCopyBytesTester(unittest.TestCase):
|
|
|
72
72
|
|
|
73
73
|
def test_copy_bytes(self) -> None:
|
|
74
74
|
rclone = Rclone(_generate_rclone_config())
|
|
75
|
-
bytes_or_err: bytes | Exception = rclone.
|
|
75
|
+
bytes_or_err: bytes | Exception = rclone.copy_bytes_multimount(
|
|
76
76
|
src="dst:rclone-api-unit-test/zachs_video/breaking_ai_mind.mp4",
|
|
77
77
|
offset=0,
|
|
78
78
|
length=1024 * 1024,
|
|
@@ -92,7 +92,7 @@ class RcloneCopyBytesTester(unittest.TestCase):
|
|
|
92
92
|
tmp = Path(tmpdir) / "tmp.mp4"
|
|
93
93
|
log = Path(tmpdir) / "log.txt"
|
|
94
94
|
rclone = Rclone(_generate_rclone_config())
|
|
95
|
-
bytes_or_err: bytes | Exception = rclone.
|
|
95
|
+
bytes_or_err: bytes | Exception = rclone.copy_bytes_multimount(
|
|
96
96
|
src="dst:rclone-api-unit-test/zachs_video/breaking_ai_mind.mp4",
|
|
97
97
|
offset=0,
|
|
98
98
|
length=1024 * 1024,
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import time
|
|
3
|
-
from concurrent.futures import Future
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from queue import Queue
|
|
7
|
-
from threading import Event
|
|
8
|
-
from typing import Any, Callable
|
|
9
|
-
|
|
10
|
-
from rclone_api.mount_read_chunker import FilePart
|
|
11
|
-
from rclone_api.s3.chunk_types import UploadState
|
|
12
|
-
from rclone_api.types import EndOfStream
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__) # noqa
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _get_file_size(file_path: Path, timeout: int = 60) -> int:
|
|
18
|
-
sleep_time = timeout / 60 if timeout > 0 else 1
|
|
19
|
-
start = time.time()
|
|
20
|
-
while True:
|
|
21
|
-
expired = time.time() - start > timeout
|
|
22
|
-
try:
|
|
23
|
-
time.sleep(sleep_time)
|
|
24
|
-
if file_path.exists():
|
|
25
|
-
return file_path.stat().st_size
|
|
26
|
-
except FileNotFoundError as e:
|
|
27
|
-
if expired:
|
|
28
|
-
print(f"File not found: {file_path}, exception is {e}")
|
|
29
|
-
raise
|
|
30
|
-
if expired:
|
|
31
|
-
raise TimeoutError(f"File {file_path} not found after {timeout} seconds")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@dataclass
|
|
35
|
-
class S3FileInfo:
|
|
36
|
-
upload_id: str
|
|
37
|
-
part_number: int
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def file_chunker(
|
|
41
|
-
upload_state: UploadState,
|
|
42
|
-
fetcher: Callable[[int, int, Any], Future[FilePart]],
|
|
43
|
-
max_chunks: int | None,
|
|
44
|
-
cancel_signal: Event,
|
|
45
|
-
queue_upload: Queue[FilePart | EndOfStream],
|
|
46
|
-
) -> None:
|
|
47
|
-
count = 0
|
|
48
|
-
|
|
49
|
-
def should_stop() -> bool:
|
|
50
|
-
nonlocal count
|
|
51
|
-
|
|
52
|
-
if max_chunks is None:
|
|
53
|
-
return False
|
|
54
|
-
if count >= max_chunks:
|
|
55
|
-
logger.info(
|
|
56
|
-
f"Stopping file chunker after {count} chunks because it exceeded max_chunks {max_chunks}"
|
|
57
|
-
)
|
|
58
|
-
return True
|
|
59
|
-
count += 1
|
|
60
|
-
return False
|
|
61
|
-
|
|
62
|
-
upload_info = upload_state.upload_info
|
|
63
|
-
file_path = upload_info.src_file_path
|
|
64
|
-
chunk_size = upload_info.chunk_size
|
|
65
|
-
# src = Path(file_path)
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
part_number = 1
|
|
69
|
-
done_part_numbers: set[int] = {
|
|
70
|
-
p.part_number for p in upload_state.parts if not isinstance(p, EndOfStream)
|
|
71
|
-
}
|
|
72
|
-
num_parts = upload_info.total_chunks()
|
|
73
|
-
|
|
74
|
-
def next_part_number() -> int | None:
|
|
75
|
-
nonlocal part_number
|
|
76
|
-
while part_number in done_part_numbers:
|
|
77
|
-
part_number += 1
|
|
78
|
-
if part_number > num_parts:
|
|
79
|
-
return None
|
|
80
|
-
return part_number
|
|
81
|
-
|
|
82
|
-
if cancel_signal.is_set():
|
|
83
|
-
logger.info(
|
|
84
|
-
f"Cancel signal is set for file chunker while processing {file_path}, returning"
|
|
85
|
-
)
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
while not should_stop():
|
|
89
|
-
logger.debug("Processing next chunk")
|
|
90
|
-
curr_part_number = next_part_number()
|
|
91
|
-
if curr_part_number is None:
|
|
92
|
-
logger.info(f"File {file_path} has completed chunking all parts")
|
|
93
|
-
break
|
|
94
|
-
assert curr_part_number is not None
|
|
95
|
-
offset = (curr_part_number - 1) * chunk_size
|
|
96
|
-
file_size = upload_info.file_size
|
|
97
|
-
|
|
98
|
-
assert offset < file_size, f"Offset {offset} is greater than file size"
|
|
99
|
-
|
|
100
|
-
# Open the file, seek, read the chunk, and close immediately.
|
|
101
|
-
# with open(file_path, "rb") as f:
|
|
102
|
-
# f.seek(offset)
|
|
103
|
-
# data = f.read(chunk_size)
|
|
104
|
-
|
|
105
|
-
# data = chunk_fetcher(offset, chunk_size).result()
|
|
106
|
-
|
|
107
|
-
assert curr_part_number is not None
|
|
108
|
-
cpn: int = curr_part_number
|
|
109
|
-
|
|
110
|
-
def on_complete(fut: Future[FilePart]) -> None:
|
|
111
|
-
logger.debug("Chunk read complete")
|
|
112
|
-
fp: FilePart = fut.result()
|
|
113
|
-
if fp.is_error():
|
|
114
|
-
logger.warning(
|
|
115
|
-
f"Error reading file: {fp}, skipping part {part_number}"
|
|
116
|
-
)
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
if fp.n_bytes() == 0:
|
|
120
|
-
logger.warning(f"Empty data for part {part_number} of {file_path}")
|
|
121
|
-
raise ValueError(
|
|
122
|
-
f"Empty data for part {part_number} of {file_path}"
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
if isinstance(fp.payload, Exception):
|
|
126
|
-
logger.warning(f"Error reading file because of error: {fp.payload}")
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
done_part_numbers.add(part_number)
|
|
130
|
-
queue_upload.put(fp)
|
|
131
|
-
|
|
132
|
-
offset = (curr_part_number - 1) * chunk_size
|
|
133
|
-
logger.info(
|
|
134
|
-
f"Reading chunk {curr_part_number} of {num_parts} for {file_path}"
|
|
135
|
-
)
|
|
136
|
-
fut = fetcher(offset, file_size, S3FileInfo(upload_info.upload_id, cpn))
|
|
137
|
-
fut.add_done_callback(on_complete)
|
|
138
|
-
# wait until the queue_upload queue can accept the next chunk
|
|
139
|
-
while queue_upload.full():
|
|
140
|
-
time.sleep(0.1)
|
|
141
|
-
except Exception as e:
|
|
142
|
-
|
|
143
|
-
logger.error(f"Error reading file: {e}", exc_info=True)
|
|
144
|
-
finally:
|
|
145
|
-
logger.info(f"Finishing FILE CHUNKER for {file_path} and adding EndOfStream")
|
|
146
|
-
queue_upload.put(EndOfStream())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|