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/process.py
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
import atexit
|
2
|
+
import subprocess
|
3
|
+
import threading
|
4
|
+
import weakref
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
import psutil
|
10
|
+
|
11
|
+
from rclone_api.config import Config
|
12
|
+
from rclone_api.util import clear_temp_config_file, get_verbose, make_temp_config_file
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass
|
16
|
+
class ProcessArgs:
|
17
|
+
cmd: list[str]
|
18
|
+
rclone_conf: Path | Config
|
19
|
+
rclone_exe: Path
|
20
|
+
cmd_list: list[str]
|
21
|
+
verbose: bool | None = None
|
22
|
+
capture_stdout: bool | None = None
|
23
|
+
log: Path | None = None
|
24
|
+
|
25
|
+
|
26
|
+
class Process:
|
27
|
+
def __init__(self, args: ProcessArgs) -> None:
|
28
|
+
assert (
|
29
|
+
args.rclone_exe.exists()
|
30
|
+
), f"rclone executable not found: {args.rclone_exe}"
|
31
|
+
self.args = args
|
32
|
+
self.log = args.log
|
33
|
+
self.tempfile: Path | None = None
|
34
|
+
|
35
|
+
verbose = get_verbose(args.verbose)
|
36
|
+
# Create a temporary config file if needed.
|
37
|
+
if isinstance(args.rclone_conf, Config):
|
38
|
+
self.tempfile = make_temp_config_file()
|
39
|
+
self.tempfile.write_text(args.rclone_conf.text, encoding="utf-8")
|
40
|
+
rclone_conf = self.tempfile
|
41
|
+
else:
|
42
|
+
rclone_conf = args.rclone_conf
|
43
|
+
|
44
|
+
assert rclone_conf.exists(), f"rclone config not found: {rclone_conf}"
|
45
|
+
|
46
|
+
# Build the command.
|
47
|
+
self.cmd = (
|
48
|
+
[str(args.rclone_exe.resolve())]
|
49
|
+
+ ["--config", str(rclone_conf.resolve())]
|
50
|
+
+ args.cmd
|
51
|
+
)
|
52
|
+
if self.args.log:
|
53
|
+
self.args.log.parent.mkdir(parents=True, exist_ok=True)
|
54
|
+
self.cmd += ["--log-file", str(self.args.log)]
|
55
|
+
if verbose:
|
56
|
+
cmd_str = subprocess.list2cmdline(self.cmd)
|
57
|
+
print(f"Running: {cmd_str}")
|
58
|
+
kwargs: dict = {"shell": False}
|
59
|
+
if args.capture_stdout:
|
60
|
+
kwargs["stdout"] = subprocess.PIPE
|
61
|
+
kwargs["stderr"] = subprocess.STDOUT
|
62
|
+
|
63
|
+
self.process = subprocess.Popen(self.cmd, **kwargs) # type: ignore
|
64
|
+
|
65
|
+
# Register an atexit callback using a weak reference to avoid keeping the Process instance alive.
|
66
|
+
self_ref = weakref.ref(self)
|
67
|
+
|
68
|
+
def exit_cleanup():
|
69
|
+
obj = self_ref()
|
70
|
+
if obj is not None:
|
71
|
+
obj._atexit_terminate()
|
72
|
+
|
73
|
+
atexit.register(exit_cleanup)
|
74
|
+
|
75
|
+
def __enter__(self) -> "Process":
|
76
|
+
return self
|
77
|
+
|
78
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
79
|
+
self.terminate()
|
80
|
+
self.wait()
|
81
|
+
self.cleanup()
|
82
|
+
|
83
|
+
def cleanup(self) -> None:
|
84
|
+
if self.tempfile:
|
85
|
+
clear_temp_config_file(self.tempfile)
|
86
|
+
|
87
|
+
def _kill_process_tree(self) -> None:
|
88
|
+
"""
|
89
|
+
Use psutil to recursively terminate the main process and all its child processes.
|
90
|
+
"""
|
91
|
+
try:
|
92
|
+
parent = psutil.Process(self.process.pid)
|
93
|
+
except psutil.NoSuchProcess:
|
94
|
+
return
|
95
|
+
|
96
|
+
# Terminate child processes.
|
97
|
+
children = parent.children(recursive=True)
|
98
|
+
if children:
|
99
|
+
print(f"Terminating {len(children)} child processes...")
|
100
|
+
for child in children:
|
101
|
+
try:
|
102
|
+
child.terminate()
|
103
|
+
except Exception as e:
|
104
|
+
print(f"Error terminating child process {child.pid}: {e}")
|
105
|
+
psutil.wait_procs(children, timeout=2)
|
106
|
+
# Kill any that remain.
|
107
|
+
for child in children:
|
108
|
+
if child.is_running():
|
109
|
+
try:
|
110
|
+
child.kill()
|
111
|
+
except Exception as e:
|
112
|
+
print(f"Error killing child process {child.pid}: {e}")
|
113
|
+
|
114
|
+
# Terminate the parent process.
|
115
|
+
if parent.is_running():
|
116
|
+
try:
|
117
|
+
parent.terminate()
|
118
|
+
except Exception as e:
|
119
|
+
print(f"Error terminating process {parent.pid}: {e}")
|
120
|
+
try:
|
121
|
+
parent.wait(timeout=3)
|
122
|
+
except psutil.TimeoutExpired:
|
123
|
+
try:
|
124
|
+
parent.kill()
|
125
|
+
except Exception as e:
|
126
|
+
print(f"Error killing process {parent.pid}: {e}")
|
127
|
+
|
128
|
+
def _atexit_terminate(self) -> None:
|
129
|
+
"""
|
130
|
+
This method is registered via atexit and uses psutil to clean up the process tree.
|
131
|
+
It runs in a daemon thread so that termination happens without blocking interpreter shutdown.
|
132
|
+
"""
|
133
|
+
if self.process.poll() is None: # Process is still running.
|
134
|
+
|
135
|
+
def terminate_sequence():
|
136
|
+
self._kill_process_tree()
|
137
|
+
|
138
|
+
t = threading.Thread(target=terminate_sequence, daemon=True)
|
139
|
+
t.start()
|
140
|
+
t.join(timeout=3)
|
141
|
+
|
142
|
+
@property
|
143
|
+
def pid(self) -> int:
|
144
|
+
return self.process.pid
|
145
|
+
|
146
|
+
def __del__(self) -> None:
|
147
|
+
self.cleanup()
|
148
|
+
|
149
|
+
def kill(self) -> None:
|
150
|
+
"""Forcefully kill the process tree."""
|
151
|
+
self._kill_process_tree()
|
152
|
+
|
153
|
+
def terminate(self) -> None:
|
154
|
+
"""Gracefully terminate the process tree."""
|
155
|
+
self._kill_process_tree()
|
156
|
+
|
157
|
+
@property
|
158
|
+
def returncode(self) -> int | None:
|
159
|
+
return self.process.returncode
|
160
|
+
|
161
|
+
@property
|
162
|
+
def stdout(self) -> Any:
|
163
|
+
return self.process.stdout
|
164
|
+
|
165
|
+
@property
|
166
|
+
def stderr(self) -> Any:
|
167
|
+
return self.process.stderr
|
168
|
+
|
169
|
+
def poll(self) -> int | None:
|
170
|
+
return self.process.poll()
|
171
|
+
|
172
|
+
def wait(self) -> int:
|
173
|
+
return self.process.wait()
|
174
|
+
|
175
|
+
def send_signal(self, sig: int) -> None:
|
176
|
+
self.process.send_signal(sig)
|
177
|
+
|
178
|
+
def __str__(self) -> str:
|
179
|
+
state = ""
|
180
|
+
rtn = self.process.poll()
|
181
|
+
if rtn is None:
|
182
|
+
state = "running"
|
183
|
+
elif rtn != 0:
|
184
|
+
state = f"error: {rtn}"
|
185
|
+
else:
|
186
|
+
state = "finished ok"
|
187
|
+
return f"Process({self.cmd}, {state})"
|