rclone-api 1.4.19__tar.gz → 1.4.22__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.4.19 → rclone_api-1.4.22}/PKG-INFO +1 -1
- {rclone_api-1.4.19 → rclone_api-1.4.22}/pyproject.toml +1 -1
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/__init__.py +8 -3
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/cmd/copy_large_s3.py +0 -2
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/cmd/copy_large_s3_finish.py +2 -2
- rclone_api-1.4.22/src/rclone_api/detail/copy_file_parts_resumable.py +42 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/http_server.py +4 -1
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/rclone_impl.py +42 -15
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/api.py +2 -2
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/multipart/finished_piece.py +21 -3
- rclone_api-1.4.22/src/rclone_api/s3/multipart/info_json.py +239 -0
- rclone_api-1.4.19/src/rclone_api/detail/copy_file_parts.py → rclone_api-1.4.22/src/rclone_api/s3/multipart/upload_parts_resumable.py +38 -243
- rclone_api-1.4.19/src/rclone_api/s3/s3_multipart_uploader_by_copy.py → rclone_api-1.4.22/src/rclone_api/s3/multipart/upload_parts_server_side_merge.py +53 -25
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/types.py +1 -1
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api.egg-info/SOURCES.txt +6 -4
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_copy_file_resumable_s3.py +21 -13
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_s3.py +1 -1
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_size_files.py +5 -1
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.aiderignore +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.gitignore +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.pylintrc +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.vscode/launch.json +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.vscode/settings.json +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/.vscode/tasks.json +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/LICENSE +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/MANIFEST.in +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/README.md +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/clean +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/install +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/lint +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/requirements.testing.txt +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/setup.cfg +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/setup.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/cmd/analyze.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/cmd/save_to_db.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/config.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/db/__init__.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/db/db.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/db/models.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/detail/walk.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/experimental/flags.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/experimental/flags_base.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/file.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/file_item.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/file_part.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/file_stream.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/group_files.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/log.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/mount.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/process.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/basic_ops.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/chunk_task.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/create.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/multipart/file_info.py +0 -0
- {rclone_api-1.4.19/src/rclone_api/s3 → rclone_api-1.4.22/src/rclone_api/s3/multipart}/merge_state.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/multipart/upload_info.py +0 -0
- /rclone_api-1.4.19/src/rclone_api/s3/upload_file_multipart.py → /rclone_api-1.4.22/src/rclone_api/s3/multipart/upload_parts_inline.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/multipart/upload_state.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/s3/types.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/scan_missing_folders.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api/util.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/test +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/archive/test_paramiko.py.disabled +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_copy.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_copy_bytes.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_copy_files.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_db.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_diff.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_file_item.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_group_files.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_is_synced.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_ls.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_ls_stream_files.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_mount.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_obscure.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_rclone_config.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_read_write_text.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_remote_control.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_remotes.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_scan_missing_folders.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_serve_http.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_size_suffix.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tests/test_walk.py +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/tox.ini +0 -0
- {rclone_api-1.4.19 → rclone_api-1.4.22}/upload_package.sh +0 -0
@@ -428,11 +428,16 @@ class Rclone:
|
|
428
428
|
src: str, # src:/Bucket/path/myfile.large.zst
|
429
429
|
dst_dir: str, # dst:/Bucket/path/myfile.large.zst-parts/part.{part_number:05d}.start-end
|
430
430
|
part_infos: list[PartInfo] | None = None,
|
431
|
-
|
431
|
+
upload_threads: int = 8, # Number of reader and writer threads to use
|
432
|
+
merge_threads: int = 4, # Number of threads to use for merging the parts
|
432
433
|
) -> Exception | None:
|
433
434
|
"""Copy a file in parts."""
|
434
435
|
return self.impl.copy_file_parts(
|
435
|
-
src=src,
|
436
|
+
src=src,
|
437
|
+
dst_dir=dst_dir,
|
438
|
+
part_infos=part_infos,
|
439
|
+
upload_threads=upload_threads,
|
440
|
+
merge_threads=merge_threads,
|
436
441
|
)
|
437
442
|
|
438
443
|
def mount(
|
@@ -496,7 +501,7 @@ class Rclone:
|
|
496
501
|
other_args: list[str] | None = None,
|
497
502
|
check: bool | None = False,
|
498
503
|
verbose: bool | None = None,
|
499
|
-
) -> SizeResult:
|
504
|
+
) -> SizeResult | Exception:
|
500
505
|
"""Get the size of a list of files. Example of files items: "remote:bucket/to/file"."""
|
501
506
|
return self.impl.size_files(
|
502
507
|
src=src,
|
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|
3
3
|
from pathlib import Path
|
4
4
|
|
5
5
|
from rclone_api import Rclone
|
6
|
-
from rclone_api.s3.
|
6
|
+
from rclone_api.s3.multipart.upload_parts_server_side_merge import (
|
7
7
|
s3_server_side_multi_part_merge,
|
8
8
|
)
|
9
9
|
|
@@ -57,7 +57,7 @@ def main() -> int:
|
|
57
57
|
rclone = Rclone(rclone_conf=args.config_path)
|
58
58
|
info_path = _get_info_path(src=args.src)
|
59
59
|
s3_server_side_multi_part_merge(
|
60
|
-
rclone=rclone.impl, info_path=info_path, max_workers=
|
60
|
+
rclone=rclone.impl, info_path=info_path, max_workers=5
|
61
61
|
)
|
62
62
|
return 0
|
63
63
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
from rclone_api.rclone_impl import RcloneImpl
|
2
|
+
from rclone_api.types import (
|
3
|
+
PartInfo,
|
4
|
+
)
|
5
|
+
|
6
|
+
|
7
|
+
def copy_file_parts_resumable(
|
8
|
+
self: RcloneImpl,
|
9
|
+
src: str, # src:/Bucket/path/myfile.large.zst
|
10
|
+
dst_dir: str, # dst:/Bucket/path/myfile.large.zst-parts/
|
11
|
+
part_infos: list[PartInfo] | None = None,
|
12
|
+
upload_threads: int = 10,
|
13
|
+
merge_threads: int = 5,
|
14
|
+
verbose: bool | None = None,
|
15
|
+
) -> Exception | None:
|
16
|
+
# _upload_parts
|
17
|
+
from rclone_api.s3.multipart.upload_parts_resumable import upload_parts_resumable
|
18
|
+
from rclone_api.s3.multipart.upload_parts_server_side_merge import (
|
19
|
+
s3_server_side_multi_part_merge,
|
20
|
+
)
|
21
|
+
|
22
|
+
if verbose is None:
|
23
|
+
verbose = self.get_verbose()
|
24
|
+
|
25
|
+
err: Exception | None = upload_parts_resumable(
|
26
|
+
self=self,
|
27
|
+
src=src,
|
28
|
+
dst_dir=dst_dir,
|
29
|
+
part_infos=part_infos,
|
30
|
+
threads=upload_threads,
|
31
|
+
)
|
32
|
+
if isinstance(err, Exception):
|
33
|
+
return err
|
34
|
+
if dst_dir.endswith("/"):
|
35
|
+
dst_dir = dst_dir[:-1]
|
36
|
+
dst_info = f"{dst_dir}/info.json"
|
37
|
+
err = s3_server_side_multi_part_merge(
|
38
|
+
rclone=self, info_path=dst_info, max_workers=merge_threads, verbose=verbose
|
39
|
+
)
|
40
|
+
if isinstance(err, Exception):
|
41
|
+
return err
|
42
|
+
return None
|
@@ -86,7 +86,10 @@ class HttpServer:
|
|
86
86
|
assert response.is_closed
|
87
87
|
# print(f"Downloaded bytes {start}-{end} to {dst}")
|
88
88
|
if range:
|
89
|
-
|
89
|
+
length = range.end - range.start
|
90
|
+
print(
|
91
|
+
f"Downloaded bytes starting at {range.start} with size {length} to {dst}"
|
92
|
+
)
|
90
93
|
else:
|
91
94
|
size = dst.stat().st_size
|
92
95
|
print(f"Downloaded {size} bytes to {dst}")
|
@@ -455,6 +455,9 @@ class RcloneImpl:
|
|
455
455
|
out = self._run(cmd)
|
456
456
|
return CompletedProcess.from_subprocess(out)
|
457
457
|
|
458
|
+
def get_verbose(self) -> bool:
|
459
|
+
return get_verbose(None)
|
460
|
+
|
458
461
|
def copy_to(
|
459
462
|
self,
|
460
463
|
src: File | str,
|
@@ -789,17 +792,21 @@ class RcloneImpl:
|
|
789
792
|
src: str, # src:/Bucket/path/myfile.large.zst
|
790
793
|
dst_dir: str, # dst:/Bucket/path/myfile.large.zst-parts/
|
791
794
|
part_infos: list[PartInfo] | None = None,
|
792
|
-
|
795
|
+
upload_threads: int = 8,
|
796
|
+
merge_threads: int = 4,
|
793
797
|
) -> Exception | None:
|
794
798
|
"""Copy parts of a file from source to destination."""
|
795
|
-
from rclone_api.detail.
|
799
|
+
from rclone_api.detail.copy_file_parts_resumable import (
|
800
|
+
copy_file_parts_resumable,
|
801
|
+
)
|
796
802
|
|
797
|
-
out =
|
803
|
+
out = copy_file_parts_resumable(
|
798
804
|
self=self,
|
799
805
|
src=src,
|
800
806
|
dst_dir=dst_dir,
|
801
807
|
part_infos=part_infos,
|
802
|
-
|
808
|
+
upload_threads=upload_threads,
|
809
|
+
merge_threads=merge_threads,
|
803
810
|
)
|
804
811
|
return out
|
805
812
|
|
@@ -853,15 +860,25 @@ class RcloneImpl:
|
|
853
860
|
|
854
861
|
def size_file(self, src: str) -> SizeSuffix | Exception:
|
855
862
|
"""Get the size of a file or directory."""
|
856
|
-
src_parent = os.path.dirname(src)
|
857
|
-
src_name = os.path.basename(src)
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
863
|
+
# src_parent = os.path.dirname(src)
|
864
|
+
# src_name = os.path.basename(src)
|
865
|
+
# can't use this because it's only one file.
|
866
|
+
# out: SizeResult = self.size_files(src_parent, [src_name])
|
867
|
+
# one_file = len(out.file_sizes) == 1
|
868
|
+
# if not one_file:
|
869
|
+
# return Exception(
|
870
|
+
# f"More than one result returned, is this is a directory? {out}"
|
871
|
+
# )
|
872
|
+
# return SizeSuffix(out.total_size)
|
873
|
+
dirlist: DirListing = self.ls(
|
874
|
+
src, listing_option=ListingOption.FILES_ONLY, max_depth=0
|
875
|
+
)
|
876
|
+
if len(dirlist.files) == 0:
|
877
|
+
return FileNotFoundError(f"File not found: {src}")
|
878
|
+
if len(dirlist.files) > 1:
|
879
|
+
return Exception(f"More than one file found: {src}")
|
880
|
+
file: File = dirlist.files[0]
|
881
|
+
return SizeSuffix(file.size)
|
865
882
|
|
866
883
|
def get_s3_credentials(
|
867
884
|
self, remote: str, verbose: bool | None = None
|
@@ -943,7 +960,9 @@ class RcloneImpl:
|
|
943
960
|
name = src_path.name
|
944
961
|
src_parent_path = Path(src).parent.as_posix()
|
945
962
|
|
946
|
-
size_result: SizeResult = self.size_files(src_parent_path, [name])
|
963
|
+
size_result: SizeResult | Exception = self.size_files(src_parent_path, [name])
|
964
|
+
if isinstance(size_result, Exception):
|
965
|
+
raise size_result
|
947
966
|
target_size = SizeSuffix(size_result.total_size)
|
948
967
|
|
949
968
|
chunk_size = chunk_size or SizeSuffix("64M")
|
@@ -1286,10 +1305,18 @@ class RcloneImpl:
|
|
1286
1305
|
other_args: list[str] | None = None,
|
1287
1306
|
check: bool | None = False,
|
1288
1307
|
verbose: bool | None = None,
|
1289
|
-
) -> SizeResult:
|
1308
|
+
) -> SizeResult | Exception:
|
1290
1309
|
"""Get the size of a list of files. Example of files items: "remote:bucket/to/file"."""
|
1291
1310
|
verbose = get_verbose(verbose)
|
1292
1311
|
check = get_check(check)
|
1312
|
+
if len(files) < 2:
|
1313
|
+
tmp = self.size_file(files[0])
|
1314
|
+
if isinstance(tmp, Exception):
|
1315
|
+
return tmp
|
1316
|
+
assert isinstance(tmp, SizeSuffix)
|
1317
|
+
return SizeResult(
|
1318
|
+
prefix=src, total_size=tmp.as_int(), file_sizes={files[0]: tmp.as_int()}
|
1319
|
+
)
|
1293
1320
|
if fast_list or (other_args and "--fast-list" in other_args):
|
1294
1321
|
warnings.warn(
|
1295
1322
|
"It's not recommended to use --fast-list with size_files as this will perform poorly on large repositories since the entire repository has to be scanned."
|
@@ -10,11 +10,11 @@ from rclone_api.s3.basic_ops import (
|
|
10
10
|
upload_file,
|
11
11
|
)
|
12
12
|
from rclone_api.s3.create import S3Config, create_s3_client
|
13
|
-
from rclone_api.s3.
|
14
|
-
from rclone_api.s3.upload_file_multipart import (
|
13
|
+
from rclone_api.s3.multipart.upload_parts_inline import (
|
15
14
|
MultiUploadResult,
|
16
15
|
upload_file_multipart,
|
17
16
|
)
|
17
|
+
from rclone_api.s3.types import S3Credentials, S3MutliPartUploadConfig, S3UploadTarget
|
18
18
|
|
19
19
|
_MIN_THRESHOLD_FOR_CHUNKING = 5 * 1024 * 1024
|
20
20
|
|
@@ -10,7 +10,17 @@ class FinishedPiece:
|
|
10
10
|
etag: str
|
11
11
|
|
12
12
|
def to_json(self) -> dict:
|
13
|
-
return {"part_number": self.part_number, "etag": self.etag}
|
13
|
+
# return {"part_number": self.part_number, "etag": self.etag}
|
14
|
+
# amazon s3 style dict
|
15
|
+
tag = self.etag
|
16
|
+
if not tag.startswith('"'):
|
17
|
+
tag = f'"{tag}"'
|
18
|
+
out = {"PartNumber": self.part_number, "ETag": self.etag}
|
19
|
+
return out
|
20
|
+
|
21
|
+
def __post_init__(self):
|
22
|
+
assert isinstance(self.part_number, int)
|
23
|
+
assert isinstance(self.etag, str)
|
14
24
|
|
15
25
|
@staticmethod
|
16
26
|
def to_json_array(
|
@@ -30,13 +40,21 @@ class FinishedPiece:
|
|
30
40
|
# assert count_eos <= 1, "Only one EndOfStream should be present"
|
31
41
|
if count_eos > 1:
|
32
42
|
warnings.warn(f"Only one EndOfStream should be present, found {count_eos}")
|
33
|
-
|
43
|
+
out = [p.to_json() for p in non_none]
|
44
|
+
return out
|
34
45
|
|
35
46
|
@staticmethod
|
36
47
|
def from_json(json: dict | None) -> "FinishedPiece | EndOfStream":
|
37
48
|
if json is None:
|
38
49
|
return EndOfStream()
|
39
|
-
|
50
|
+
part_number = json.get("PartNumber")
|
51
|
+
etag = json.get("ETag")
|
52
|
+
assert isinstance(etag, str)
|
53
|
+
# handle the double quotes around the etag
|
54
|
+
etag = etag.replace('"', "")
|
55
|
+
assert isinstance(part_number, int)
|
56
|
+
assert isinstance(etag, str)
|
57
|
+
return FinishedPiece(part_number=part_number, etag=etag)
|
40
58
|
|
41
59
|
@staticmethod
|
42
60
|
def from_json_array(json: dict) -> list["FinishedPiece"]:
|
@@ -0,0 +1,239 @@
|
|
1
|
+
import hashlib
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import warnings
|
5
|
+
from datetime import datetime
|
6
|
+
|
7
|
+
from rclone_api.dir_listing import DirListing
|
8
|
+
from rclone_api.rclone_impl import RcloneImpl
|
9
|
+
from rclone_api.types import (
|
10
|
+
PartInfo,
|
11
|
+
SizeSuffix,
|
12
|
+
)
|
13
|
+
|
14
|
+
|
15
|
+
def _fetch_all_names(
|
16
|
+
self: RcloneImpl,
|
17
|
+
src: str,
|
18
|
+
) -> list[str]:
|
19
|
+
dl: DirListing = self.ls(src)
|
20
|
+
files = dl.files
|
21
|
+
filenames: list[str] = [f.name for f in files]
|
22
|
+
filtered: list[str] = [f for f in filenames if f.startswith("part.")]
|
23
|
+
return filtered
|
24
|
+
|
25
|
+
|
26
|
+
def _get_info_json(self: RcloneImpl, src: str | None, src_info: str) -> dict:
|
27
|
+
from rclone_api.file import File
|
28
|
+
|
29
|
+
data: dict
|
30
|
+
text: str
|
31
|
+
if src is None:
|
32
|
+
# just try to load the file
|
33
|
+
text_or_err = self.read_text(src_info)
|
34
|
+
if isinstance(text_or_err, Exception):
|
35
|
+
raise FileNotFoundError(f"Could not load {src_info}: {text_or_err}")
|
36
|
+
assert isinstance(text_or_err, str)
|
37
|
+
text = text_or_err
|
38
|
+
data = json.loads(text)
|
39
|
+
return data
|
40
|
+
|
41
|
+
src_stat: File | Exception = self.stat(src)
|
42
|
+
if isinstance(src_stat, Exception):
|
43
|
+
# just try to load the file
|
44
|
+
raise FileNotFoundError(f"Failed to stat {src}: {src_stat}")
|
45
|
+
|
46
|
+
now: datetime = datetime.now()
|
47
|
+
new_data = {
|
48
|
+
"new": True,
|
49
|
+
"created": now.isoformat(),
|
50
|
+
"src": src,
|
51
|
+
"src_modtime": src_stat.mod_time(),
|
52
|
+
"size": src_stat.size,
|
53
|
+
"chunksize": None,
|
54
|
+
"chunksize_int": None,
|
55
|
+
"first_part": None,
|
56
|
+
"last_part": None,
|
57
|
+
"hash": None,
|
58
|
+
}
|
59
|
+
|
60
|
+
text_or_err = self.read_text(src_info)
|
61
|
+
err: Exception | None = text_or_err if isinstance(text_or_err, Exception) else None
|
62
|
+
if isinstance(text_or_err, Exception):
|
63
|
+
warnings.warn(f"Failed to read {src_info}: {text_or_err}")
|
64
|
+
return new_data
|
65
|
+
assert isinstance(text_or_err, str)
|
66
|
+
text = text_or_err
|
67
|
+
|
68
|
+
if err is not None:
|
69
|
+
return new_data
|
70
|
+
|
71
|
+
try:
|
72
|
+
data = json.loads(text)
|
73
|
+
return data
|
74
|
+
except Exception as e:
|
75
|
+
warnings.warn(f"Failed to parse JSON: {e} at {src_info}")
|
76
|
+
return new_data
|
77
|
+
|
78
|
+
|
79
|
+
def _save_info_json(self: RcloneImpl, src: str, data: dict) -> None:
|
80
|
+
data = data.copy()
|
81
|
+
data["new"] = False
|
82
|
+
# hash
|
83
|
+
|
84
|
+
h = hashlib.md5()
|
85
|
+
tmp = [
|
86
|
+
data.get("src"),
|
87
|
+
data.get("src_modtime"),
|
88
|
+
data.get("size"),
|
89
|
+
data.get("chunksize_int"),
|
90
|
+
]
|
91
|
+
data_vals: list[str] = [str(v) for v in tmp]
|
92
|
+
str_data = "".join(data_vals)
|
93
|
+
h.update(str_data.encode("utf-8"))
|
94
|
+
data["hash"] = h.hexdigest()
|
95
|
+
json_str = json.dumps(data, indent=0)
|
96
|
+
self.write_text(dst=src, text=json_str)
|
97
|
+
|
98
|
+
|
99
|
+
class InfoJson:
|
100
|
+
def __init__(self, rclone: RcloneImpl, src: str | None, src_info: str) -> None:
|
101
|
+
self.rclone = rclone
|
102
|
+
self.src = src
|
103
|
+
self.src_info = src_info
|
104
|
+
self.data: dict = {}
|
105
|
+
|
106
|
+
def load(self) -> bool:
|
107
|
+
"""Returns true if the file exist and is now loaded."""
|
108
|
+
self.data = _get_info_json(self.rclone, self.src, self.src_info)
|
109
|
+
return not self.data.get("new", False)
|
110
|
+
|
111
|
+
def save(self) -> None:
|
112
|
+
_save_info_json(self.rclone, self.src_info, self.data)
|
113
|
+
|
114
|
+
def print(self) -> None:
|
115
|
+
self.rclone.print(self.src_info)
|
116
|
+
|
117
|
+
def fetch_all_finished(self) -> list[str]:
|
118
|
+
parent_path = os.path.dirname(self.src_info)
|
119
|
+
out = _fetch_all_names(self.rclone, parent_path)
|
120
|
+
return out
|
121
|
+
|
122
|
+
def fetch_all_finished_part_numbers(self) -> list[int]:
|
123
|
+
names = self.fetch_all_finished()
|
124
|
+
part_numbers = [int(name.split("_")[0].split(".")[1]) for name in names]
|
125
|
+
return part_numbers
|
126
|
+
|
127
|
+
@property
|
128
|
+
def parts_dir(self) -> str:
|
129
|
+
parts_dir = os.path.dirname(self.src_info)
|
130
|
+
if parts_dir.endswith("/"):
|
131
|
+
parts_dir = parts_dir[:-1]
|
132
|
+
return parts_dir
|
133
|
+
|
134
|
+
@property
|
135
|
+
def dst(self) -> str:
|
136
|
+
parts_dir = self.parts_dir
|
137
|
+
assert parts_dir.endswith("-parts")
|
138
|
+
out = parts_dir[:-6]
|
139
|
+
return out
|
140
|
+
|
141
|
+
@property
|
142
|
+
def dst_name(self) -> str:
|
143
|
+
return os.path.basename(self.dst)
|
144
|
+
|
145
|
+
def compute_all_parts(self) -> list[PartInfo] | Exception:
|
146
|
+
# full_part_infos: list[PartInfo] | Exception = PartInfo.split_parts(
|
147
|
+
# src_size, SizeSuffix("96MB")
|
148
|
+
try:
|
149
|
+
|
150
|
+
src_size = self.size
|
151
|
+
chunk_size = self.chunksize
|
152
|
+
assert isinstance(src_size, SizeSuffix)
|
153
|
+
assert isinstance(chunk_size, SizeSuffix)
|
154
|
+
first_part = self.data["first_part"]
|
155
|
+
last_part = self.data["last_part"]
|
156
|
+
full_part_infos: list[PartInfo] = PartInfo.split_parts(src_size, chunk_size)
|
157
|
+
return full_part_infos[first_part : last_part + 1]
|
158
|
+
except Exception as e:
|
159
|
+
return e
|
160
|
+
|
161
|
+
def compute_all_part_numbers(self) -> list[int] | Exception:
|
162
|
+
all_parts: list[PartInfo] | Exception = self.compute_all_parts()
|
163
|
+
if isinstance(all_parts, Exception):
|
164
|
+
raise all_parts
|
165
|
+
|
166
|
+
all_part_nums: list[int] = [p.part_number for p in all_parts]
|
167
|
+
return all_part_nums
|
168
|
+
|
169
|
+
def fetch_remaining_part_numbers(self) -> list[int] | Exception:
|
170
|
+
all_part_nums: list[int] | Exception = self.compute_all_part_numbers()
|
171
|
+
if isinstance(all_part_nums, Exception):
|
172
|
+
return all_part_nums
|
173
|
+
finished_part_nums: list[int] = self.fetch_all_finished_part_numbers()
|
174
|
+
remaining_part_nums: list[int] = list(
|
175
|
+
set(all_part_nums) - set(finished_part_nums)
|
176
|
+
)
|
177
|
+
return sorted(remaining_part_nums)
|
178
|
+
|
179
|
+
def fetch_is_done(self) -> bool:
|
180
|
+
remaining_part_nums: list[int] | Exception = self.fetch_remaining_part_numbers()
|
181
|
+
if isinstance(remaining_part_nums, Exception):
|
182
|
+
return False
|
183
|
+
return len(remaining_part_nums) == 0
|
184
|
+
|
185
|
+
@property
|
186
|
+
def new(self) -> bool:
|
187
|
+
return self.data.get("new", False)
|
188
|
+
|
189
|
+
@property
|
190
|
+
def chunksize(self) -> SizeSuffix | None:
|
191
|
+
chunksize_int: int | None = self.data.get("chunksize_int")
|
192
|
+
if chunksize_int is None:
|
193
|
+
return None
|
194
|
+
return SizeSuffix(chunksize_int)
|
195
|
+
|
196
|
+
@chunksize.setter
|
197
|
+
def chunksize(self, value: SizeSuffix) -> None:
|
198
|
+
self.data["chunksize"] = str(value)
|
199
|
+
self.data["chunksize_int"] = value.as_int()
|
200
|
+
|
201
|
+
@property
|
202
|
+
def src_modtime(self) -> datetime:
|
203
|
+
return datetime.fromisoformat(self.data["src_modtime"])
|
204
|
+
|
205
|
+
@src_modtime.setter
|
206
|
+
def src_modtime(self, value: datetime) -> None:
|
207
|
+
self.data["src_modtime"] = value.isoformat()
|
208
|
+
|
209
|
+
@property
|
210
|
+
def size(self) -> SizeSuffix:
|
211
|
+
return SizeSuffix(self.data["size"])
|
212
|
+
|
213
|
+
def _get_first_part(self) -> int | None:
|
214
|
+
return self.data.get("first_part")
|
215
|
+
|
216
|
+
def _set_first_part(self, value: int) -> None:
|
217
|
+
self.data["first_part"] = value
|
218
|
+
|
219
|
+
def _get_last_part(self) -> int | None:
|
220
|
+
return self.data.get("last_part")
|
221
|
+
|
222
|
+
def _set_last_part(self, value: int) -> None:
|
223
|
+
self.data["last_part"] = value
|
224
|
+
|
225
|
+
first_part: int | None = property(_get_first_part, _set_first_part) # type: ignore
|
226
|
+
last_part: int | None = property(_get_last_part, _set_last_part) # type: ignore
|
227
|
+
|
228
|
+
@property
|
229
|
+
def hash(self) -> str | None:
|
230
|
+
return self.data.get("hash")
|
231
|
+
|
232
|
+
def to_json_str(self) -> str:
|
233
|
+
return json.dumps(self.data)
|
234
|
+
|
235
|
+
def __repr__(self):
|
236
|
+
return f"InfoJson({self.src}, {self.src_info}, {self.data})"
|
237
|
+
|
238
|
+
def __str__(self):
|
239
|
+
return self.to_json_str()
|