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.
Files changed (61) hide show
  1. rclone_api/__init__.py +951 -0
  2. rclone_api/assets/example.txt +1 -0
  3. rclone_api/cli.py +15 -0
  4. rclone_api/cmd/analyze.py +51 -0
  5. rclone_api/cmd/copy_large_s3.py +111 -0
  6. rclone_api/cmd/copy_large_s3_finish.py +81 -0
  7. rclone_api/cmd/list_files.py +27 -0
  8. rclone_api/cmd/save_to_db.py +77 -0
  9. rclone_api/completed_process.py +60 -0
  10. rclone_api/config.py +87 -0
  11. rclone_api/convert.py +31 -0
  12. rclone_api/db/__init__.py +3 -0
  13. rclone_api/db/db.py +277 -0
  14. rclone_api/db/models.py +57 -0
  15. rclone_api/deprecated.py +24 -0
  16. rclone_api/detail/copy_file_parts_resumable.py +42 -0
  17. rclone_api/detail/walk.py +116 -0
  18. rclone_api/diff.py +164 -0
  19. rclone_api/dir.py +113 -0
  20. rclone_api/dir_listing.py +66 -0
  21. rclone_api/exec.py +40 -0
  22. rclone_api/experimental/flags.py +89 -0
  23. rclone_api/experimental/flags_base.py +58 -0
  24. rclone_api/file.py +205 -0
  25. rclone_api/file_item.py +68 -0
  26. rclone_api/file_part.py +198 -0
  27. rclone_api/file_stream.py +52 -0
  28. rclone_api/filelist.py +30 -0
  29. rclone_api/group_files.py +256 -0
  30. rclone_api/http_server.py +244 -0
  31. rclone_api/install.py +95 -0
  32. rclone_api/log.py +44 -0
  33. rclone_api/mount.py +55 -0
  34. rclone_api/mount_util.py +247 -0
  35. rclone_api/process.py +187 -0
  36. rclone_api/rclone_impl.py +1285 -0
  37. rclone_api/remote.py +21 -0
  38. rclone_api/rpath.py +102 -0
  39. rclone_api/s3/api.py +109 -0
  40. rclone_api/s3/basic_ops.py +61 -0
  41. rclone_api/s3/chunk_task.py +187 -0
  42. rclone_api/s3/create.py +107 -0
  43. rclone_api/s3/multipart/file_info.py +7 -0
  44. rclone_api/s3/multipart/finished_piece.py +69 -0
  45. rclone_api/s3/multipart/info_json.py +239 -0
  46. rclone_api/s3/multipart/merge_state.py +147 -0
  47. rclone_api/s3/multipart/upload_info.py +62 -0
  48. rclone_api/s3/multipart/upload_parts_inline.py +356 -0
  49. rclone_api/s3/multipart/upload_parts_resumable.py +304 -0
  50. rclone_api/s3/multipart/upload_parts_server_side_merge.py +546 -0
  51. rclone_api/s3/multipart/upload_state.py +165 -0
  52. rclone_api/s3/types.py +67 -0
  53. rclone_api/scan_missing_folders.py +153 -0
  54. rclone_api/types.py +402 -0
  55. rclone_api/util.py +324 -0
  56. rclone_api-1.5.8.dist-info/LICENSE +21 -0
  57. rclone_api-1.5.8.dist-info/METADATA +969 -0
  58. rclone_api-1.5.8.dist-info/RECORD +61 -0
  59. rclone_api-1.5.8.dist-info/WHEEL +5 -0
  60. rclone_api-1.5.8.dist-info/entry_points.txt +5 -0
  61. 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.