rclone-api 1.4.6__tar.gz → 1.4.8__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.6 → rclone_api-1.4.8}/PKG-INFO +1 -1
- {rclone_api-1.4.6 → rclone_api-1.4.8}/pyproject.toml +1 -1
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/cmd/copy_large_s3.py +5 -13
- rclone_api-1.4.8/src/rclone_api/cmd/copy_large_s3_finish.py +217 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/detail/copy_file_parts.py +109 -17
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/process.py +6 -14
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/rclone_impl.py +61 -44
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/multipart/finished_piece.py +4 -1
- rclone_api-1.4.8/src/rclone_api/s3/s3_multipart_uploader_by_copy.py +395 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/types.py +1 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/types.py +13 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/util.py +52 -9
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api.egg-info/SOURCES.txt +2 -1
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_diff.py +2 -2
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_s3.py +1 -0
- rclone_api-1.4.6/src/rclone_api/s3/s3_multipart_uploader.py +0 -138
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.aiderignore +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.gitignore +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.pylintrc +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.vscode/launch.json +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.vscode/settings.json +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/.vscode/tasks.json +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/LICENSE +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/MANIFEST.in +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/README.md +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/clean +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/install +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/lint +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/requirements.testing.txt +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/setup.cfg +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/setup.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/__init__.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/cmd/analyze.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/cmd/save_to_db.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/config.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/db/__init__.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/db/db.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/db/models.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/detail/walk.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/experimental/flags.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/experimental/flags_base.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/file.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/file_item.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/file_part.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/file_stream.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/group_files.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/http_server.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/log.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/mount.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/api.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/basic_ops.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/chunk_task.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/create.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/multipart/file_info.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/multipart/upload_info.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/multipart/upload_state.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/s3/upload_file_multipart.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api/scan_missing_folders.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/test +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/archive/test_paramiko.py.disabled +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_copy.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_copy_bytes.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_copy_file_resumable_s3.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_copy_files.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_db.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_file_item.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_group_files.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_is_synced.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_ls.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_ls_stream_files.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_mount.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_obscure.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_rclone_config.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_read_write_text.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_remote_control.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_remotes.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_scan_missing_folders.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_serve_http.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_size_files.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_size_suffix.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tests/test_walk.py +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/tox.ini +0 -0
- {rclone_api-1.4.6 → rclone_api-1.4.8}/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,217 @@
|
|
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
|
+
|
120
|
+
print(parts_dir)
|
121
|
+
print(source_keys)
|
122
|
+
|
123
|
+
parent_path = parts_dir.split(s3_bucket)[1]
|
124
|
+
if parent_path.startswith("/"):
|
125
|
+
parent_path = parent_path[1:]
|
126
|
+
|
127
|
+
first_part: int | None = info.first_part
|
128
|
+
last_part: int | None = info.last_part
|
129
|
+
size: SizeSuffix | None = info.size
|
130
|
+
|
131
|
+
assert first_part is not None
|
132
|
+
assert last_part is not None
|
133
|
+
assert size is not None
|
134
|
+
|
135
|
+
def _to_s3_key(name: str) -> str:
|
136
|
+
out = f"{parent_path}/{name}"
|
137
|
+
return out
|
138
|
+
|
139
|
+
# s3_keys: list[str] = [_to_s3_key(name=p) for p in source_keys]
|
140
|
+
parts: list[tuple[int, str]] = []
|
141
|
+
for i in range(first_part, last_part + 1):
|
142
|
+
part_name = f"part.{i:05d}"
|
143
|
+
s3_key = _to_s3_key(name=part_name)
|
144
|
+
parts.append((i, s3_key))
|
145
|
+
|
146
|
+
# for key in parts:
|
147
|
+
# print(key)
|
148
|
+
|
149
|
+
chunksize = info.chunksize
|
150
|
+
assert chunksize is not None
|
151
|
+
|
152
|
+
finish_multipart_upload_from_keys(
|
153
|
+
s3_client=s3_client,
|
154
|
+
source_bucket=s3_creds.bucket_name,
|
155
|
+
parts=parts,
|
156
|
+
destination_bucket=s3_creds.bucket_name,
|
157
|
+
destination_key=dst,
|
158
|
+
chunk_size=chunksize.as_int(),
|
159
|
+
final_size=size.as_int(),
|
160
|
+
retries=3,
|
161
|
+
)
|
162
|
+
|
163
|
+
if False:
|
164
|
+
print(finish_multipart_upload_from_keys)
|
165
|
+
print(s3_client)
|
166
|
+
print("done")
|
167
|
+
|
168
|
+
# def finish_multipart_upload_from_keys(
|
169
|
+
# s3_client: BaseClient,
|
170
|
+
# source_bucket: str,
|
171
|
+
# source_keys: list[str],
|
172
|
+
# destination_bucket: str,
|
173
|
+
# destination_key: str,
|
174
|
+
# chunk_size: int = 5 * 1024 * 1024, # 5MB default
|
175
|
+
# retries: int = 3,
|
176
|
+
# byte_ranges: list[str] | None = None,
|
177
|
+
|
178
|
+
# if False:
|
179
|
+
# finish_multipart_upload_from_keys(
|
180
|
+
# s3_client=s3_client,
|
181
|
+
# source_bucket="TODO",
|
182
|
+
# source_keys=[p.key for p in all_parts],
|
183
|
+
# destination_bucket=info.dst_bucket,
|
184
|
+
# destination_key=info.dst_key,
|
185
|
+
# chunk_size=5 * 1024 * 1024,
|
186
|
+
# retries=3,
|
187
|
+
# byte_ranges=None,
|
188
|
+
# )
|
189
|
+
|
190
|
+
# print(all_parts)
|
191
|
+
|
192
|
+
|
193
|
+
def main() -> int:
|
194
|
+
"""Main entry point."""
|
195
|
+
args = _parse_args()
|
196
|
+
rclone = Rclone(rclone_conf=args.config_path)
|
197
|
+
info_json = f"{args.src}/info.json".replace("//", "/")
|
198
|
+
info = InfoJson(rclone.impl, src=None, src_info=info_json)
|
199
|
+
loaded = info.load()
|
200
|
+
assert loaded
|
201
|
+
print(info)
|
202
|
+
do_finish_part(rclone=rclone, info=info, dst=args.dst)
|
203
|
+
return 0
|
204
|
+
|
205
|
+
|
206
|
+
if __name__ == "__main__":
|
207
|
+
import sys
|
208
|
+
|
209
|
+
sys.argv.append("--config")
|
210
|
+
sys.argv.append("rclone.conf")
|
211
|
+
sys.argv.append(
|
212
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst-parts/"
|
213
|
+
)
|
214
|
+
sys.argv.append(
|
215
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst"
|
216
|
+
)
|
217
|
+
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,60 @@ 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
|
+
def compute_all_parts(self) -> list[PartInfo] | Exception:
|
215
|
+
# full_part_infos: list[PartInfo] | Exception = PartInfo.split_parts(
|
216
|
+
# src_size, SizeSuffix("96MB")
|
217
|
+
try:
|
218
|
+
|
219
|
+
src_size = self.size
|
220
|
+
chunk_size = self.chunksize
|
221
|
+
assert isinstance(src_size, SizeSuffix)
|
222
|
+
assert isinstance(chunk_size, SizeSuffix)
|
223
|
+
first_part = self.data["first_part"]
|
224
|
+
last_part = self.data["last_part"]
|
225
|
+
full_part_infos: list[PartInfo] = PartInfo.split_parts(src_size, chunk_size)
|
226
|
+
return full_part_infos[first_part : last_part + 1]
|
227
|
+
except Exception as e:
|
228
|
+
return e
|
229
|
+
|
230
|
+
def compute_all_part_numbers(self) -> list[int] | Exception:
|
231
|
+
all_parts: list[PartInfo] | Exception = self.compute_all_parts()
|
232
|
+
if isinstance(all_parts, Exception):
|
233
|
+
raise all_parts
|
234
|
+
|
235
|
+
all_part_nums: list[int] = [p.part_number for p in all_parts]
|
236
|
+
return all_part_nums
|
237
|
+
|
238
|
+
def fetch_remaining_part_numbers(self) -> list[int] | Exception:
|
239
|
+
all_part_nums: list[int] | Exception = self.compute_all_part_numbers()
|
240
|
+
if isinstance(all_part_nums, Exception):
|
241
|
+
return all_part_nums
|
242
|
+
finished_part_nums: list[int] = self.fetch_all_finished_part_numbers()
|
243
|
+
remaining_part_nums: list[int] = list(
|
244
|
+
set(all_part_nums) - set(finished_part_nums)
|
245
|
+
)
|
246
|
+
return sorted(remaining_part_nums)
|
247
|
+
|
248
|
+
def fetch_is_done(self) -> bool:
|
249
|
+
remaining_part_nums: list[int] | Exception = self.fetch_remaining_part_numbers()
|
250
|
+
if isinstance(remaining_part_nums, Exception):
|
251
|
+
return False
|
252
|
+
return len(remaining_part_nums) == 0
|
253
|
+
|
197
254
|
@property
|
198
255
|
def new(self) -> bool:
|
199
256
|
return self.data.get("new", False)
|
200
257
|
|
201
258
|
@property
|
202
259
|
def chunksize(self) -> SizeSuffix | None:
|
203
|
-
|
204
|
-
if
|
260
|
+
chunksize_int: int | None = self.data.get("chunksize_int")
|
261
|
+
if chunksize_int is None:
|
205
262
|
return None
|
206
|
-
return SizeSuffix(
|
263
|
+
return SizeSuffix(chunksize_int)
|
207
264
|
|
208
265
|
@chunksize.setter
|
209
266
|
def chunksize(self, value: SizeSuffix) -> None:
|
@@ -219,21 +276,24 @@ class InfoJson:
|
|
219
276
|
self.data["src_modtime"] = value.isoformat()
|
220
277
|
|
221
278
|
@property
|
222
|
-
def
|
279
|
+
def size(self) -> SizeSuffix:
|
280
|
+
return SizeSuffix(self.data["size"])
|
281
|
+
|
282
|
+
def _get_first_part(self) -> int | None:
|
223
283
|
return self.data.get("first_part")
|
224
284
|
|
225
|
-
|
226
|
-
def first_part(self, value: int) -> None:
|
285
|
+
def _set_first_part(self, value: int) -> None:
|
227
286
|
self.data["first_part"] = value
|
228
287
|
|
229
|
-
|
230
|
-
def last_part(self) -> int | None:
|
288
|
+
def _get_last_part(self) -> int | None:
|
231
289
|
return self.data.get("last_part")
|
232
290
|
|
233
|
-
|
234
|
-
def last_part(self, value: int) -> None:
|
291
|
+
def _set_last_part(self, value: int) -> None:
|
235
292
|
self.data["last_part"] = value
|
236
293
|
|
294
|
+
first_part: int | None = property(_get_first_part, _set_first_part) # type: ignore
|
295
|
+
last_part: int | None = property(_get_last_part, _set_last_part) # type: ignore
|
296
|
+
|
237
297
|
@property
|
238
298
|
def hash(self) -> str | None:
|
239
299
|
return self.data.get("hash")
|
@@ -248,6 +308,36 @@ class InfoJson:
|
|
248
308
|
return self.to_json_str()
|
249
309
|
|
250
310
|
|
311
|
+
def collapse_runs(numbers: list[int]) -> list[str]:
|
312
|
+
if not numbers:
|
313
|
+
return []
|
314
|
+
|
315
|
+
runs = []
|
316
|
+
start = numbers[0]
|
317
|
+
prev = numbers[0]
|
318
|
+
|
319
|
+
for num in numbers[1:]:
|
320
|
+
if num == prev + 1:
|
321
|
+
# Continue current run
|
322
|
+
prev = num
|
323
|
+
else:
|
324
|
+
# End current run
|
325
|
+
if start == prev:
|
326
|
+
runs.append(str(start))
|
327
|
+
else:
|
328
|
+
runs.append(f"{start}-{prev}")
|
329
|
+
start = num
|
330
|
+
prev = num
|
331
|
+
|
332
|
+
# Append the final run
|
333
|
+
if start == prev:
|
334
|
+
runs.append(str(start))
|
335
|
+
else:
|
336
|
+
runs.append(f"{start}-{prev}")
|
337
|
+
|
338
|
+
return runs
|
339
|
+
|
340
|
+
|
251
341
|
def copy_file_parts(
|
252
342
|
self: RcloneImpl,
|
253
343
|
src: str, # src:/Bucket/path/myfile.large.zst
|
@@ -297,7 +387,9 @@ def copy_file_parts(
|
|
297
387
|
first_part_number = part_infos[0].part_number
|
298
388
|
last_part_number = part_infos[-1].part_number
|
299
389
|
|
300
|
-
print(
|
390
|
+
print(
|
391
|
+
f"all_numbers_already_done: {collapse_runs(sorted(list(all_numbers_already_done)))}"
|
392
|
+
)
|
301
393
|
|
302
394
|
filtered_part_infos: list[PartInfo] = []
|
303
395
|
for part_info in part_infos:
|
@@ -306,7 +398,7 @@ def copy_file_parts(
|
|
306
398
|
part_infos = filtered_part_infos
|
307
399
|
|
308
400
|
remaining_part_numbers: list[int] = [p.part_number for p in part_infos]
|
309
|
-
print(f"remaining_part_numbers: {remaining_part_numbers}")
|
401
|
+
print(f"remaining_part_numbers: {collapse_runs(remaining_part_numbers)}")
|
310
402
|
|
311
403
|
if len(part_infos) == 0:
|
312
404
|
return Exception(f"No parts to copy for {src}")
|
@@ -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(
|
@@ -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
|