rclone-api 1.4.15__tar.gz → 1.4.19__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.15 → rclone_api-1.4.19}/PKG-INFO +1 -1
- {rclone_api-1.4.15 → rclone_api-1.4.19}/pyproject.toml +1 -1
- rclone_api-1.4.19/src/rclone_api/cmd/copy_large_s3_finish.py +73 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/detail/copy_file_parts.py +4 -1
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/process.py +65 -40
- rclone_api-1.4.19/src/rclone_api/s3/merge_state.py +147 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/multipart/finished_piece.py +15 -5
- rclone_api-1.4.19/src/rclone_api/s3/s3_multipart_uploader_by_copy.py +518 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/util.py +84 -23
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api.egg-info/SOURCES.txt +1 -0
- rclone_api-1.4.15/src/rclone_api/cmd/copy_large_s3_finish.py +0 -170
- rclone_api-1.4.15/src/rclone_api/s3/s3_multipart_uploader_by_copy.py +0 -269
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.aiderignore +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.gitignore +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.pylintrc +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.vscode/launch.json +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.vscode/settings.json +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/.vscode/tasks.json +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/LICENSE +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/MANIFEST.in +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/README.md +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/clean +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/install +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/lint +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/requirements.testing.txt +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/setup.cfg +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/setup.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/__init__.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/cmd/analyze.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/cmd/copy_large_s3.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/cmd/save_to_db.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/config.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/db/__init__.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/db/db.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/db/models.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/detail/walk.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/experimental/flags.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/experimental/flags_base.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/file.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/file_item.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/file_part.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/file_stream.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/group_files.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/http_server.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/log.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/mount.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/rclone_impl.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/api.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/basic_ops.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/chunk_task.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/create.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/multipart/file_info.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/multipart/upload_info.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/multipart/upload_state.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/types.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/s3/upload_file_multipart.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/scan_missing_folders.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api/types.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/test +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/archive/test_paramiko.py.disabled +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_copy.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_copy_bytes.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_copy_file_resumable_s3.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_copy_files.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_db.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_diff.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_file_item.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_group_files.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_is_synced.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_ls.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_ls_stream_files.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_mount.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_obscure.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_rclone_config.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_read_write_text.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_remote_control.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_remotes.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_s3.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_scan_missing_folders.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_serve_http.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_size_files.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_size_suffix.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tests/test_walk.py +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/tox.ini +0 -0
- {rclone_api-1.4.15 → rclone_api-1.4.19}/upload_package.sh +0 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
import argparse
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from rclone_api import Rclone
|
6
|
+
from rclone_api.s3.s3_multipart_uploader_by_copy import (
|
7
|
+
s3_server_side_multi_part_merge,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class Args:
|
13
|
+
config_path: Path
|
14
|
+
src: str # like dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst-parts/ (info.json will be located here)
|
15
|
+
verbose: bool
|
16
|
+
|
17
|
+
|
18
|
+
def list_files(rclone: Rclone, path: str):
|
19
|
+
"""List files in a remote path."""
|
20
|
+
for dirlisting in rclone.walk(path):
|
21
|
+
for file in dirlisting.files:
|
22
|
+
print(file.path)
|
23
|
+
|
24
|
+
|
25
|
+
def _parse_args() -> Args:
|
26
|
+
parser = argparse.ArgumentParser(description="List files in a remote path.")
|
27
|
+
parser.add_argument("src", help="Directory that holds the info.json file")
|
28
|
+
parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true")
|
29
|
+
parser.add_argument(
|
30
|
+
"--config", help="Path to rclone config file", type=Path, required=False
|
31
|
+
)
|
32
|
+
args = parser.parse_args()
|
33
|
+
config: Path | None = args.config
|
34
|
+
if config is None:
|
35
|
+
config = Path("rclone.conf")
|
36
|
+
if not config.exists():
|
37
|
+
raise FileNotFoundError(f"Config file not found: {config}")
|
38
|
+
assert config is not None
|
39
|
+
out = Args(
|
40
|
+
config_path=config,
|
41
|
+
src=args.src,
|
42
|
+
verbose=args.verbose,
|
43
|
+
)
|
44
|
+
return out
|
45
|
+
|
46
|
+
|
47
|
+
def _get_info_path(src: str) -> str:
|
48
|
+
if src.endswith("/"):
|
49
|
+
src = src[:-1]
|
50
|
+
info_path = f"{src}/info.json"
|
51
|
+
return info_path
|
52
|
+
|
53
|
+
|
54
|
+
def main() -> int:
|
55
|
+
"""Main entry point."""
|
56
|
+
args = _parse_args()
|
57
|
+
rclone = Rclone(rclone_conf=args.config_path)
|
58
|
+
info_path = _get_info_path(src=args.src)
|
59
|
+
s3_server_side_multi_part_merge(
|
60
|
+
rclone=rclone.impl, info_path=info_path, max_workers=1
|
61
|
+
)
|
62
|
+
return 0
|
63
|
+
|
64
|
+
|
65
|
+
if __name__ == "__main__":
|
66
|
+
import sys
|
67
|
+
|
68
|
+
sys.argv.append("--config")
|
69
|
+
sys.argv.append("rclone.conf")
|
70
|
+
sys.argv.append(
|
71
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst-parts/"
|
72
|
+
)
|
73
|
+
main()
|
@@ -209,7 +209,10 @@ class InfoJson:
|
|
209
209
|
|
210
210
|
@property
|
211
211
|
def parts_dir(self) -> str:
|
212
|
-
|
212
|
+
parts_dir = os.path.dirname(self.src_info)
|
213
|
+
if parts_dir.endswith("/"):
|
214
|
+
parts_dir = parts_dir[:-1]
|
215
|
+
return parts_dir
|
213
216
|
|
214
217
|
@property
|
215
218
|
def dst(self) -> str:
|
@@ -1,12 +1,13 @@
|
|
1
1
|
import atexit
|
2
2
|
import subprocess
|
3
3
|
import threading
|
4
|
-
import time
|
5
4
|
import weakref
|
6
5
|
from dataclasses import dataclass
|
7
6
|
from pathlib import Path
|
8
7
|
from typing import Any
|
9
8
|
|
9
|
+
import psutil
|
10
|
+
|
10
11
|
from rclone_api.config import Config
|
11
12
|
from rclone_api.util import clear_temp_config_file, get_verbose, make_temp_config_file
|
12
13
|
|
@@ -24,20 +25,25 @@ class ProcessArgs:
|
|
24
25
|
|
25
26
|
class Process:
|
26
27
|
def __init__(self, args: ProcessArgs) -> None:
|
27
|
-
assert
|
28
|
+
assert (
|
29
|
+
args.rclone_exe.exists()
|
30
|
+
), f"rclone executable not found: {args.rclone_exe}"
|
28
31
|
self.args = args
|
29
32
|
self.log = args.log
|
30
33
|
self.tempfile: Path | None = None
|
34
|
+
|
31
35
|
verbose = get_verbose(args.verbose)
|
36
|
+
# Create a temporary config file if needed.
|
32
37
|
if isinstance(args.rclone_conf, Config):
|
33
|
-
self.
|
34
|
-
self.
|
35
|
-
rclone_conf = self.
|
38
|
+
self.tempfile = make_temp_config_file()
|
39
|
+
self.tempfile.write_text(args.rclone_conf.text, encoding="utf-8")
|
40
|
+
rclone_conf = self.tempfile
|
36
41
|
else:
|
37
42
|
rclone_conf = args.rclone_conf
|
38
43
|
|
39
|
-
assert rclone_conf.exists()
|
44
|
+
assert rclone_conf.exists(), f"rclone config not found: {rclone_conf}"
|
40
45
|
|
46
|
+
# Build the command.
|
41
47
|
self.cmd = (
|
42
48
|
[str(args.rclone_exe.resolve())]
|
43
49
|
+ ["--config", str(rclone_conf.resolve())]
|
@@ -49,16 +55,14 @@ class Process:
|
|
49
55
|
if verbose:
|
50
56
|
cmd_str = subprocess.list2cmdline(self.cmd)
|
51
57
|
print(f"Running: {cmd_str}")
|
52
|
-
kwargs: dict = {}
|
53
|
-
kwargs["shell"] = False
|
58
|
+
kwargs: dict = {"shell": False}
|
54
59
|
if args.capture_stdout:
|
55
60
|
kwargs["stdout"] = subprocess.PIPE
|
56
61
|
kwargs["stderr"] = subprocess.STDOUT
|
57
62
|
|
58
63
|
self.process = subprocess.Popen(self.cmd, **kwargs) # type: ignore
|
59
64
|
|
60
|
-
# Register an atexit callback using a weak reference to avoid
|
61
|
-
# keeping the Process instance alive solely due to the callback.
|
65
|
+
# Register an atexit callback using a weak reference to avoid keeping the Process instance alive.
|
62
66
|
self_ref = weakref.ref(self)
|
63
67
|
|
64
68
|
def exit_cleanup():
|
@@ -77,39 +81,60 @@ class Process:
|
|
77
81
|
self.cleanup()
|
78
82
|
|
79
83
|
def cleanup(self) -> None:
|
80
|
-
|
84
|
+
if self.tempfile:
|
85
|
+
clear_temp_config_file(self.tempfile)
|
81
86
|
|
82
|
-
def
|
87
|
+
def _kill_process_tree(self) -> None:
|
83
88
|
"""
|
84
|
-
|
85
|
-
If the process does not exit within a short timeout, it is aggressively killed.
|
89
|
+
Use psutil to recursively terminate the main process and all its child processes.
|
86
90
|
"""
|
87
|
-
|
88
|
-
|
89
|
-
|
91
|
+
try:
|
92
|
+
parent = psutil.Process(self.process.pid)
|
93
|
+
except psutil.NoSuchProcess:
|
94
|
+
return
|
95
|
+
|
96
|
+
# Terminate child processes.
|
97
|
+
children = parent.children(recursive=True)
|
98
|
+
if children:
|
99
|
+
print(f"Terminating {len(children)} child processes...")
|
100
|
+
for child in children:
|
90
101
|
try:
|
91
|
-
|
92
|
-
self.process.terminate()
|
102
|
+
child.terminate()
|
93
103
|
except Exception as e:
|
94
|
-
print(f"Error
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
time.sleep(0.1)
|
100
|
-
# If still running, kill aggressively.
|
101
|
-
if self.process.poll() is None:
|
104
|
+
print(f"Error terminating child process {child.pid}: {e}")
|
105
|
+
psutil.wait_procs(children, timeout=2)
|
106
|
+
# Kill any that remain.
|
107
|
+
for child in children:
|
108
|
+
if child.is_running():
|
102
109
|
try:
|
103
|
-
|
110
|
+
child.kill()
|
104
111
|
except Exception as e:
|
105
|
-
print(f"Error
|
106
|
-
|
112
|
+
print(f"Error killing child process {child.pid}: {e}")
|
113
|
+
|
114
|
+
# Terminate the parent process.
|
115
|
+
if parent.is_running():
|
116
|
+
try:
|
117
|
+
parent.terminate()
|
118
|
+
except Exception as e:
|
119
|
+
print(f"Error terminating process {parent.pid}: {e}")
|
120
|
+
try:
|
121
|
+
parent.wait(timeout=3)
|
122
|
+
except psutil.TimeoutExpired:
|
107
123
|
try:
|
108
|
-
|
109
|
-
except Exception:
|
110
|
-
|
124
|
+
parent.kill()
|
125
|
+
except Exception as e:
|
126
|
+
print(f"Error killing process {parent.pid}: {e}")
|
127
|
+
|
128
|
+
def _atexit_terminate(self) -> None:
|
129
|
+
"""
|
130
|
+
This method is registered via atexit and uses psutil to clean up the process tree.
|
131
|
+
It runs in a daemon thread so that termination happens without blocking interpreter shutdown.
|
132
|
+
"""
|
133
|
+
if self.process.poll() is None: # Process is still running.
|
134
|
+
|
135
|
+
def terminate_sequence():
|
136
|
+
self._kill_process_tree()
|
111
137
|
|
112
|
-
# Run the termination sequence in a separate daemon thread.
|
113
138
|
t = threading.Thread(target=terminate_sequence, daemon=True)
|
114
139
|
t.start()
|
115
140
|
t.join(timeout=3)
|
@@ -122,12 +147,12 @@ class Process:
|
|
122
147
|
self.cleanup()
|
123
148
|
|
124
149
|
def kill(self) -> None:
|
125
|
-
|
126
|
-
|
150
|
+
"""Forcefully kill the process tree."""
|
151
|
+
self._kill_process_tree()
|
127
152
|
|
128
153
|
def terminate(self) -> None:
|
129
|
-
|
130
|
-
|
154
|
+
"""Gracefully terminate the process tree."""
|
155
|
+
self._kill_process_tree()
|
131
156
|
|
132
157
|
@property
|
133
158
|
def returncode(self) -> int | None:
|
@@ -147,8 +172,8 @@ class Process:
|
|
147
172
|
def wait(self) -> int:
|
148
173
|
return self.process.wait()
|
149
174
|
|
150
|
-
def send_signal(self,
|
151
|
-
|
175
|
+
def send_signal(self, sig: int) -> None:
|
176
|
+
self.process.send_signal(sig)
|
152
177
|
|
153
178
|
def __str__(self) -> str:
|
154
179
|
state = ""
|
@@ -0,0 +1,147 @@
|
|
1
|
+
"""
|
2
|
+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/upload_part_copy.html
|
3
|
+
* client.upload_part_copy
|
4
|
+
|
5
|
+
This module provides functionality for S3 multipart uploads, including copying parts
|
6
|
+
from existing S3 objects using upload_part_copy.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import json
|
10
|
+
from dataclasses import dataclass
|
11
|
+
from typing import Any
|
12
|
+
|
13
|
+
from rclone_api.rclone_impl import RcloneImpl
|
14
|
+
from rclone_api.s3.multipart.finished_piece import FinishedPiece
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class Part:
|
19
|
+
part_number: int
|
20
|
+
s3_key: str
|
21
|
+
|
22
|
+
def to_json(self) -> dict:
|
23
|
+
return {"part_number": self.part_number, "s3_key": self.s3_key}
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def from_json(json_dict: dict) -> "Part | Exception":
|
27
|
+
part_number = json_dict.get("part_number")
|
28
|
+
s3_key = json_dict.get("s3_key")
|
29
|
+
if part_number is None or s3_key is None:
|
30
|
+
return Exception(f"Invalid JSON: {json_dict}")
|
31
|
+
return Part(part_number=part_number, s3_key=s3_key)
|
32
|
+
|
33
|
+
@staticmethod
|
34
|
+
def from_json_array(json_array: list[dict]) -> list["Part"] | Exception:
|
35
|
+
try:
|
36
|
+
out: list[Part] = []
|
37
|
+
for j in json_array:
|
38
|
+
ok_or_err = Part.from_json(j)
|
39
|
+
if isinstance(ok_or_err, Exception):
|
40
|
+
return ok_or_err
|
41
|
+
else:
|
42
|
+
out.append(ok_or_err)
|
43
|
+
return out
|
44
|
+
except Exception as e:
|
45
|
+
return e
|
46
|
+
|
47
|
+
|
48
|
+
class MergeState:
|
49
|
+
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
rclone_impl: RcloneImpl,
|
53
|
+
merge_path: str,
|
54
|
+
upload_id: str,
|
55
|
+
bucket: str,
|
56
|
+
dst_key: str,
|
57
|
+
finished: list[FinishedPiece],
|
58
|
+
all_parts: list[Part],
|
59
|
+
) -> None:
|
60
|
+
self.rclone_impl: RcloneImpl = rclone_impl
|
61
|
+
self.merge_path: str = merge_path
|
62
|
+
self.merge_parts_path: str = f"{merge_path}/merge" # future use?
|
63
|
+
self.upload_id: str = upload_id
|
64
|
+
self.bucket: str = bucket
|
65
|
+
self.dst_key: str = dst_key
|
66
|
+
self.finished: list[FinishedPiece] = list(finished)
|
67
|
+
self.all_parts: list[Part] = list(all_parts)
|
68
|
+
|
69
|
+
def on_finished(self, finished_piece: FinishedPiece) -> None:
|
70
|
+
self.finished.append(finished_piece)
|
71
|
+
|
72
|
+
def remaining_parts(self) -> list[Part]:
|
73
|
+
finished_parts: set[int] = set([p.part_number for p in self.finished])
|
74
|
+
remaining = [p for p in self.all_parts if p.part_number not in finished_parts]
|
75
|
+
return remaining
|
76
|
+
|
77
|
+
@staticmethod
|
78
|
+
def from_json(rclone_impl: RcloneImpl, json: dict) -> "MergeState | Exception":
|
79
|
+
try:
|
80
|
+
merge_path = json["merge_path"]
|
81
|
+
bucket = json["bucket"]
|
82
|
+
dst_key = json["dst_key"]
|
83
|
+
finished: list[FinishedPiece] = FinishedPiece.from_json_array(
|
84
|
+
json["finished"]
|
85
|
+
)
|
86
|
+
all_parts: list[Part | Exception] = [Part.from_json(j) for j in json["all"]]
|
87
|
+
all_parts_no_err: list[Part] = [
|
88
|
+
p for p in all_parts if not isinstance(p, Exception)
|
89
|
+
]
|
90
|
+
upload_id: str = json["upload_id"]
|
91
|
+
errs: list[Exception] = [p for p in all_parts if isinstance(p, Exception)]
|
92
|
+
if len(errs):
|
93
|
+
return Exception(f"Errors in parts: {errs}")
|
94
|
+
return MergeState(
|
95
|
+
rclone_impl=rclone_impl,
|
96
|
+
merge_path=merge_path,
|
97
|
+
upload_id=upload_id,
|
98
|
+
bucket=bucket,
|
99
|
+
dst_key=dst_key,
|
100
|
+
finished=finished,
|
101
|
+
all_parts=all_parts_no_err,
|
102
|
+
)
|
103
|
+
except Exception as e:
|
104
|
+
return e
|
105
|
+
|
106
|
+
def to_json(self) -> dict:
|
107
|
+
finished = self.finished.copy()
|
108
|
+
all_parts = self.all_parts.copy()
|
109
|
+
return {
|
110
|
+
"merge_path": self.merge_path,
|
111
|
+
"bucket": self.bucket,
|
112
|
+
"dst_key": self.dst_key,
|
113
|
+
"upload_id": self.upload_id,
|
114
|
+
"finished": FinishedPiece.to_json_array(finished),
|
115
|
+
"all": [part.to_json() for part in all_parts],
|
116
|
+
}
|
117
|
+
|
118
|
+
def to_json_str(self) -> str:
|
119
|
+
data = self.to_json()
|
120
|
+
out = json.dumps(data, indent=2)
|
121
|
+
return out
|
122
|
+
|
123
|
+
def __str__(self):
|
124
|
+
return self.to_json_str()
|
125
|
+
|
126
|
+
def __repr__(self):
|
127
|
+
return self.to_json_str()
|
128
|
+
|
129
|
+
def write(self, rclone_impl: Any, dst: str) -> None:
|
130
|
+
from rclone_api.rclone_impl import RcloneImpl
|
131
|
+
|
132
|
+
assert isinstance(rclone_impl, RcloneImpl)
|
133
|
+
json_str = self.to_json_str()
|
134
|
+
rclone_impl.write_text(dst, json_str)
|
135
|
+
|
136
|
+
def read(self, rclone_impl: Any, src: str) -> None:
|
137
|
+
from rclone_api.rclone_impl import RcloneImpl
|
138
|
+
|
139
|
+
assert isinstance(rclone_impl, RcloneImpl)
|
140
|
+
json_str = rclone_impl.read_text(src)
|
141
|
+
if isinstance(json_str, Exception):
|
142
|
+
raise json_str
|
143
|
+
json_dict = json.loads(json_str)
|
144
|
+
ok_or_err = FinishedPiece.from_json_array(json_dict["finished"])
|
145
|
+
if isinstance(ok_or_err, Exception):
|
146
|
+
raise ok_or_err
|
147
|
+
self.finished = ok_or_err
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import json
|
2
1
|
import warnings
|
3
2
|
from dataclasses import dataclass
|
4
3
|
|
@@ -13,11 +12,10 @@ class FinishedPiece:
|
|
13
12
|
def to_json(self) -> dict:
|
14
13
|
return {"part_number": self.part_number, "etag": self.etag}
|
15
14
|
|
16
|
-
def to_json_str(self) -> str:
|
17
|
-
return json.dumps(self.to_json(), indent=0)
|
18
|
-
|
19
15
|
@staticmethod
|
20
|
-
def to_json_array(
|
16
|
+
def to_json_array(
|
17
|
+
parts: list["FinishedPiece | EndOfStream"] | list["FinishedPiece"],
|
18
|
+
) -> list[dict]:
|
21
19
|
non_none: list[FinishedPiece] = []
|
22
20
|
for p in parts:
|
23
21
|
if not isinstance(p, EndOfStream):
|
@@ -39,3 +37,15 @@ class FinishedPiece:
|
|
39
37
|
if json is None:
|
40
38
|
return EndOfStream()
|
41
39
|
return FinishedPiece(**json)
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def from_json_array(json: dict) -> list["FinishedPiece"]:
|
43
|
+
tmp = [FinishedPiece.from_json(j) for j in json]
|
44
|
+
out: list[FinishedPiece] = []
|
45
|
+
for t in tmp:
|
46
|
+
if isinstance(t, FinishedPiece):
|
47
|
+
out.append(t)
|
48
|
+
return out
|
49
|
+
|
50
|
+
def __hash__(self) -> int:
|
51
|
+
return hash(self.part_number)
|