rclone-api 1.4.32__py2.py3-none-any.whl → 1.5.0__py2.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/util.py CHANGED
@@ -1,307 +1,315 @@
1
- import atexit
2
- import os
3
- import random
4
- import shutil
5
- import signal
6
- import subprocess
7
- import warnings
8
- import weakref
9
- from pathlib import Path
10
- from threading import Lock
11
- from typing import Any
12
-
13
- import psutil
14
-
15
- from rclone_api.config import Config
16
- from rclone_api.dir import Dir
17
- from rclone_api.remote import Remote
18
- from rclone_api.rpath import RPath
19
- from rclone_api.types import S3PathInfo
20
-
21
- # from .rclone import Rclone
22
-
23
- _PRINT_LOCK = Lock()
24
-
25
- _TMP_CONFIG_DIR = Path(".") / ".rclone" / "tmp_config"
26
- _RCLONE_CONFIGS_LIST: list[Path] = []
27
- _DO_CLEANUP = os.getenv("RCLONE_API_CLEANUP", "1") == "1"
28
-
29
-
30
- def _clean_configs(signum=None, frame=None) -> None:
31
- if not _DO_CLEANUP:
32
- return
33
- for config in _RCLONE_CONFIGS_LIST:
34
- try:
35
- config.unlink()
36
- except Exception as e:
37
- print(f"Error deleting config file: {config}, {e}")
38
- _RCLONE_CONFIGS_LIST.clear()
39
- if signum is not None:
40
- signal.signal(signum, signal.SIG_DFL)
41
- os.kill(os.getpid(), signum)
42
-
43
-
44
- def _init_cleanup() -> None:
45
- atexit.register(_clean_configs)
46
-
47
- for sig in (signal.SIGINT, signal.SIGTERM):
48
- signal.signal(sig, _clean_configs)
49
-
50
-
51
- _init_cleanup()
52
-
53
-
54
- def make_temp_config_file() -> Path:
55
- from rclone_api.util import random_str
56
-
57
- tmpdir = _TMP_CONFIG_DIR / random_str(32)
58
- tmpdir.mkdir(parents=True, exist_ok=True)
59
- tmpfile = tmpdir / "rclone.conf"
60
- _RCLONE_CONFIGS_LIST.append(tmpfile)
61
- return tmpfile
62
-
63
-
64
- def clear_temp_config_file(path: Path | None) -> None:
65
- if (path is None) or (not path.exists()) or (not _DO_CLEANUP):
66
- return
67
- try:
68
- path.unlink()
69
- except Exception as e:
70
- print(f"Error deleting config file: {path}, {e}")
71
-
72
-
73
- def locked_print(*args, **kwargs):
74
- with _PRINT_LOCK:
75
- print(*args, **kwargs)
76
-
77
-
78
- def port_is_free(port: int) -> bool:
79
- import socket
80
-
81
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
82
- return s.connect_ex(("localhost", port)) != 0
83
-
84
-
85
- def find_free_port() -> int:
86
- tries = 20
87
- port = random.randint(10000, 20000)
88
- while tries > 0:
89
- if port_is_free(port):
90
- return port
91
- tries -= 1
92
- port = random.randint(10000, 20000)
93
- warnings.warn(f"Failed to find a free port, so using {port}")
94
- return port
95
-
96
-
97
- def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
98
- from rclone_api.rclone_impl import RcloneImpl
99
-
100
- assert isinstance(rclone, RcloneImpl)
101
- # if str then it will be remote:path
102
- if isinstance(item, str):
103
- # return RPath(item)
104
- # remote_name: str = item.split(":")[0]
105
- parts = item.split(":")
106
- remote_name = parts[0]
107
- path = ":".join(parts[1:])
108
- remote = Remote(name=remote_name, rclone=rclone)
109
- out = RPath(
110
- remote=remote,
111
- path=path,
112
- name="",
113
- size=0,
114
- mime_type="",
115
- mod_time="",
116
- is_dir=True,
117
- )
118
- out.set_rclone(rclone)
119
- return out
120
- elif isinstance(item, Dir):
121
- return item.path
122
- elif isinstance(item, Remote):
123
- out = RPath(
124
- remote=item,
125
- path=str(item),
126
- name=str(item),
127
- size=0,
128
- mime_type="inode/directory",
129
- mod_time="",
130
- is_dir=True,
131
- )
132
- out.set_rclone(rclone)
133
- return out
134
- else:
135
- raise ValueError(f"Invalid type for item: {type(item)}")
136
-
137
-
138
- def get_verbose(verbose: bool | None) -> bool:
139
- if verbose is not None:
140
- return verbose
141
- # get it from the environment
142
- return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
143
-
144
-
145
- def get_check(check: bool | None) -> bool:
146
- if check is not None:
147
- return check
148
- # get it from the environment
149
- return bool(int(os.getenv("RCLONE_API_CHECK", "1")))
150
-
151
-
152
- def get_rclone_exe(rclone_exe: Path | None) -> Path:
153
- if rclone_exe is None:
154
-
155
- rclone_which_path = shutil.which("rclone")
156
- if rclone_which_path is None:
157
- raise ValueError("rclone executable not found")
158
- return Path(rclone_which_path)
159
- return rclone_exe
160
-
161
-
162
- def rclone_execute(
163
- cmd: list[str],
164
- rclone_conf: Path | Config,
165
- rclone_exe: Path,
166
- check: bool,
167
- capture: bool | Path | None = None,
168
- verbose: bool | None = None,
169
- ) -> subprocess.CompletedProcess:
170
- tmpfile: Path | None = None
171
- verbose = get_verbose(verbose)
172
-
173
- # Handle the Path case for capture.
174
- output_file: Path | None = None
175
- if isinstance(capture, Path):
176
- output_file = capture
177
- capture = False # When redirecting to file, don't capture to memory.
178
- else:
179
- capture = capture if isinstance(capture, bool) else True
180
-
181
- try:
182
- # Create a temporary config file if needed.
183
- if isinstance(rclone_conf, Config):
184
- tmpfile = make_temp_config_file()
185
- tmpfile.write_text(rclone_conf.text, encoding="utf-8")
186
- rclone_conf = tmpfile
187
-
188
- # Build the command line.
189
- full_cmd = (
190
- [str(rclone_exe.resolve())] + ["--config", str(rclone_conf.resolve())] + cmd
191
- )
192
- if verbose:
193
- cmd_str = subprocess.list2cmdline(full_cmd)
194
- print(f"\nRunning: {cmd_str}")
195
-
196
- # Prepare subprocess parameters.
197
- proc_kwargs: dict[str, Any] = {
198
- "encoding": "utf-8",
199
- "shell": False,
200
- "stderr": subprocess.PIPE,
201
- }
202
- file_handle = None
203
- if output_file:
204
- # Open the file for writing.
205
- file_handle = open(output_file, "w", encoding="utf-8")
206
- proc_kwargs["stdout"] = file_handle
207
- else:
208
- proc_kwargs["stdout"] = subprocess.PIPE if capture else None
209
-
210
- # Start the process.
211
- process = subprocess.Popen(full_cmd, **proc_kwargs)
212
-
213
- # Register an atexit callback that uses psutil to kill the process tree.
214
- proc_ref = weakref.ref(process)
215
-
216
- def cleanup():
217
- proc = proc_ref()
218
- if proc is None:
219
- return
220
- try:
221
- parent = psutil.Process(proc.pid)
222
- except psutil.NoSuchProcess:
223
- return
224
- # Terminate all child processes first.
225
- children = parent.children(recursive=True)
226
- if children:
227
- print(f"Terminating {len(children)} child process(es)...")
228
- for child in children:
229
- try:
230
- child.terminate()
231
- except Exception as e:
232
- print(f"Error terminating child {child.pid}: {e}")
233
- psutil.wait_procs(children, timeout=2)
234
- for child in children:
235
- if child.is_running():
236
- try:
237
- child.kill()
238
- except Exception as e:
239
- print(f"Error killing child {child.pid}: {e}")
240
- # Now terminate the parent process.
241
- if parent.is_running():
242
- try:
243
- parent.terminate()
244
- parent.wait(timeout=3)
245
- except (psutil.TimeoutExpired, Exception):
246
- try:
247
- parent.kill()
248
- except Exception as e:
249
- print(f"Error killing process {parent.pid}: {e}")
250
-
251
- atexit.register(cleanup)
252
-
253
- # Wait for the process to complete.
254
- out, err = process.communicate()
255
- # Close the file handle if used.
256
- if file_handle:
257
- file_handle.close()
258
-
259
- cp: subprocess.CompletedProcess = subprocess.CompletedProcess(
260
- args=full_cmd,
261
- returncode=process.returncode,
262
- stdout=out,
263
- stderr=err,
264
- )
265
-
266
- # Warn or raise if return code is non-zero.
267
- if cp.returncode != 0:
268
- cmd_str = subprocess.list2cmdline(full_cmd)
269
- warnings.warn(
270
- f"Error running: {cmd_str}, returncode: {cp.returncode}\n"
271
- f"{cp.stdout}\n{cp.stderr}"
272
- )
273
- if check:
274
- raise subprocess.CalledProcessError(
275
- cp.returncode, full_cmd, cp.stdout, cp.stderr
276
- )
277
- return cp
278
- finally:
279
- clear_temp_config_file(tmpfile)
280
-
281
-
282
- def split_s3_path(path: str) -> S3PathInfo:
283
- if ":" not in path:
284
- raise ValueError(f"Invalid S3 path: {path}")
285
-
286
- prts = path.split(":", 1)
287
- remote = prts[0]
288
- path = prts[1]
289
- parts: list[str] = []
290
- for part in path.split("/"):
291
- part = part.strip()
292
- if part:
293
- parts.append(part)
294
- if len(parts) < 2:
295
- raise ValueError(f"Invalid S3 path: {path}")
296
- bucket = parts[0]
297
- key = "/".join(parts[1:])
298
- assert bucket
299
- assert key
300
- return S3PathInfo(remote=remote, bucket=bucket, key=key)
301
-
302
-
303
- def random_str(length: int) -> str:
304
- import random
305
- import string
306
-
307
- return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
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
+ config.unlink()
44
+ except Exception as e:
45
+ print(f"Error deleting config file: {config}, {e}")
46
+ _RCLONE_CONFIGS_LIST.clear()
47
+ if signum is not None:
48
+ signal.signal(signum, signal.SIG_DFL)
49
+ os.kill(os.getpid(), signum)
50
+
51
+
52
+ def _init_cleanup() -> None:
53
+ atexit.register(_clean_configs)
54
+
55
+ for sig in (signal.SIGINT, signal.SIGTERM):
56
+ signal.signal(sig, _clean_configs)
57
+
58
+
59
+ _init_cleanup()
60
+
61
+
62
+ def make_temp_config_file() -> Path:
63
+ from rclone_api.util import random_str
64
+
65
+ tmpdir = _TMP_CONFIG_DIR / random_str(32)
66
+ tmpdir.mkdir(parents=True, exist_ok=True)
67
+ tmpfile = tmpdir / "rclone.conf"
68
+ _RCLONE_CONFIGS_LIST.append(tmpfile)
69
+ return tmpfile
70
+
71
+
72
+ def clear_temp_config_file(path: Path | None) -> None:
73
+ if (path is None) or (not path.exists()) or (not _DO_CLEANUP):
74
+ return
75
+ try:
76
+ path.unlink()
77
+ except Exception as e:
78
+ print(f"Error deleting config file: {path}, {e}")
79
+
80
+
81
+ def locked_print(*args, **kwargs):
82
+ with _PRINT_LOCK:
83
+ print(*args, **kwargs)
84
+
85
+
86
+ def port_is_free(port: int) -> bool:
87
+ import socket
88
+
89
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
90
+ return s.connect_ex(("localhost", port)) != 0
91
+
92
+
93
+ def find_free_port() -> int:
94
+ tries = 20
95
+ port = random.randint(10000, 20000)
96
+ while tries > 0:
97
+ if port_is_free(port):
98
+ return port
99
+ tries -= 1
100
+ port = random.randint(10000, 20000)
101
+ warnings.warn(f"Failed to find a free port, so using {port}")
102
+ return port
103
+
104
+
105
+ def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
106
+ from rclone_api.rclone_impl import RcloneImpl
107
+
108
+ assert isinstance(rclone, RcloneImpl)
109
+ # if str then it will be remote:path
110
+ if isinstance(item, str):
111
+ # return RPath(item)
112
+ # remote_name: str = item.split(":")[0]
113
+ parts = item.split(":")
114
+ remote_name = parts[0]
115
+ path = ":".join(parts[1:])
116
+ remote = Remote(name=remote_name, rclone=rclone)
117
+ out = RPath(
118
+ remote=remote,
119
+ path=path,
120
+ name="",
121
+ size=0,
122
+ mime_type="",
123
+ mod_time="",
124
+ is_dir=True,
125
+ )
126
+ out.set_rclone(rclone)
127
+ return out
128
+ elif isinstance(item, Dir):
129
+ return item.path
130
+ elif isinstance(item, Remote):
131
+ out = RPath(
132
+ remote=item,
133
+ path=str(item),
134
+ name=str(item),
135
+ size=0,
136
+ mime_type="inode/directory",
137
+ mod_time="",
138
+ is_dir=True,
139
+ )
140
+ out.set_rclone(rclone)
141
+ return out
142
+ else:
143
+ raise ValueError(f"Invalid type for item: {type(item)}")
144
+
145
+
146
+ def get_verbose(verbose: bool | None) -> bool:
147
+ if verbose is not None:
148
+ return verbose
149
+ # get it from the environment
150
+ return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
151
+
152
+
153
+ def get_check(check: bool | None) -> bool:
154
+ if check is not None:
155
+ return check
156
+ # get it from the environment
157
+ return bool(int(os.getenv("RCLONE_API_CHECK", "1")))
158
+
159
+
160
+ def get_rclone_exe(rclone_exe: Path | None) -> Path:
161
+ if rclone_exe is None:
162
+ rclone_which_path = shutil.which("rclone")
163
+ if rclone_which_path is not None:
164
+ return Path(rclone_which_path)
165
+ rclone_download(out=_RCLONE_EXE, replace=False)
166
+ return _RCLONE_EXE
167
+ return rclone_exe
168
+
169
+
170
+ def rclone_execute(
171
+ cmd: list[str],
172
+ rclone_conf: Path | Config,
173
+ rclone_exe: Path,
174
+ check: bool,
175
+ capture: bool | Path | None = None,
176
+ verbose: bool | None = None,
177
+ ) -> subprocess.CompletedProcess:
178
+ tmpfile: Path | None = None
179
+ verbose = get_verbose(verbose)
180
+
181
+ # Handle the Path case for capture.
182
+ output_file: Path | None = None
183
+ if isinstance(capture, Path):
184
+ output_file = capture
185
+ capture = False # When redirecting to file, don't capture to memory.
186
+ else:
187
+ capture = capture if isinstance(capture, bool) else True
188
+
189
+ try:
190
+ # Create a temporary config file if needed.
191
+ if isinstance(rclone_conf, Config):
192
+ tmpfile = make_temp_config_file()
193
+ tmpfile.write_text(rclone_conf.text, encoding="utf-8")
194
+ rclone_conf = tmpfile
195
+
196
+ # Build the command line.
197
+ full_cmd = (
198
+ [str(rclone_exe.resolve())] + ["--config", str(rclone_conf.resolve())] + cmd
199
+ )
200
+ if verbose:
201
+ cmd_str = subprocess.list2cmdline(full_cmd)
202
+ print(f"\nRunning: {cmd_str}")
203
+
204
+ # Prepare subprocess parameters.
205
+ proc_kwargs: dict[str, Any] = {
206
+ "encoding": "utf-8",
207
+ "shell": False,
208
+ "stderr": subprocess.PIPE,
209
+ }
210
+ file_handle = None
211
+ if output_file:
212
+ # Open the file for writing.
213
+ file_handle = open(output_file, "w", encoding="utf-8")
214
+ proc_kwargs["stdout"] = file_handle
215
+ else:
216
+ proc_kwargs["stdout"] = subprocess.PIPE if capture else None
217
+
218
+ # Start the process.
219
+ process = subprocess.Popen(full_cmd, **proc_kwargs)
220
+
221
+ # Register an atexit callback that uses psutil to kill the process tree.
222
+ proc_ref = weakref.ref(process)
223
+
224
+ def cleanup():
225
+ proc = proc_ref()
226
+ if proc is None:
227
+ return
228
+ try:
229
+ parent = psutil.Process(proc.pid)
230
+ except psutil.NoSuchProcess:
231
+ return
232
+ # Terminate all child processes first.
233
+ children = parent.children(recursive=True)
234
+ if children:
235
+ print(f"Terminating {len(children)} child process(es)...")
236
+ for child in children:
237
+ try:
238
+ child.terminate()
239
+ except Exception as e:
240
+ print(f"Error terminating child {child.pid}: {e}")
241
+ psutil.wait_procs(children, timeout=2)
242
+ for child in children:
243
+ if child.is_running():
244
+ try:
245
+ child.kill()
246
+ except Exception as e:
247
+ print(f"Error killing child {child.pid}: {e}")
248
+ # Now terminate the parent process.
249
+ if parent.is_running():
250
+ try:
251
+ parent.terminate()
252
+ parent.wait(timeout=3)
253
+ except (psutil.TimeoutExpired, Exception):
254
+ try:
255
+ parent.kill()
256
+ except Exception as e:
257
+ print(f"Error killing process {parent.pid}: {e}")
258
+
259
+ atexit.register(cleanup)
260
+
261
+ # Wait for the process to complete.
262
+ out, err = process.communicate()
263
+ # Close the file handle if used.
264
+ if file_handle:
265
+ file_handle.close()
266
+
267
+ cp: subprocess.CompletedProcess = subprocess.CompletedProcess(
268
+ args=full_cmd,
269
+ returncode=process.returncode,
270
+ stdout=out,
271
+ stderr=err,
272
+ )
273
+
274
+ # Warn or raise if return code is non-zero.
275
+ if cp.returncode != 0:
276
+ cmd_str = subprocess.list2cmdline(full_cmd)
277
+ warnings.warn(
278
+ f"Error running: {cmd_str}, returncode: {cp.returncode}\n"
279
+ f"{cp.stdout}\n{cp.stderr}"
280
+ )
281
+ if check:
282
+ raise subprocess.CalledProcessError(
283
+ cp.returncode, full_cmd, cp.stdout, cp.stderr
284
+ )
285
+ return cp
286
+ finally:
287
+ clear_temp_config_file(tmpfile)
288
+
289
+
290
+ def split_s3_path(path: str) -> S3PathInfo:
291
+ if ":" not in path:
292
+ raise ValueError(f"Invalid S3 path: {path}")
293
+
294
+ prts = path.split(":", 1)
295
+ remote = prts[0]
296
+ path = prts[1]
297
+ parts: list[str] = []
298
+ for part in path.split("/"):
299
+ part = part.strip()
300
+ if part:
301
+ parts.append(part)
302
+ if len(parts) < 2:
303
+ raise ValueError(f"Invalid S3 path: {path}")
304
+ bucket = parts[0]
305
+ key = "/".join(parts[1:])
306
+ assert bucket
307
+ assert key
308
+ return S3PathInfo(remote=remote, bucket=bucket, key=key)
309
+
310
+
311
+ def random_str(length: int) -> str:
312
+ import random
313
+ import string
314
+
315
+ return "".join(random.choices(string.ascii_lowercase + string.digits, k=length))