remote-cmd-manager 2.2.0__tar.gz → 2.3.0__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.
- {remote_cmd_manager-2.2.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.3.0}/PKG-INFO +16 -12
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/README.md +12 -11
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/pyproject.toml +3 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/__init__.py +1 -1
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/_version.py +1 -1
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/async_ssh_client.py +141 -13
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/ssh_client.py +161 -28
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0/remote_cmd_manager.egg-info}/PKG-INFO +16 -12
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_async_ssh_client.py +253 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_ssh_client.py +132 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/LICENSE +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/MANIFEST.in +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/config.example.yaml +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/examples/basic_usage.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/examples/deploy_script.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/examples/nginx_batch_update.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/examples/system_health_check.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/cli/__init__.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/cli/main.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/__init__.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/async_connection_pool.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/host.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/host_manager.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/core/sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/repository/__init__.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/repository/host_repository.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/repository/json_host_repository.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/repository/sqlite_host_repository.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/__init__.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/_host_runner.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/_pool_policy.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/_types.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/async_batch_executor.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/batch_executor.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/credential_provider.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/host_service.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/retry_policy.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/ssh_service.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/storage_factory.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/service/task_runner.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/__init__.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/config.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/credential_guard.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/crypto.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/exceptions.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd/utils/logging_utils.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/SOURCES.txt +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/requires.txt +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/requirements.txt +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/setup.cfg +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/__init__.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/conftest.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/integration/conftest.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/integration/test_ssh_connection.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/performance/__init__.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/performance/conftest.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/performance/test_benchmarks.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_async_batch_executor.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_batch_executor.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_cli.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_config.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_credential_provider.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_crypto.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_exceptions.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_host.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_host_manager.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_host_service.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_keyring_provider.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_logging_utils.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_pool_policy.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_release_metadata.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_repository.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_retry_policy.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_sqlite_repository.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_ssh_service.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_storage_factory.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.2.0 → remote_cmd_manager-2.3.0}/tests/test_task_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remote_cmd_manager
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Cross-platform SSH remote server management tool (Linux/macOS/Windows) — lightweight, zero-configuration CLI and Python API for managing servers over SSH. Supports host CRUD, batch commands, file transfer, credential encryption, async execution, and more.
|
|
5
5
|
Author-email: Vae-Scrooge <vaescrooge@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,9 @@ Classifier: Programming Language :: Python :: 3
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
24
|
Classifier: Topic :: System :: Systems Administration
|
|
22
25
|
Classifier: Topic :: System :: Networking
|
|
23
26
|
Classifier: Environment :: Console
|
|
@@ -86,27 +89,28 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
86
89
|
|
|
87
90
|
---
|
|
88
91
|
|
|
89
|
-
## v2.
|
|
92
|
+
## v2.3.0 Release Highlights
|
|
90
93
|
|
|
91
|
-
v2.
|
|
92
|
-
|
|
94
|
+
v2.3.0 makes blocking SFTP operations timeout-safe, hardens async cancellation cleanup,
|
|
95
|
+
and aligns the supported Python range and release tooling. See the
|
|
93
96
|
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
94
97
|
|
|
95
|
-
###
|
|
98
|
+
### SFTP Timeouts & Reliability
|
|
96
99
|
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
+
- Blocking SFTP operations now support an inactivity timeout (default: `ConnectionConfig.timeout`, 30 s): uploads, downloads, directory listing, directory creation, removal, and file-info calls accept an optional `timeout` argument.
|
|
101
|
+
- The synchronous `SSHClient` applies the timeout to the Paramiko SFTP channel, so a stalled read/write aborts the actual operation and raises `SSHFileTransferError`.
|
|
102
|
+
- The asynchronous `AsyncSSHClient` bounds SFTP channel startup with a timeout and uses a progress-based inactivity watchdog to cancel stalled transfers, awaiting the aborted operation and preserving caller cancellation during cleanup.
|
|
103
|
+
- Timed-out or cancelled SFTP sessions are closed and discarded, so a stale or desynchronized channel can never be reused.
|
|
100
104
|
|
|
101
105
|
### Release Engineering
|
|
102
106
|
|
|
103
|
-
-
|
|
104
|
-
- CI
|
|
105
|
-
-
|
|
107
|
+
- Python 3.9–3.14 are officially supported and CI-tested; `Requires-Python` remains `>=3.9`.
|
|
108
|
+
- CI runs release-metadata validation for CHANGELOG.md-only changes.
|
|
109
|
+
- Publish builds pin `build==1.5.0` and `twine==7.0.0`.
|
|
106
110
|
|
|
107
111
|
## Table of Contents
|
|
108
112
|
|
|
109
|
-
- [v2.
|
|
113
|
+
- [v2.3.0 Release Highlights](#v230-release-highlights)
|
|
110
114
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
111
115
|
- [Quick Start](#quick-start)
|
|
112
116
|
- [Use Cases](#use-cases)
|
|
@@ -36,27 +36,28 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
## v2.
|
|
39
|
+
## v2.3.0 Release Highlights
|
|
40
40
|
|
|
41
|
-
v2.
|
|
42
|
-
|
|
41
|
+
v2.3.0 makes blocking SFTP operations timeout-safe, hardens async cancellation cleanup,
|
|
42
|
+
and aligns the supported Python range and release tooling. See the
|
|
43
43
|
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
44
44
|
|
|
45
|
-
###
|
|
45
|
+
### SFTP Timeouts & Reliability
|
|
46
46
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
47
|
+
- Blocking SFTP operations now support an inactivity timeout (default: `ConnectionConfig.timeout`, 30 s): uploads, downloads, directory listing, directory creation, removal, and file-info calls accept an optional `timeout` argument.
|
|
48
|
+
- The synchronous `SSHClient` applies the timeout to the Paramiko SFTP channel, so a stalled read/write aborts the actual operation and raises `SSHFileTransferError`.
|
|
49
|
+
- The asynchronous `AsyncSSHClient` bounds SFTP channel startup with a timeout and uses a progress-based inactivity watchdog to cancel stalled transfers, awaiting the aborted operation and preserving caller cancellation during cleanup.
|
|
50
|
+
- Timed-out or cancelled SFTP sessions are closed and discarded, so a stale or desynchronized channel can never be reused.
|
|
50
51
|
|
|
51
52
|
### Release Engineering
|
|
52
53
|
|
|
53
|
-
-
|
|
54
|
-
- CI
|
|
55
|
-
-
|
|
54
|
+
- Python 3.9–3.14 are officially supported and CI-tested; `Requires-Python` remains `>=3.9`.
|
|
55
|
+
- CI runs release-metadata validation for CHANGELOG.md-only changes.
|
|
56
|
+
- Publish builds pin `build==1.5.0` and `twine==7.0.0`.
|
|
56
57
|
|
|
57
58
|
## Table of Contents
|
|
58
59
|
|
|
59
|
-
- [v2.
|
|
60
|
+
- [v2.3.0 Release Highlights](#v230-release-highlights)
|
|
60
61
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
61
62
|
- [Quick Start](#quick-start)
|
|
62
63
|
- [Use Cases](#use-cases)
|
|
@@ -22,6 +22,9 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: 3.9",
|
|
23
23
|
"Programming Language :: Python :: 3.10",
|
|
24
24
|
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
25
28
|
"Topic :: System :: Systems Administration",
|
|
26
29
|
"Topic :: System :: Networking",
|
|
27
30
|
"Environment :: Console",
|
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
- 密码、密钥等敏感信息不入日志(沿用项目 SensitiveDataFilter 规范)。
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
import asyncio
|
|
18
|
+
import contextlib
|
|
17
19
|
import logging
|
|
18
20
|
import shlex
|
|
19
21
|
import stat
|
|
22
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
20
23
|
from pathlib import Path
|
|
21
24
|
from typing import Any, Optional
|
|
22
25
|
|
|
@@ -34,6 +37,7 @@ from remote_cmd.utils.exceptions import (
|
|
|
34
37
|
SSHConnectionError,
|
|
35
38
|
SSHFileTransferError,
|
|
36
39
|
SSHTimeoutError,
|
|
40
|
+
ValidationError,
|
|
37
41
|
)
|
|
38
42
|
|
|
39
43
|
logger = logging.getLogger(__name__)
|
|
@@ -286,45 +290,169 @@ class AsyncSSHClient:
|
|
|
286
290
|
# ------------------------------------------------------------------
|
|
287
291
|
# SFTP / 文件传输
|
|
288
292
|
# ------------------------------------------------------------------
|
|
289
|
-
async def _get_sftp(self) -> asyncssh.SFTPClient:
|
|
293
|
+
async def _get_sftp(self, timeout: Optional[float] = None) -> asyncssh.SFTPClient:
|
|
294
|
+
"""获取 SFTP 客户端(延迟初始化)。
|
|
295
|
+
|
|
296
|
+
v2.3:打开 SFTP channel 本身也受有效超时约束,避免握手阶段永久挂起。
|
|
297
|
+
"""
|
|
290
298
|
conn = await self._get_conn()
|
|
291
299
|
if self._sftp is None:
|
|
300
|
+
effective = self._effective_sftp_timeout(timeout)
|
|
292
301
|
try:
|
|
293
|
-
self._sftp = await conn.start_sftp_client()
|
|
302
|
+
self._sftp = await asyncio.wait_for(conn.start_sftp_client(), timeout=effective)
|
|
303
|
+
except asyncio.TimeoutError as e:
|
|
304
|
+
raise SSHFileTransferError(
|
|
305
|
+
f"failed to open SFTP channel: timed out after {effective:g} seconds"
|
|
306
|
+
) from e
|
|
294
307
|
except (OSError, asyncssh.Error) as e:
|
|
295
308
|
raise SSHFileTransferError(f"failed to open SFTP channel: {e}") from e
|
|
296
309
|
return self._sftp
|
|
297
310
|
|
|
298
|
-
|
|
299
|
-
"""
|
|
300
|
-
|
|
311
|
+
def _effective_sftp_timeout(self, timeout: Optional[float]) -> float:
|
|
312
|
+
"""返回 SFTP 操作的有效 inactivity 超时(秒)。
|
|
313
|
+
|
|
314
|
+
显式 timeout 优先,否则回退到 ``ConnectionConfig.timeout``。
|
|
315
|
+
"""
|
|
316
|
+
if timeout is not None:
|
|
317
|
+
if timeout <= 0:
|
|
318
|
+
raise ValidationError(f"timeout must be > 0, got: {timeout}")
|
|
319
|
+
return timeout
|
|
320
|
+
return self.config.timeout
|
|
321
|
+
|
|
322
|
+
def _discard_sftp(self) -> None:
|
|
323
|
+
"""关闭并丢弃缓存的 SFTP 会话(超时后防止复用可能失步的会话)。"""
|
|
324
|
+
sftp, self._sftp = self._sftp, None
|
|
325
|
+
if sftp is not None:
|
|
326
|
+
with contextlib.suppress(Exception):
|
|
327
|
+
sftp.exit()
|
|
328
|
+
|
|
329
|
+
async def _run_sftp_operation(
|
|
330
|
+
self,
|
|
331
|
+
operation: Callable[[Callable[..., None]], Awaitable[Any]],
|
|
332
|
+
timeout: Optional[float],
|
|
333
|
+
description: str,
|
|
334
|
+
) -> Any:
|
|
335
|
+
"""运行 SFTP 操作并施加 inactivity(静默)超时。
|
|
336
|
+
|
|
337
|
+
asyncssh 的 SFTP API 没有原生超时参数,因此使用 watchdog:
|
|
338
|
+
``operation`` 收到一个 progress 回调(传给 ``put``/``get`` 的
|
|
339
|
+
``progress_handler``),每次有数据进展就刷新时间戳;超过有效超时
|
|
340
|
+
没有任何进展则取消操作、丢弃失步的 SFTP 会话并抛出
|
|
341
|
+
``SSHFileTransferError``。只要数据持续流动,长时间传输不会被打断。
|
|
342
|
+
"""
|
|
343
|
+
effective = self._effective_sftp_timeout(timeout)
|
|
344
|
+
loop = asyncio.get_running_loop()
|
|
345
|
+
last_activity = loop.time()
|
|
346
|
+
|
|
347
|
+
def _mark_progress(*_args: Any) -> None:
|
|
348
|
+
nonlocal last_activity
|
|
349
|
+
last_activity = loop.time()
|
|
350
|
+
|
|
351
|
+
task = asyncio.ensure_future(operation(_mark_progress))
|
|
352
|
+
interval = min(0.5, max(effective / 10.0, 0.01))
|
|
353
|
+
cancelled: Optional[asyncio.CancelledError] = None
|
|
354
|
+
try:
|
|
355
|
+
while True:
|
|
356
|
+
done, _pending = await asyncio.wait({task}, timeout=interval)
|
|
357
|
+
if task in done:
|
|
358
|
+
return task.result()
|
|
359
|
+
if loop.time() - last_activity >= effective:
|
|
360
|
+
break
|
|
361
|
+
except asyncio.CancelledError as exc:
|
|
362
|
+
# 外层取消:记录后统一走清理路径(不在此处 await,避免
|
|
363
|
+
# 取消事件再次进入本处理器)
|
|
364
|
+
cancelled = exc
|
|
365
|
+
|
|
366
|
+
# 统一清理:立即丢弃可能失步的 SFTP 会话;取消并等待子任务收尾。
|
|
367
|
+
# shield 确保清理期间到达的再次取消只打断"等待"本身,而不会
|
|
368
|
+
# 把清理过程转换成超时错误或被整体吞掉。
|
|
369
|
+
self._discard_sftp()
|
|
370
|
+
task.cancel()
|
|
371
|
+
try:
|
|
372
|
+
await asyncio.shield(task)
|
|
373
|
+
except asyncio.CancelledError:
|
|
374
|
+
if not task.cancelled():
|
|
375
|
+
# 清理期间外层再次取消:保留取消语义
|
|
376
|
+
raise
|
|
377
|
+
except Exception: # noqa: BLE001 - 子任务失败不应掩盖超时/取消语义
|
|
378
|
+
pass
|
|
379
|
+
|
|
380
|
+
if cancelled is not None:
|
|
381
|
+
raise cancelled
|
|
382
|
+
raise SSHFileTransferError(
|
|
383
|
+
f"{description} timed out after {effective:g} seconds of inactivity"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
async def upload_file(
|
|
387
|
+
self,
|
|
388
|
+
local_path: str,
|
|
389
|
+
remote_path: str,
|
|
390
|
+
timeout: Optional[float] = None,
|
|
391
|
+
) -> None:
|
|
392
|
+
"""异步上传本地文件到远程服务器。
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
local_path: 本地文件路径
|
|
396
|
+
remote_path: 远程目标路径
|
|
397
|
+
timeout: inactivity 超时(秒);None 表示使用
|
|
398
|
+
``ConnectionConfig.timeout``。静默超过该时长即中止
|
|
399
|
+
"""
|
|
400
|
+
sftp = await self._get_sftp(timeout)
|
|
301
401
|
local_file = Path(local_path)
|
|
302
402
|
if not local_file.exists():
|
|
303
403
|
raise SSHFileTransferError(f"Local file not found: {local_path}")
|
|
304
404
|
logger.info(f"uploading file: {local_path} -> {remote_path}")
|
|
405
|
+
|
|
406
|
+
async def _operation(progress: Callable[..., None]) -> None:
|
|
407
|
+
await sftp.put(str(local_file), remote_path, progress_handler=progress)
|
|
408
|
+
|
|
305
409
|
try:
|
|
306
|
-
await
|
|
410
|
+
await self._run_sftp_operation(_operation, timeout, "file upload")
|
|
307
411
|
except (OSError, asyncssh.Error) as e:
|
|
308
412
|
raise SSHFileTransferError(f"file upload failed: {e}") from e
|
|
309
413
|
logger.info("file upload finished")
|
|
310
414
|
|
|
311
|
-
async def download_file(
|
|
312
|
-
|
|
313
|
-
|
|
415
|
+
async def download_file(
|
|
416
|
+
self,
|
|
417
|
+
remote_path: str,
|
|
418
|
+
local_path: str,
|
|
419
|
+
timeout: Optional[float] = None,
|
|
420
|
+
) -> None:
|
|
421
|
+
"""异步从远程服务器下载文件。
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
remote_path: 远程文件路径
|
|
425
|
+
local_path: 本地目标路径
|
|
426
|
+
timeout: inactivity 超时(秒);None 表示使用
|
|
427
|
+
``ConnectionConfig.timeout``。静默超过该时长即中止
|
|
428
|
+
"""
|
|
429
|
+
sftp = await self._get_sftp(timeout)
|
|
314
430
|
local_file = Path(local_path)
|
|
315
431
|
local_file.parent.mkdir(parents=True, exist_ok=True)
|
|
316
432
|
logger.info(f"downloading file: {remote_path} -> {local_path}")
|
|
433
|
+
|
|
434
|
+
async def _operation(progress: Callable[..., None]) -> None:
|
|
435
|
+
await sftp.get(remote_path, str(local_file), progress_handler=progress)
|
|
436
|
+
|
|
317
437
|
try:
|
|
318
|
-
await
|
|
438
|
+
await self._run_sftp_operation(_operation, timeout, "file download")
|
|
319
439
|
except (OSError, asyncssh.Error) as e:
|
|
320
440
|
raise SSHFileTransferError(f"file download failed: {e}") from e
|
|
321
441
|
logger.info("file download finished")
|
|
322
442
|
|
|
323
|
-
async def list_remote_directory(
|
|
443
|
+
async def list_remote_directory(
|
|
444
|
+
self,
|
|
445
|
+
remote_path: str = ".",
|
|
446
|
+
timeout: Optional[float] = None,
|
|
447
|
+
) -> list[RemoteFileEntry]:
|
|
324
448
|
"""异步列出远程目录内容(结构与同步 SSHClient 一致)。"""
|
|
325
|
-
sftp = await self._get_sftp()
|
|
449
|
+
sftp = await self._get_sftp(timeout)
|
|
450
|
+
|
|
451
|
+
async def _operation(_progress: Callable[..., None]) -> Sequence[Any]:
|
|
452
|
+
return await sftp.readdir(remote_path)
|
|
453
|
+
|
|
326
454
|
try:
|
|
327
|
-
names = await
|
|
455
|
+
names = await self._run_sftp_operation(_operation, timeout, "list remote directory")
|
|
328
456
|
except (OSError, asyncssh.Error) as e:
|
|
329
457
|
raise SSHFileTransferError(f"failed to list remote directory: {e}") from e
|
|
330
458
|
|