rclone-api 1.5.8__py3-none-any.whl
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/__init__.py +951 -0
- rclone_api/assets/example.txt +1 -0
- rclone_api/cli.py +15 -0
- rclone_api/cmd/analyze.py +51 -0
- rclone_api/cmd/copy_large_s3.py +111 -0
- rclone_api/cmd/copy_large_s3_finish.py +81 -0
- rclone_api/cmd/list_files.py +27 -0
- rclone_api/cmd/save_to_db.py +77 -0
- rclone_api/completed_process.py +60 -0
- rclone_api/config.py +87 -0
- rclone_api/convert.py +31 -0
- rclone_api/db/__init__.py +3 -0
- rclone_api/db/db.py +277 -0
- rclone_api/db/models.py +57 -0
- rclone_api/deprecated.py +24 -0
- rclone_api/detail/copy_file_parts_resumable.py +42 -0
- rclone_api/detail/walk.py +116 -0
- rclone_api/diff.py +164 -0
- rclone_api/dir.py +113 -0
- rclone_api/dir_listing.py +66 -0
- rclone_api/exec.py +40 -0
- rclone_api/experimental/flags.py +89 -0
- rclone_api/experimental/flags_base.py +58 -0
- rclone_api/file.py +205 -0
- rclone_api/file_item.py +68 -0
- rclone_api/file_part.py +198 -0
- rclone_api/file_stream.py +52 -0
- rclone_api/filelist.py +30 -0
- rclone_api/group_files.py +256 -0
- rclone_api/http_server.py +244 -0
- rclone_api/install.py +95 -0
- rclone_api/log.py +44 -0
- rclone_api/mount.py +55 -0
- rclone_api/mount_util.py +247 -0
- rclone_api/process.py +187 -0
- rclone_api/rclone_impl.py +1285 -0
- rclone_api/remote.py +21 -0
- rclone_api/rpath.py +102 -0
- rclone_api/s3/api.py +109 -0
- rclone_api/s3/basic_ops.py +61 -0
- rclone_api/s3/chunk_task.py +187 -0
- rclone_api/s3/create.py +107 -0
- rclone_api/s3/multipart/file_info.py +7 -0
- rclone_api/s3/multipart/finished_piece.py +69 -0
- rclone_api/s3/multipart/info_json.py +239 -0
- rclone_api/s3/multipart/merge_state.py +147 -0
- rclone_api/s3/multipart/upload_info.py +62 -0
- rclone_api/s3/multipart/upload_parts_inline.py +356 -0
- rclone_api/s3/multipart/upload_parts_resumable.py +304 -0
- rclone_api/s3/multipart/upload_parts_server_side_merge.py +546 -0
- rclone_api/s3/multipart/upload_state.py +165 -0
- rclone_api/s3/types.py +67 -0
- rclone_api/scan_missing_folders.py +153 -0
- rclone_api/types.py +402 -0
- rclone_api/util.py +324 -0
- rclone_api-1.5.8.dist-info/LICENSE +21 -0
- rclone_api-1.5.8.dist-info/METADATA +969 -0
- rclone_api-1.5.8.dist-info/RECORD +61 -0
- rclone_api-1.5.8.dist-info/WHEEL +5 -0
- rclone_api-1.5.8.dist-info/entry_points.txt +5 -0
- rclone_api-1.5.8.dist-info/top_level.txt +1 -0
rclone_api/util.py
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
import atexit
|
2
|
+
import os
|
3
|
+
import platform
|
4
|
+
import random
|
5
|
+
import shutil
|
6
|
+
import signal
|
7
|
+
import subprocess
|
8
|
+
import warnings
|
9
|
+
import weakref
|
10
|
+
from pathlib import Path
|
11
|
+
from threading import Lock
|
12
|
+
from typing import Any
|
13
|
+
|
14
|
+
import psutil
|
15
|
+
from appdirs import user_cache_dir
|
16
|
+
|
17
|
+
from rclone_api.config import Config
|
18
|
+
from rclone_api.dir import Dir
|
19
|
+
from rclone_api.install import rclone_download
|
20
|
+
from rclone_api.remote import Remote
|
21
|
+
from rclone_api.rpath import RPath
|
22
|
+
from rclone_api.types import S3PathInfo
|
23
|
+
|
24
|
+
# from .rclone import Rclone
|
25
|
+
|
26
|
+
_PRINT_LOCK = Lock()
|
27
|
+
|
28
|
+
_TMP_CONFIG_DIR = Path(".") / ".rclone" / "tmp_config"
|
29
|
+
_RCLONE_CONFIGS_LIST: list[Path] = []
|
30
|
+
_DO_CLEANUP = os.getenv("RCLONE_API_CLEANUP", "1") == "1"
|
31
|
+
_CACHE_DIR = Path(user_cache_dir("rclone_api"))
|
32
|
+
|
33
|
+
_RCLONE_EXE = _CACHE_DIR / "rclone"
|
34
|
+
if platform.system() == "Windows":
|
35
|
+
_RCLONE_EXE = _RCLONE_EXE.with_suffix(".exe")
|
36
|
+
|
37
|
+
|
38
|
+
def _clean_configs(signum=None, frame=None) -> None:
|
39
|
+
if not _DO_CLEANUP:
|
40
|
+
return
|
41
|
+
for config in _RCLONE_CONFIGS_LIST:
|
42
|
+
try:
|
43
|
+
if config.exists():
|
44
|
+
if config.is_dir():
|
45
|
+
shutil.rmtree(config, ignore_errors=True)
|
46
|
+
else:
|
47
|
+
config.unlink()
|
48
|
+
except Exception as e:
|
49
|
+
print(f"Error deleting config file: {config}, {e}")
|
50
|
+
_RCLONE_CONFIGS_LIST.clear()
|
51
|
+
if signum is not None:
|
52
|
+
signal.signal(signum, signal.SIG_DFL)
|
53
|
+
os.kill(os.getpid(), signum)
|
54
|
+
|
55
|
+
|
56
|
+
def _init_cleanup() -> None:
|
57
|
+
atexit.register(_clean_configs)
|
58
|
+
|
59
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
60
|
+
signal.signal(sig, _clean_configs)
|
61
|
+
|
62
|
+
|
63
|
+
_init_cleanup()
|
64
|
+
|
65
|
+
|
66
|
+
def make_temp_config_file() -> Path:
|
67
|
+
from rclone_api.util import random_str
|
68
|
+
|
69
|
+
tmpdir = _TMP_CONFIG_DIR / random_str(32)
|
70
|
+
tmpdir.mkdir(parents=True, exist_ok=True)
|
71
|
+
tmpfile = tmpdir / "rclone.conf"
|
72
|
+
_RCLONE_CONFIGS_LIST.append(tmpdir)
|
73
|
+
return tmpfile
|
74
|
+
|
75
|
+
|
76
|
+
def clear_temp_config_file(path: Path | None) -> None:
|
77
|
+
if (path is None) or (not path.exists()) or (not _DO_CLEANUP):
|
78
|
+
return
|
79
|
+
try:
|
80
|
+
path.unlink()
|
81
|
+
except Exception as e:
|
82
|
+
print(f"Error deleting config file: {path}, {e}")
|
83
|
+
|
84
|
+
|
85
|
+
def locked_print(*args, **kwargs):
|
86
|
+
with _PRINT_LOCK:
|
87
|
+
print(*args, **kwargs)
|
88
|
+
|
89
|
+
|
90
|
+
def port_is_free(port: int) -> bool:
|
91
|
+
import socket
|
92
|
+
|
93
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
94
|
+
return s.connect_ex(("localhost", port)) != 0
|
95
|
+
|
96
|
+
|
97
|
+
def find_free_port() -> int:
|
98
|
+
tries = 20
|
99
|
+
port = random.randint(10000, 20000)
|
100
|
+
while tries > 0:
|
101
|
+
if port_is_free(port):
|
102
|
+
return port
|
103
|
+
tries -= 1
|
104
|
+
port = random.randint(10000, 20000)
|
105
|
+
warnings.warn(f"Failed to find a free port, so using {port}")
|
106
|
+
return port
|
107
|
+
|
108
|
+
|
109
|
+
def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
|
110
|
+
from rclone_api.rclone_impl import RcloneImpl
|
111
|
+
|
112
|
+
assert isinstance(rclone, RcloneImpl)
|
113
|
+
# if str then it will be remote:path
|
114
|
+
if isinstance(item, str):
|
115
|
+
# return RPath(item)
|
116
|
+
# remote_name: str = item.split(":")[0]
|
117
|
+
parts = item.split(":")
|
118
|
+
remote_name = parts[0]
|
119
|
+
path = ":".join(parts[1:])
|
120
|
+
remote = Remote(name=remote_name, rclone=rclone)
|
121
|
+
out = RPath(
|
122
|
+
remote=remote,
|
123
|
+
path=path,
|
124
|
+
name="",
|
125
|
+
size=0,
|
126
|
+
mime_type="",
|
127
|
+
mod_time="",
|
128
|
+
is_dir=True,
|
129
|
+
)
|
130
|
+
out.set_rclone(rclone)
|
131
|
+
return out
|
132
|
+
elif isinstance(item, Dir):
|
133
|
+
return item.path
|
134
|
+
elif isinstance(item, Remote):
|
135
|
+
out = RPath(
|
136
|
+
remote=item,
|
137
|
+
path=str(item),
|
138
|
+
name=str(item),
|
139
|
+
size=0,
|
140
|
+
mime_type="inode/directory",
|
141
|
+
mod_time="",
|
142
|
+
is_dir=True,
|
143
|
+
)
|
144
|
+
out.set_rclone(rclone)
|
145
|
+
return out
|
146
|
+
else:
|
147
|
+
raise ValueError(f"Invalid type for item: {type(item)}")
|
148
|
+
|
149
|
+
|
150
|
+
def get_verbose(verbose: bool | None) -> bool:
|
151
|
+
if verbose is not None:
|
152
|
+
return verbose
|
153
|
+
# get it from the environment
|
154
|
+
return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
|
155
|
+
|
156
|
+
|
157
|
+
def get_check(check: bool | None) -> bool:
|
158
|
+
if check is not None:
|
159
|
+
return check
|
160
|
+
# get it from the environment
|
161
|
+
return bool(int(os.getenv("RCLONE_API_CHECK", "1")))
|
162
|
+
|
163
|
+
|
164
|
+
def get_rclone_exe(rclone_exe: Path | None) -> Path:
|
165
|
+
if rclone_exe is None:
|
166
|
+
rclone_which_path = shutil.which("rclone")
|
167
|
+
if rclone_which_path is not None:
|
168
|
+
return Path(rclone_which_path)
|
169
|
+
rclone_download(out=_RCLONE_EXE, replace=False)
|
170
|
+
return _RCLONE_EXE
|
171
|
+
return rclone_exe
|
172
|
+
|
173
|
+
|
174
|
+
def upgrade_rclone() -> Path:
|
175
|
+
rclone_download(out=_RCLONE_EXE, replace=True)
|
176
|
+
return _RCLONE_EXE
|
177
|
+
|
178
|
+
|
179
|
+
def rclone_execute(
|
180
|
+
cmd: list[str],
|
181
|
+
rclone_conf: Path | Config,
|
182
|
+
rclone_exe: Path,
|
183
|
+
check: bool,
|
184
|
+
capture: bool | Path | None = None,
|
185
|
+
verbose: bool | None = None,
|
186
|
+
) -> subprocess.CompletedProcess:
|
187
|
+
tmpfile: Path | None = None
|
188
|
+
verbose = get_verbose(verbose)
|
189
|
+
|
190
|
+
# Handle the Path case for capture.
|
191
|
+
output_file: Path | None = None
|
192
|
+
if isinstance(capture, Path):
|
193
|
+
output_file = capture
|
194
|
+
capture = False # When redirecting to file, don't capture to memory.
|
195
|
+
else:
|
196
|
+
capture = capture if isinstance(capture, bool) else True
|
197
|
+
|
198
|
+
try:
|
199
|
+
# Create a temporary config file if needed.
|
200
|
+
if isinstance(rclone_conf, Config):
|
201
|
+
tmpfile = make_temp_config_file()
|
202
|
+
tmpfile.write_text(rclone_conf.text, encoding="utf-8")
|
203
|
+
rclone_conf = tmpfile
|
204
|
+
|
205
|
+
# Build the command line.
|
206
|
+
full_cmd = (
|
207
|
+
[str(rclone_exe.resolve())] + ["--config", str(rclone_conf.resolve())] + cmd
|
208
|
+
)
|
209
|
+
if verbose:
|
210
|
+
cmd_str = subprocess.list2cmdline(full_cmd)
|
211
|
+
print(f"\nRunning: {cmd_str}")
|
212
|
+
|
213
|
+
# Prepare subprocess parameters.
|
214
|
+
proc_kwargs: dict[str, Any] = {
|
215
|
+
"encoding": "utf-8",
|
216
|
+
"shell": False,
|
217
|
+
"stderr": subprocess.PIPE,
|
218
|
+
}
|
219
|
+
file_handle = None
|
220
|
+
if output_file:
|
221
|
+
# Open the file for writing.
|
222
|
+
file_handle = open(output_file, "w", encoding="utf-8")
|
223
|
+
proc_kwargs["stdout"] = file_handle
|
224
|
+
else:
|
225
|
+
proc_kwargs["stdout"] = subprocess.PIPE if capture else None
|
226
|
+
|
227
|
+
# Start the process.
|
228
|
+
process = subprocess.Popen(full_cmd, **proc_kwargs)
|
229
|
+
|
230
|
+
# Register an atexit callback that uses psutil to kill the process tree.
|
231
|
+
proc_ref = weakref.ref(process)
|
232
|
+
|
233
|
+
def cleanup():
|
234
|
+
proc = proc_ref()
|
235
|
+
if proc is None:
|
236
|
+
return
|
237
|
+
try:
|
238
|
+
parent = psutil.Process(proc.pid)
|
239
|
+
except psutil.NoSuchProcess:
|
240
|
+
return
|
241
|
+
# Terminate all child processes first.
|
242
|
+
children = parent.children(recursive=True)
|
243
|
+
if children:
|
244
|
+
print(f"Terminating {len(children)} child process(es)...")
|
245
|
+
for child in children:
|
246
|
+
try:
|
247
|
+
child.terminate()
|
248
|
+
except Exception as e:
|
249
|
+
print(f"Error terminating child {child.pid}: {e}")
|
250
|
+
psutil.wait_procs(children, timeout=2)
|
251
|
+
for child in children:
|
252
|
+
if child.is_running():
|
253
|
+
try:
|
254
|
+
child.kill()
|
255
|
+
except Exception as e:
|
256
|
+
print(f"Error killing child {child.pid}: {e}")
|
257
|
+
# Now terminate the parent process.
|
258
|
+
if parent.is_running():
|
259
|
+
try:
|
260
|
+
parent.terminate()
|
261
|
+
parent.wait(timeout=3)
|
262
|
+
except (psutil.TimeoutExpired, Exception):
|
263
|
+
try:
|
264
|
+
parent.kill()
|
265
|
+
except Exception as e:
|
266
|
+
print(f"Error killing process {parent.pid}: {e}")
|
267
|
+
|
268
|
+
atexit.register(cleanup)
|
269
|
+
|
270
|
+
# Wait for the process to complete.
|
271
|
+
out, err = process.communicate()
|
272
|
+
# Close the file handle if used.
|
273
|
+
if file_handle:
|
274
|
+
file_handle.close()
|
275
|
+
|
276
|
+
cp: subprocess.CompletedProcess = subprocess.CompletedProcess(
|
277
|
+
args=full_cmd,
|
278
|
+
returncode=process.returncode,
|
279
|
+
stdout=out,
|
280
|
+
stderr=err,
|
281
|
+
)
|
282
|
+
|
283
|
+
# Warn or raise if return code is non-zero.
|
284
|
+
if cp.returncode != 0:
|
285
|
+
cmd_str = subprocess.list2cmdline(full_cmd)
|
286
|
+
warnings.warn(
|
287
|
+
f"Error running: {cmd_str}, returncode: {cp.returncode}\n"
|
288
|
+
f"{cp.stdout}\n{cp.stderr}"
|
289
|
+
)
|
290
|
+
if check:
|
291
|
+
raise subprocess.CalledProcessError(
|
292
|
+
cp.returncode, full_cmd, cp.stdout, cp.stderr
|
293
|
+
)
|
294
|
+
return cp
|
295
|
+
finally:
|
296
|
+
clear_temp_config_file(tmpfile)
|
297
|
+
|
298
|
+
|
299
|
+
def split_s3_path(path: str) -> S3PathInfo:
|
300
|
+
if ":" not in path:
|
301
|
+
raise ValueError(f"Invalid S3 path: {path}")
|
302
|
+
|
303
|
+
prts = path.split(":", 1)
|
304
|
+
remote = prts[0]
|
305
|
+
path = prts[1]
|
306
|
+
parts: list[str] = []
|
307
|
+
for part in path.split("/"):
|
308
|
+
part = part.strip()
|
309
|
+
if part:
|
310
|
+
parts.append(part)
|
311
|
+
if len(parts) < 2:
|
312
|
+
raise ValueError(f"Invalid S3 path: {path}")
|
313
|
+
bucket = parts[0]
|
314
|
+
key = "/".join(parts[1:])
|
315
|
+
assert bucket
|
316
|
+
assert key
|
317
|
+
return S3PathInfo(remote=remote, bucket=bucket, key=key)
|
318
|
+
|
319
|
+
|
320
|
+
def random_str(length: int) -> str:
|
321
|
+
import random
|
322
|
+
import string
|
323
|
+
|
324
|
+
return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 zackees
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|