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.
Files changed (107) hide show
  1. {rclone_api-1.4.7 → rclone_api-1.4.9}/PKG-INFO +1 -1
  2. {rclone_api-1.4.7 → rclone_api-1.4.9}/pyproject.toml +1 -1
  3. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cmd/copy_large_s3.py +5 -13
  4. rclone_api-1.4.9/src/rclone_api/cmd/copy_large_s3_finish.py +228 -0
  5. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/detail/copy_file_parts.py +86 -15
  6. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/process.py +6 -14
  7. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/rclone_impl.py +61 -44
  8. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/create.py +1 -1
  9. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/multipart/finished_piece.py +4 -1
  10. rclone_api-1.4.9/src/rclone_api/s3/s3_multipart_uploader_by_copy.py +419 -0
  11. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/types.py +1 -0
  12. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/types.py +13 -0
  13. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/util.py +52 -9
  14. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/PKG-INFO +1 -1
  15. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/SOURCES.txt +2 -1
  16. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_diff.py +2 -2
  17. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_s3.py +1 -0
  18. rclone_api-1.4.7/src/rclone_api/s3/s3_multipart_uploader.py +0 -138
  19. {rclone_api-1.4.7 → rclone_api-1.4.9}/.aiderignore +0 -0
  20. {rclone_api-1.4.7 → rclone_api-1.4.9}/.github/workflows/lint.yml +0 -0
  21. {rclone_api-1.4.7 → rclone_api-1.4.9}/.github/workflows/push_macos.yml +0 -0
  22. {rclone_api-1.4.7 → rclone_api-1.4.9}/.github/workflows/push_ubuntu.yml +0 -0
  23. {rclone_api-1.4.7 → rclone_api-1.4.9}/.github/workflows/push_win.yml +0 -0
  24. {rclone_api-1.4.7 → rclone_api-1.4.9}/.gitignore +0 -0
  25. {rclone_api-1.4.7 → rclone_api-1.4.9}/.pylintrc +0 -0
  26. {rclone_api-1.4.7 → rclone_api-1.4.9}/.vscode/launch.json +0 -0
  27. {rclone_api-1.4.7 → rclone_api-1.4.9}/.vscode/settings.json +0 -0
  28. {rclone_api-1.4.7 → rclone_api-1.4.9}/.vscode/tasks.json +0 -0
  29. {rclone_api-1.4.7 → rclone_api-1.4.9}/LICENSE +0 -0
  30. {rclone_api-1.4.7 → rclone_api-1.4.9}/MANIFEST.in +0 -0
  31. {rclone_api-1.4.7 → rclone_api-1.4.9}/README.md +0 -0
  32. {rclone_api-1.4.7 → rclone_api-1.4.9}/clean +0 -0
  33. {rclone_api-1.4.7 → rclone_api-1.4.9}/install +0 -0
  34. {rclone_api-1.4.7 → rclone_api-1.4.9}/lint +0 -0
  35. {rclone_api-1.4.7 → rclone_api-1.4.9}/requirements.testing.txt +0 -0
  36. {rclone_api-1.4.7 → rclone_api-1.4.9}/setup.cfg +0 -0
  37. {rclone_api-1.4.7 → rclone_api-1.4.9}/setup.py +0 -0
  38. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/__init__.py +0 -0
  39. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/assets/example.txt +0 -0
  40. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cli.py +0 -0
  41. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cmd/analyze.py +0 -0
  42. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cmd/list_files.py +0 -0
  43. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/cmd/save_to_db.py +0 -0
  44. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/completed_process.py +0 -0
  45. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/config.py +0 -0
  46. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/convert.py +0 -0
  47. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/db/__init__.py +0 -0
  48. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/db/db.py +0 -0
  49. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/db/models.py +0 -0
  50. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/deprecated.py +0 -0
  51. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/detail/walk.py +0 -0
  52. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/diff.py +0 -0
  53. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/dir.py +0 -0
  54. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/dir_listing.py +0 -0
  55. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/exec.py +0 -0
  56. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/experimental/flags.py +0 -0
  57. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/experimental/flags_base.py +0 -0
  58. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/file.py +0 -0
  59. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/file_item.py +0 -0
  60. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/file_part.py +0 -0
  61. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/file_stream.py +0 -0
  62. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/filelist.py +0 -0
  63. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/group_files.py +0 -0
  64. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/http_server.py +0 -0
  65. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/log.py +0 -0
  66. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/mount.py +0 -0
  67. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/remote.py +0 -0
  68. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/rpath.py +0 -0
  69. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/api.py +0 -0
  70. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/basic_ops.py +0 -0
  71. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/chunk_task.py +0 -0
  72. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/multipart/file_info.py +0 -0
  73. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/multipart/upload_info.py +0 -0
  74. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/multipart/upload_state.py +0 -0
  75. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/s3/upload_file_multipart.py +0 -0
  76. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api/scan_missing_folders.py +0 -0
  77. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  78. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/entry_points.txt +0 -0
  79. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/requires.txt +0 -0
  80. {rclone_api-1.4.7 → rclone_api-1.4.9}/src/rclone_api.egg-info/top_level.txt +0 -0
  81. {rclone_api-1.4.7 → rclone_api-1.4.9}/test +0 -0
  82. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/archive/test_paramiko.py.disabled +0 -0
  83. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_cmd_list_files.py +0 -0
  84. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_copy.py +0 -0
  85. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_copy_bytes.py +0 -0
  86. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_copy_file_resumable_s3.py +0 -0
  87. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_copy_files.py +0 -0
  88. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_db.py +0 -0
  89. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_file_item.py +0 -0
  90. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_group_files.py +0 -0
  91. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_is_synced.py +0 -0
  92. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_ls.py +0 -0
  93. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_ls_stream_files.py +0 -0
  94. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_mount.py +0 -0
  95. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_mount_s3.py +0 -0
  96. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_obscure.py +0 -0
  97. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_rclone_config.py +0 -0
  98. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_read_write_text.py +0 -0
  99. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_remote_control.py +0 -0
  100. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_remotes.py +0 -0
  101. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_scan_missing_folders.py +0 -0
  102. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_serve_http.py +0 -0
  103. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_size_files.py +0 -0
  104. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_size_suffix.py +0 -0
  105. {rclone_api-1.4.7 → rclone_api-1.4.9}/tests/test_walk.py +0 -0
  106. {rclone_api-1.4.7 → rclone_api-1.4.9}/tox.ini +0 -0
  107. {rclone_api-1.4.7 → rclone_api-1.4.9}/upload_package.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.4.7
