rclone-api 1.4.7__tar.gz → 1.4.9__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.7 → rclone_api-1.4.9}/PKG-INFO +1 -1
- {rclone_api-1.4.7 → rclone_api-1.4.9}/pyproject.toml +1 -1
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cmd/copy_large_s3.py +5 -13
- rclone_api-1.4.9/src/rclone_api/cmd/copy_large_s3_finish.py +228 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/detail/copy_file_parts.py +86 -15
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/process.py +6 -14
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/rclone_impl.py +61 -44
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/create.py +1 -1
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/multipart/finished_piece.py +4 -1
- rclone_api-1.4.9/src/rclone_api/s3/s3_multipart_uploader_by_copy.py +419 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/types.py +1 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/types.py +13 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/util.py +52 -9
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/SOURCES.txt +2 -1
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_diff.py +2 -2
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_s3.py +1 -0
- rclone_api-1.4.7/src/rclone_api/s3/s3_multipart_uploader.py +0 -138
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.aiderignore +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.gitignore +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.pylintrc +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.vscode/launch.json +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.vscode/settings.json +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/.vscode/tasks.json +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/LICENSE +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/MANIFEST.in +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/README.md +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/clean +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/install +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/lint +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/requirements.testing.txt +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/setup.cfg +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/setup.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/__init__.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cmd/analyze.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cmd/save_to_db.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/config.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/db/__init__.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/db/db.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/db/models.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/detail/walk.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/experimental/flags.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/experimental/flags_base.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/file.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/file_item.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/file_part.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/file_stream.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/group_files.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/http_server.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/log.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/mount.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/api.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/basic_ops.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/chunk_task.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/multipart/file_info.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/multipart/upload_info.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/multipart/upload_state.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/upload_file_multipart.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/scan_missing_folders.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/test +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/archive/test_paramiko.py.disabled +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_copy.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_copy_bytes.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_copy_file_resumable_s3.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_copy_files.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_db.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_file_item.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_group_files.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_is_synced.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_ls.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_ls_stream_files.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_mount.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_obscure.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_rclone_config.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_read_write_text.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_remote_control.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_remotes.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_scan_missing_folders.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_serve_http.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_size_files.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_size_suffix.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_walk.py +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/tox.ini +0 -0
- {rclone_api-1.4.7 → rclone_api-1.4.9}/upload_package.sh +0 -0
@@ -11,8 +11,7 @@ class Args:
|
|
11
11
|
src: str
|
12
12
|
dst: str
|
13
13
|
chunk_size: SizeSuffix
|
14
|
-
|
15
|
-
write_threads: int
|
14
|
+
threads: int
|
16
15
|
retries: int
|
17
16
|
save_state_json: Path
|
18
17
|
verbose: bool
|
@@ -40,16 +39,10 @@ def _parse_args() -> Args:
|
|
40
39
|
default="128MB", # if this is too low or too high an s3 service
|
41
40
|
)
|
42
41
|
parser.add_argument(
|
43
|
-
"--
|
44
|
-
help="Number of concurrent read threads per chunk, only one chunk will be read at a time",
|
45
|
-
type=int,
|
46
|
-
default=8,
|
47
|
-
)
|
48
|
-
parser.add_argument(
|
49
|
-
"--write-threads",
|
42
|
+
"--threads",
|
50
43
|
help="Max number of chunks to upload in parallel to the destination, each chunk is uploaded in a separate thread",
|
51
44
|
type=int,
|
52
|
-
default=
|
45
|
+
default=8,
|
53
46
|
)
|
54
47
|
parser.add_argument("--retries", help="Number of retries", type=int, default=3)
|
55
48
|
parser.add_argument(
|
@@ -70,9 +63,8 @@ def _parse_args() -> Args:
|
|
70
63
|
config_path=config,
|
71
64
|
src=args.src,
|
72
65
|
dst=args.dst,
|
66
|
+
threads=args.threads,
|
73
67
|
chunk_size=SizeSuffix(args.chunk_size),
|
74
|
-
read_threads=args.read_threads,
|
75
|
-
write_threads=args.write_threads,
|
76
68
|
retries=args.retries,
|
77
69
|
save_state_json=args.resume_json,
|
78
70
|
verbose=args.verbose,
|
@@ -122,6 +114,6 @@ if __name__ == "__main__":
|
|
122
114
|
"45061:aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
|
123
115
|
)
|
124
116
|
sys.argv.append(
|
125
|
-
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
|
117
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst-parts"
|
126
118
|
)
|
127
119
|
main()
|
@@ -0,0 +1,228 @@
|
|
1
|
+
import argparse
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from rclone_api import Rclone
|
6
|
+
from rclone_api.detail.copy_file_parts import InfoJson
|
7
|
+
from rclone_api.s3.s3_multipart_uploader_by_copy import (
|
8
|
+
finish_multipart_upload_from_keys,
|
9
|
+
)
|
10
|
+
from rclone_api.types import SizeSuffix
|
11
|
+
|
12
|
+
DATA_SOURCE = (
|
13
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
# response = client.upload_part_copy(
|
18
|
+
# Bucket='string',
|
19
|
+
# CopySource='string' or {'Bucket': 'string', 'Key': 'string', 'VersionId': 'string'},
|
20
|
+
# CopySourceIfMatch='string',
|
21
|
+
# CopySourceIfModifiedSince=datetime(2015, 1, 1),
|
22
|
+
# CopySourceIfNoneMatch='string',
|
23
|
+
# CopySourceIfUnmodifiedSince=datetime(2015, 1, 1),
|
24
|
+
# CopySourceRange='string',
|
25
|
+
# Key='string',
|
26
|
+
# PartNumber=123,
|
27
|
+
# UploadId='string',
|
28
|
+
# SSECustomerAlgorithm='string',
|
29
|
+
# SSECustomerKey='string',
|
30
|
+
# CopySourceSSECustomerAlgorithm='string',
|
31
|
+
# CopySourceSSECustomerKey='string',
|
32
|
+
# RequestPayer='requester',
|
33
|
+
# ExpectedBucketOwner='string',
|
34
|
+
# ExpectedSourceBucketOwner='string'
|
35
|
+
# )
|
36
|
+
|
37
|
+
|
38
|
+
@dataclass
|
39
|
+
class Args:
|
40
|
+
config_path: Path
|
41
|
+
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)
|
42
|
+
dst: str # like dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst
|
43
|
+
verbose: bool
|
44
|
+
|
45
|
+
|
46
|
+
def list_files(rclone: Rclone, path: str):
|
47
|
+
"""List files in a remote path."""
|
48
|
+
for dirlisting in rclone.walk(path):
|
49
|
+
for file in dirlisting.files:
|
50
|
+
print(file.path)
|
51
|
+
|
52
|
+
|
53
|
+
def _parse_args() -> Args:
|
54
|
+
parser = argparse.ArgumentParser(description="List files in a remote path.")
|
55
|
+
parser.add_argument("src", help="File to copy")
|
56
|
+
parser.add_argument("dst", help="Destination file")
|
57
|
+
parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true")
|
58
|
+
parser.add_argument(
|
59
|
+
"--config", help="Path to rclone config file", type=Path, required=False
|
60
|
+
)
|
61
|
+
parser.add_argument(
|
62
|
+
"--chunk-size",
|
63
|
+
help="Chunk size that will be read and uploaded in SizeSuffix form, too low or too high will cause issues",
|
64
|
+
type=str,
|
65
|
+
default="128MB", # if this is too low or too high an s3 service
|
66
|
+
)
|
67
|
+
|
68
|
+
args = parser.parse_args()
|
69
|
+
config: Path | None = args.config
|
70
|
+
if config is None:
|
71
|
+
config = Path("rclone.conf")
|
72
|
+
if not config.exists():
|
73
|
+
raise FileNotFoundError(f"Config file not found: {config}")
|
74
|
+
assert config is not None
|
75
|
+
out = Args(
|
76
|
+
config_path=config,
|
77
|
+
src=args.src,
|
78
|
+
dst=args.dst,
|
79
|
+
verbose=args.verbose,
|
80
|
+
)
|
81
|
+
return out
|
82
|
+
|
83
|
+
|
84
|
+
# from dataclasses import dataclass
|
85
|
+
|
86
|
+
# def parse_info_json(text: str) -> UploadInfo:
|
87
|
+
# import json
|
88
|
+
# data = json.loads(text)
|
89
|
+
# chunk_size = data["chunksize_int"]
|
90
|
+
# first_part = data["first_part"]
|
91
|
+
# last_part = data["last_part"]
|
92
|
+
# assert isinstance(chunk_size, int)
|
93
|
+
# assert isinstance(first_part, int)
|
94
|
+
# assert isinstance(last_part, int)
|
95
|
+
# assert first_part <= last_part
|
96
|
+
# parts: list[str] = []
|
97
|
+
# fmt = "part.{:05d}_{}-{}"
|
98
|
+
# for i in range(first_part, last_part + 1):
|
99
|
+
# offset: int = i * chunk_size
|
100
|
+
# end: int = (i + 1) * chunk_size
|
101
|
+
# part = fmt.format(i, offset, end)
|
102
|
+
# parts.append(part)
|
103
|
+
# return UploadInfo(chunk_size=chunk_size, parts=parts)
|
104
|
+
|
105
|
+
|
106
|
+
def do_finish_part(rclone: Rclone, info: InfoJson, dst: str) -> None:
|
107
|
+
from rclone_api.s3.create import BaseClient, S3Credentials, create_s3_client
|
108
|
+
|
109
|
+
s3_creds: S3Credentials = rclone.impl.get_s3_credentials(remote=dst)
|
110
|
+
s3_client: BaseClient = create_s3_client(s3_creds)
|
111
|
+
s3_bucket = s3_creds.bucket_name
|
112
|
+
is_done = info.fetch_is_done()
|
113
|
+
assert is_done, f"Upload is not done: {info}"
|
114
|
+
|
115
|
+
parts_dir = info.parts_dir
|
116
|
+
if parts_dir.endswith("/"):
|
117
|
+
parts_dir = parts_dir[:-1]
|
118
|
+
source_keys = info.fetch_all_finished()
|
119
|
+
# print(parts_dir)
|
120
|
+
# print(source_keys)
|
121
|
+
|
122
|
+
parts_path = parts_dir.split(s3_bucket)[1]
|
123
|
+
if parts_path.startswith("/"):
|
124
|
+
parts_path = parts_path[1:]
|
125
|
+
|
126
|
+
first_part: int | None = info.first_part
|
127
|
+
last_part: int | None = info.last_part
|
128
|
+
size: SizeSuffix | None = info.size
|
129
|
+
|
130
|
+
assert first_part is not None
|
131
|
+
assert last_part is not None
|
132
|
+
assert size is not None
|
133
|
+
|
134
|
+
def _to_s3_key(name: str | None) -> str:
|
135
|
+
if name:
|
136
|
+
out = f"{parts_path}/{name}"
|
137
|
+
return out
|
138
|
+
out = f"{parts_path}"
|
139
|
+
return out
|
140
|
+
|
141
|
+
# s3_keys: list[str] = [_to_s3_key(name=p) for p in source_keys]
|
142
|
+
parts: list[tuple[int, str]] = []
|
143
|
+
part_num = 1
|
144
|
+
for part_key in source_keys:
|
145
|
+
s3_key = _to_s3_key(name=part_key)
|
146
|
+
parts.append((part_num, s3_key))
|
147
|
+
part_num += 1
|
148
|
+
|
149
|
+
# for key in parts:
|
150
|
+
# print(key)
|
151
|
+
|
152
|
+
chunksize = info.chunksize
|
153
|
+
assert chunksize is not None
|
154
|
+
|
155
|
+
import os
|
156
|
+
|
157
|
+
dst_name = info.dst_name
|
158
|
+
dst_dir = os.path.dirname(parts_path)
|
159
|
+
# dst_key =
|
160
|
+
dst_key = f"{dst_dir}/{dst_name}"
|
161
|
+
|
162
|
+
finish_multipart_upload_from_keys(
|
163
|
+
s3_client=s3_client,
|
164
|
+
source_bucket=s3_creds.bucket_name,
|
165
|
+
parts=parts,
|
166
|
+
destination_bucket=s3_creds.bucket_name,
|
167
|
+
destination_key=dst_key,
|
168
|
+
chunk_size=chunksize.as_int(),
|
169
|
+
final_size=size.as_int(),
|
170
|
+
max_workers=100,
|
171
|
+
retries=3,
|
172
|
+
)
|
173
|
+
|
174
|
+
if False:
|
175
|
+
print(finish_multipart_upload_from_keys)
|
176
|
+
print(s3_client)
|
177
|
+
print("done")
|
178
|
+
|
179
|
+
# def finish_multipart_upload_from_keys(
|
180
|
+
# s3_client: BaseClient,
|
181
|
+
# source_bucket: str,
|
182
|
+
# source_keys: list[str],
|
183
|
+
# destination_bucket: str,
|
184
|
+
# destination_key: str,
|
185
|
+
# chunk_size: int = 5 * 1024 * 1024, # 5MB default
|
186
|
+
# retries: int = 3,
|
187
|
+
# byte_ranges: list[str] | None = None,
|
188
|
+
|
189
|
+
# if False:
|
190
|
+
# finish_multipart_upload_from_keys(
|
191
|
+
# s3_client=s3_client,
|
192
|
+
# source_bucket="TODO",
|
193
|
+
# source_keys=[p.key for p in all_parts],
|
194
|
+
# destination_bucket=info.dst_bucket,
|
195
|
+
# destination_key=info.dst_key,
|
196
|
+
# chunk_size=5 * 1024 * 1024,
|
197
|
+
# retries=3,
|
198
|
+
# byte_ranges=None,
|
199
|
+
# )
|
200
|
+
|
201
|
+
# print(all_parts)
|
202
|
+
|
203
|
+
|
204
|
+
def main() -> int:
|
205
|
+
"""Main entry point."""
|
206
|
+
args = _parse_args()
|
207
|
+
rclone = Rclone(rclone_conf=args.config_path)
|
208
|
+
info_json = f"{args.src}/info.json".replace("//", "/")
|
209
|
+
info = InfoJson(rclone.impl, src=None, src_info=info_json)
|
210
|
+
loaded = info.load()
|
211
|
+
assert loaded
|
212
|
+
print(info)
|
213
|
+
do_finish_part(rclone=rclone, info=info, dst=args.dst)
|
214
|
+
return 0
|
215
|
+
|
216
|
+
|
217
|
+
if __name__ == "__main__":
|
218
|
+
import sys
|
219
|
+
|
220
|
+
sys.argv.append("--config")
|
221
|
+
sys.argv.append("rclone.conf")
|
222
|
+
sys.argv.append(
|
223
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst-parts/"
|
224
|
+
)
|
225
|
+
sys.argv.append(
|
226
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
|
227
|
+
)
|
228
|
+
main()
|
@@ -53,7 +53,7 @@ def upload_task(self: RcloneImpl, upload_part: UploadPart) -> UploadPart:
|
|
53
53
|
msg = "\n#########################################\n"
|
54
54
|
msg += f"# Uploading {upload_part.chunk} to {upload_part.dst_part}\n"
|
55
55
|
msg += "#########################################\n"
|
56
|
-
print
|
56
|
+
print(msg)
|
57
57
|
self.copy_to(upload_part.chunk.as_posix(), upload_part.dst_part)
|
58
58
|
return upload_part
|
59
59
|
except Exception as e:
|
@@ -106,11 +106,24 @@ def _fetch_all_names(
|
|
106
106
|
return filtered
|
107
107
|
|
108
108
|
|
109
|
-
def _get_info_json(self: RcloneImpl, src: str, src_info: str) -> dict:
|
109
|
+
def _get_info_json(self: RcloneImpl, src: str | None, src_info: str) -> dict:
|
110
110
|
from rclone_api.file import File
|
111
111
|
|
112
|
+
data: dict
|
113
|
+
text: str
|
114
|
+
if src is None:
|
115
|
+
# just try to load the file
|
116
|
+
text_or_err = self.read_text(src_info)
|
117
|
+
if isinstance(text_or_err, Exception):
|
118
|
+
raise FileNotFoundError(f"Could not load {src_info}: {text_or_err}")
|
119
|
+
assert isinstance(text_or_err, str)
|
120
|
+
text = text_or_err
|
121
|
+
data = json.loads(text)
|
122
|
+
return data
|
123
|
+
|
112
124
|
src_stat: File | Exception = self.stat(src)
|
113
125
|
if isinstance(src_stat, Exception):
|
126
|
+
# just try to load the file
|
114
127
|
raise FileNotFoundError(f"Failed to stat {src}: {src_stat}")
|
115
128
|
|
116
129
|
now: datetime = datetime.now()
|
@@ -133,12 +146,11 @@ def _get_info_json(self: RcloneImpl, src: str, src_info: str) -> dict:
|
|
133
146
|
warnings.warn(f"Failed to read {src_info}: {text_or_err}")
|
134
147
|
return new_data
|
135
148
|
assert isinstance(text_or_err, str)
|
136
|
-
text
|
149
|
+
text = text_or_err
|
137
150
|
|
138
151
|
if err is not None:
|
139
152
|
return new_data
|
140
153
|
|
141
|
-
data: dict = {}
|
142
154
|
try:
|
143
155
|
data = json.loads(text)
|
144
156
|
return data
|
@@ -168,13 +180,14 @@ def _save_info_json(self: RcloneImpl, src: str, data: dict) -> None:
|
|
168
180
|
|
169
181
|
|
170
182
|
class InfoJson:
|
171
|
-
def __init__(self, rclone: RcloneImpl, src: str, src_info: str) -> None:
|
183
|
+
def __init__(self, rclone: RcloneImpl, src: str | None, src_info: str) -> None:
|
172
184
|
self.rclone = rclone
|
173
185
|
self.src = src
|
174
186
|
self.src_info = src_info
|
175
187
|
self.data: dict = {}
|
176
188
|
|
177
189
|
def load(self) -> bool:
|
190
|
+
"""Returns true if the file exist and is now loaded."""
|
178
191
|
self.data = _get_info_json(self.rclone, self.src, self.src_info)
|
179
192
|
return not self.data.get("new", False)
|
180
193
|
|
@@ -194,16 +207,71 @@ class InfoJson:
|
|
194
207
|
part_numbers = [int(name.split("_")[0].split(".")[1]) for name in names]
|
195
208
|
return part_numbers
|
196
209
|
|
210
|
+
@property
|
211
|
+
def parts_dir(self) -> str:
|
212
|
+
return os.path.dirname(self.src_info)
|
213
|
+
|
214
|
+
@property
|
215
|
+
def dst(self) -> str:
|
216
|
+
parts_dir = self.parts_dir
|
217
|
+
assert parts_dir.endswith("-parts")
|
218
|
+
out = parts_dir[:-6]
|
219
|
+
return out
|
220
|
+
|
221
|
+
@property
|
222
|
+
def dst_name(self) -> str:
|
223
|
+
return os.path.basename(self.dst)
|
224
|
+
|
225
|
+
def compute_all_parts(self) -> list[PartInfo] | Exception:
|
226
|
+
# full_part_infos: list[PartInfo] | Exception = PartInfo.split_parts(
|
227
|
+
# src_size, SizeSuffix("96MB")
|
228
|
+
try:
|
229
|
+
|
230
|
+
src_size = self.size
|
231
|
+
chunk_size = self.chunksize
|
232
|
+
assert isinstance(src_size, SizeSuffix)
|
233
|
+
assert isinstance(chunk_size, SizeSuffix)
|
234
|
+
first_part = self.data["first_part"]
|
235
|
+
last_part = self.data["last_part"]
|
236
|
+
full_part_infos: list[PartInfo] = PartInfo.split_parts(src_size, chunk_size)
|
237
|
+
return full_part_infos[first_part : last_part + 1]
|
238
|
+
except Exception as e:
|
239
|
+
return e
|
240
|
+
|
241
|
+
def compute_all_part_numbers(self) -> list[int] | Exception:
|
242
|
+
all_parts: list[PartInfo] | Exception = self.compute_all_parts()
|
243
|
+
if isinstance(all_parts, Exception):
|
244
|
+
raise all_parts
|
245
|
+
|
246
|
+
all_part_nums: list[int] = [p.part_number for p in all_parts]
|
247
|
+
return all_part_nums
|
248
|
+
|
249
|
+
def fetch_remaining_part_numbers(self) -> list[int] | Exception:
|
250
|
+
all_part_nums: list[int] | Exception = self.compute_all_part_numbers()
|
251
|
+
if isinstance(all_part_nums, Exception):
|
252
|
+
return all_part_nums
|
253
|
+
finished_part_nums: list[int] = self.fetch_all_finished_part_numbers()
|
254
|
+
remaining_part_nums: list[int] = list(
|
255
|
+
set(all_part_nums) - set(finished_part_nums)
|
256
|
+
)
|
257
|
+
return sorted(remaining_part_nums)
|
258
|
+
|
259
|
+
def fetch_is_done(self) -> bool:
|
260
|
+
remaining_part_nums: list[int] | Exception = self.fetch_remaining_part_numbers()
|
261
|
+
if isinstance(remaining_part_nums, Exception):
|
262
|
+
return False
|
263
|
+
return len(remaining_part_nums) == 0
|
264
|
+
|
197
265
|
@property
|
198
266
|
def new(self) -> bool:
|
199
267
|
return self.data.get("new", False)
|
200
268
|
|
201
269
|
@property
|
202
270
|
def chunksize(self) -> SizeSuffix | None:
|
203
|
-
|
204
|
-
if
|
271
|
+
chunksize_int: int | None = self.data.get("chunksize_int")
|
272
|
+
if chunksize_int is None:
|
205
273
|
return None
|
206
|
-
return SizeSuffix(
|
274
|
+
return SizeSuffix(chunksize_int)
|
207
275
|
|
208
276
|
@chunksize.setter
|
209
277
|
def chunksize(self, value: SizeSuffix) -> None:
|
@@ -219,21 +287,24 @@ class InfoJson:
|
|
219
287
|
self.data["src_modtime"] = value.isoformat()
|
220
288
|
|
221
289
|
@property
|
222
|
-
def
|
290
|
+
def size(self) -> SizeSuffix:
|
291
|
+
return SizeSuffix(self.data["size"])
|
292
|
+
|
293
|
+
def _get_first_part(self) -> int | None:
|
223
294
|
return self.data.get("first_part")
|
224
295
|
|
225
|
-
|
226
|
-
def first_part(self, value: int) -> None:
|
296
|
+
def _set_first_part(self, value: int) -> None:
|
227
297
|
self.data["first_part"] = value
|
228
298
|
|
229
|
-
|
230
|
-
def last_part(self) -> int | None:
|
299
|
+
def _get_last_part(self) -> int | None:
|
231
300
|
return self.data.get("last_part")
|
232
301
|
|
233
|
-
|
234
|
-
def last_part(self, value: int) -> None:
|
302
|
+
def _set_last_part(self, value: int) -> None:
|
235
303
|
self.data["last_part"] = value
|
236
304
|
|
305
|
+
first_part: int | None = property(_get_first_part, _set_first_part) # type: ignore
|
306
|
+
last_part: int | None = property(_get_last_part, _set_last_part) # type: ignore
|
307
|
+
|
237
308
|
@property
|
238
309
|
def hash(self) -> str | None:
|
239
310
|
return self.data.get("hash")
|
@@ -5,11 +5,10 @@ import time
|
|
5
5
|
import weakref
|
6
6
|
from dataclasses import dataclass
|
7
7
|
from pathlib import Path
|
8
|
-
from tempfile import TemporaryDirectory
|
9
8
|
from typing import Any
|
10
9
|
|
11
10
|
from rclone_api.config import Config
|
12
|
-
from rclone_api.util import get_verbose
|
11
|
+
from rclone_api.util import clear_temp_config_file, get_verbose, make_temp_config_file
|
13
12
|
|
14
13
|
|
15
14
|
@dataclass
|
@@ -28,17 +27,14 @@ class Process:
|
|
28
27
|
assert args.rclone_exe.exists()
|
29
28
|
self.args = args
|
30
29
|
self.log = args.log
|
31
|
-
self.
|
30
|
+
self.tempfile: Path | None = None
|
32
31
|
verbose = get_verbose(args.verbose)
|
33
32
|
if isinstance(args.rclone_conf, Config):
|
34
|
-
self.
|
35
|
-
tmpfile
|
36
|
-
|
37
|
-
rclone_conf = tmpfile
|
38
|
-
self.needs_cleanup = True
|
33
|
+
self.tmpfile = make_temp_config_file()
|
34
|
+
self.tmpfile.write_text(args.rclone_conf.text, encoding="utf-8")
|
35
|
+
rclone_conf = self.tmpfile
|
39
36
|
else:
|
40
37
|
rclone_conf = args.rclone_conf
|
41
|
-
self.needs_cleanup = False
|
42
38
|
|
43
39
|
assert rclone_conf.exists()
|
44
40
|
|
@@ -81,11 +77,7 @@ class Process:
|
|
81
77
|
self.cleanup()
|
82
78
|
|
83
79
|
def cleanup(self) -> None:
|
84
|
-
|
85
|
-
try:
|
86
|
-
self.tempdir.cleanup()
|
87
|
-
except Exception as e:
|
88
|
-
print(f"Error cleaning up tempdir: {e}")
|
80
|
+
clear_temp_config_file(self.tempfile)
|
89
81
|
|
90
82
|
def _atexit_terminate(self) -> None:
|
91
83
|
"""
|
@@ -32,6 +32,7 @@ from rclone_api.mount import Mount, clean_mount, prepare_mount
|
|
32
32
|
from rclone_api.process import Process
|
33
33
|
from rclone_api.remote import Remote
|
34
34
|
from rclone_api.rpath import RPath
|
35
|
+
from rclone_api.s3.create import S3Credentials
|
35
36
|
from rclone_api.s3.types import (
|
36
37
|
MultiUploadResult,
|
37
38
|
S3MutliPartUploadConfig,
|
@@ -862,6 +863,64 @@ class RcloneImpl:
|
|
862
863
|
)
|
863
864
|
return SizeSuffix(out.total_size)
|
864
865
|
|
866
|
+
def get_s3_credentials(
|
867
|
+
self, remote: str, verbose: bool | None = None
|
868
|
+
) -> S3Credentials:
|
869
|
+
from rclone_api.util import S3PathInfo, split_s3_path
|
870
|
+
|
871
|
+
verbose = get_verbose(verbose)
|
872
|
+
path_info: S3PathInfo = split_s3_path(remote)
|
873
|
+
|
874
|
+
# path_info: S3PathInfo = split_s3_path(remote)
|
875
|
+
remote = path_info.remote
|
876
|
+
bucket_name = path_info.bucket
|
877
|
+
|
878
|
+
remote = path_info.remote
|
879
|
+
parsed: Parsed = self.config.parse()
|
880
|
+
sections: dict[str, Section] = parsed.sections
|
881
|
+
if remote not in sections:
|
882
|
+
raise ValueError(
|
883
|
+
f"Remote {remote} not found in rclone config, remotes are: {sections.keys()}"
|
884
|
+
)
|
885
|
+
|
886
|
+
section: Section = sections[remote]
|
887
|
+
dst_type = section.type()
|
888
|
+
if dst_type != "s3" and dst_type != "b2":
|
889
|
+
raise ValueError(
|
890
|
+
f"Remote {remote} is not an S3 remote, it is of type {dst_type}"
|
891
|
+
)
|
892
|
+
|
893
|
+
def get_provider_str(section=section) -> str | None:
|
894
|
+
type: str = section.type()
|
895
|
+
provider: str | None = section.provider()
|
896
|
+
if provider is not None:
|
897
|
+
return provider
|
898
|
+
if type == "b2":
|
899
|
+
return S3Provider.BACKBLAZE.value
|
900
|
+
if type != "s3":
|
901
|
+
raise ValueError(f"Remote {remote} is not an S3 remote")
|
902
|
+
return S3Provider.S3.value
|
903
|
+
|
904
|
+
provider: str
|
905
|
+
if provided_provider_str := get_provider_str():
|
906
|
+
if verbose:
|
907
|
+
print(f"Using provided provider: {provided_provider_str}")
|
908
|
+
provider = provided_provider_str
|
909
|
+
else:
|
910
|
+
if verbose:
|
911
|
+
print(f"Using default provider: {S3Provider.S3.value}")
|
912
|
+
provider = S3Provider.S3.value
|
913
|
+
provider_enum = S3Provider.from_str(provider)
|
914
|
+
|
915
|
+
s3_creds: S3Credentials = S3Credentials(
|
916
|
+
bucket_name=bucket_name,
|
917
|
+
provider=provider_enum,
|
918
|
+
access_key_id=section.access_key_id(),
|
919
|
+
secret_access_key=section.secret_access_key(),
|
920
|
+
endpoint_url=section.endpoint(),
|
921
|
+
)
|
922
|
+
return s3_creds
|
923
|
+
|
865
924
|
def copy_file_resumable_s3(
|
866
925
|
self,
|
867
926
|
src: str,
|
@@ -878,7 +937,6 @@ class RcloneImpl:
|
|
878
937
|
"""For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
|
879
938
|
from rclone_api.http_server import HttpFetcher, HttpServer
|
880
939
|
from rclone_api.s3.api import S3Client
|
881
|
-
from rclone_api.s3.create import S3Credentials
|
882
940
|
from rclone_api.util import S3PathInfo, split_s3_path
|
883
941
|
|
884
942
|
src_path = Path(src)
|
@@ -909,51 +967,10 @@ class RcloneImpl:
|
|
909
967
|
)
|
910
968
|
|
911
969
|
path_info: S3PathInfo = split_s3_path(dst)
|
912
|
-
remote = path_info.remote
|
970
|
+
# remote = path_info.remote
|
913
971
|
bucket_name = path_info.bucket
|
914
972
|
s3_key = path_info.key
|
915
|
-
|
916
|
-
sections: dict[str, Section] = parsed.sections
|
917
|
-
if remote not in sections:
|
918
|
-
raise ValueError(
|
919
|
-
f"Remote {remote} not found in rclone config, remotes are: {sections.keys()}"
|
920
|
-
)
|
921
|
-
|
922
|
-
section: Section = sections[remote]
|
923
|
-
dst_type = section.type()
|
924
|
-
if dst_type != "s3" and dst_type != "b2":
|
925
|
-
raise ValueError(
|
926
|
-
f"Remote {remote} is not an S3 remote, it is of type {dst_type}"
|
927
|
-
)
|
928
|
-
|
929
|
-
def get_provider_str(section=section) -> str | None:
|
930
|
-
type: str = section.type()
|
931
|
-
provider: str | None = section.provider()
|
932
|
-
if provider is not None:
|
933
|
-
return provider
|
934
|
-
if type == "b2":
|
935
|
-
return S3Provider.BACKBLAZE.value
|
936
|
-
if type != "s3":
|
937
|
-
raise ValueError(f"Remote {remote} is not an S3 remote")
|
938
|
-
return S3Provider.S3.value
|
939
|
-
|
940
|
-
provider: str
|
941
|
-
if provided_provider_str := get_provider_str():
|
942
|
-
if verbose:
|
943
|
-
print(f"Using provided provider: {provided_provider_str}")
|
944
|
-
provider = provided_provider_str
|
945
|
-
else:
|
946
|
-
if verbose:
|
947
|
-
print(f"Using default provider: {S3Provider.S3.value}")
|
948
|
-
provider = S3Provider.S3.value
|
949
|
-
provider_enum = S3Provider.from_str(provider)
|
950
|
-
|
951
|
-
s3_creds: S3Credentials = S3Credentials(
|
952
|
-
provider=provider_enum,
|
953
|
-
access_key_id=section.access_key_id(),
|
954
|
-
secret_access_key=section.secret_access_key(),
|
955
|
-
endpoint_url=section.endpoint(),
|
956
|
-
)
|
973
|
+
s3_creds: S3Credentials = self.get_s3_credentials(dst, verbose=verbose)
|
957
974
|
|
958
975
|
port = random.randint(10000, 20000)
|
959
976
|
http_server: HttpServer = self.serve_http(
|
@@ -25,7 +25,7 @@ def _create_backblaze_s3_client(creds: S3Credentials, verbose: bool) -> BaseClie
|
|
25
25
|
aws_access_key_id=access_key,
|
26
26
|
aws_secret_access_key=secret_key,
|
27
27
|
endpoint_url=endpoint_url,
|
28
|
-
verify=False, # Disables SSL certificate verification
|
28
|
+
# verify=False, # Disables SSL certificate verification
|
29
29
|
config=Config(
|
30
30
|
signature_version="s3v4",
|
31
31
|
region_name=region_name,
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import json
|
2
|
+
import warnings
|
2
3
|
from dataclasses import dataclass
|
3
4
|
|
4
5
|
from rclone_api.types import EndOfStream
|
@@ -28,7 +29,9 @@ class FinishedPiece:
|
|
28
29
|
for p in parts:
|
29
30
|
if p is EndOfStream:
|
30
31
|
count_eos += 1
|
31
|
-
assert count_eos <= 1, "Only one EndOfStream should be present"
|
32
|
+
# assert count_eos <= 1, "Only one EndOfStream should be present"
|
33
|
+
if count_eos > 1:
|
34
|
+
warnings.warn(f"Only one EndOfStream should be present, found {count_eos}")
|
32
35
|
return [p.to_json() for p in non_none]
|
33
36
|
|
34
37
|
@staticmethod
|