3
+ Version: 1.4.9
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -25,7 +25,7 @@ dependencies = [
25
25
  ]
26
26
 
27
27
  # Change this with the version number bump.
28
- version = "1.4.7"
28
+ version = "1.4.9"
29
29
 
30
30
  [tool.setuptools]
31
31
  package-dir = {"" = "src"}
@@ -11,8 +11,7 @@ class Args:
11
11
  src: str
12
12
  dst: str
13
13
  chunk_size: SizeSuffix
14
- read_threads: int
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
- "--read-threads",
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=16,
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: str = text_or_err
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
- chunksize: str | None = self.data.get("chunksize")
204
- if chunksize is None:
271
+ chunksize_int: int | None = self.data.get("chunksize_int")
272
+ if chunksize_int is None:
205
273
  return None
206
- return SizeSuffix(chunksize)
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 first_part(self) -> int | None:
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
- @first_part.setter
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
- @property
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
- @last_part.setter
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.tempdir: TemporaryDirectory | None = None
30
+ self.tempfile: Path | None = None
32
31
  verbose = get_verbose(args.verbose)
33
32
  if isinstance(args.rclone_conf, Config):
34
- self.tempdir = TemporaryDirectory()
35
- tmpfile = Path(self.tempdir.name) / "rclone.conf"
36
- tmpfile.write_text(args.rclone_conf.text, encoding="utf-8")
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
- if self.tempdir and self.needs_cleanup:
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
- parsed: Parsed = self.config.parse()
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