pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__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 (203) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +18 -3
  4. pyinfra/api/arguments.py +406 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +67 -18
  12. pyinfra/api/facts.py +253 -202
  13. pyinfra/api/host.py +413 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/operation.py +432 -262
  16. pyinfra/api/operations.py +273 -260
  17. pyinfra/api/state.py +302 -248
  18. pyinfra/api/util.py +291 -368
  19. pyinfra/connectors/base.py +173 -0
  20. pyinfra/connectors/chroot.py +212 -0
  21. pyinfra/connectors/docker.py +381 -0
  22. pyinfra/connectors/dockerssh.py +297 -0
  23. pyinfra/connectors/local.py +238 -0
  24. pyinfra/connectors/scp/__init__.py +1 -0
  25. pyinfra/connectors/scp/client.py +204 -0
  26. pyinfra/connectors/ssh.py +670 -0
  27. pyinfra/connectors/ssh_util.py +114 -0
  28. pyinfra/connectors/sshuserclient/client.py +309 -0
  29. pyinfra/connectors/sshuserclient/config.py +102 -0
  30. pyinfra/connectors/terraform.py +135 -0
  31. pyinfra/connectors/util.py +410 -0
  32. pyinfra/connectors/vagrant.py +183 -0
  33. pyinfra/context.py +145 -0
  34. pyinfra/facts/__init__.py +7 -6
  35. pyinfra/facts/apk.py +22 -7
  36. pyinfra/facts/apt.py +117 -60
  37. pyinfra/facts/brew.py +100 -15
  38. pyinfra/facts/bsdinit.py +23 -0
  39. pyinfra/facts/cargo.py +37 -0
  40. pyinfra/facts/choco.py +47 -0
  41. pyinfra/facts/crontab.py +195 -0
  42. pyinfra/facts/deb.py +94 -0
  43. pyinfra/facts/dnf.py +48 -0
  44. pyinfra/facts/docker.py +96 -23
  45. pyinfra/facts/efibootmgr.py +113 -0
  46. pyinfra/facts/files.py +630 -58
  47. pyinfra/facts/flatpak.py +77 -0
  48. pyinfra/facts/freebsd.py +70 -0
  49. pyinfra/facts/gem.py +19 -6
  50. pyinfra/facts/git.py +59 -14
  51. pyinfra/facts/gpg.py +150 -0
  52. pyinfra/facts/hardware.py +313 -167
  53. pyinfra/facts/iptables.py +72 -62
  54. pyinfra/facts/launchd.py +44 -0
  55. pyinfra/facts/lxd.py +17 -4
  56. pyinfra/facts/mysql.py +122 -86
  57. pyinfra/facts/npm.py +17 -9
  58. pyinfra/facts/openrc.py +71 -0
  59. pyinfra/facts/opkg.py +246 -0
  60. pyinfra/facts/pacman.py +50 -7
  61. pyinfra/facts/pip.py +24 -7
  62. pyinfra/facts/pipx.py +82 -0
  63. pyinfra/facts/pkg.py +15 -6
  64. pyinfra/facts/pkgin.py +35 -0
  65. pyinfra/facts/podman.py +54 -0
  66. pyinfra/facts/postgres.py +178 -0
  67. pyinfra/facts/postgresql.py +6 -147
  68. pyinfra/facts/rpm.py +105 -0
  69. pyinfra/facts/runit.py +77 -0
  70. pyinfra/facts/selinux.py +161 -0
  71. pyinfra/facts/server.py +746 -285
  72. pyinfra/facts/snap.py +88 -0
  73. pyinfra/facts/systemd.py +139 -0
  74. pyinfra/facts/sysvinit.py +59 -0
  75. pyinfra/facts/upstart.py +35 -0
  76. pyinfra/facts/util/__init__.py +17 -0
  77. pyinfra/facts/util/databases.py +4 -6
  78. pyinfra/facts/util/packaging.py +37 -6
  79. pyinfra/facts/util/units.py +30 -0
  80. pyinfra/facts/util/win_files.py +99 -0
  81. pyinfra/facts/vzctl.py +20 -13
  82. pyinfra/facts/xbps.py +35 -0
  83. pyinfra/facts/yum.py +34 -40
  84. pyinfra/facts/zfs.py +77 -0
  85. pyinfra/facts/zypper.py +42 -0
  86. pyinfra/local.py +45 -83
  87. pyinfra/operations/__init__.py +12 -0
  88. pyinfra/operations/apk.py +98 -0
  89. pyinfra/operations/apt.py +488 -0
  90. pyinfra/operations/brew.py +231 -0
  91. pyinfra/operations/bsdinit.py +59 -0
  92. pyinfra/operations/cargo.py +45 -0
  93. pyinfra/operations/choco.py +61 -0
  94. pyinfra/operations/crontab.py +191 -0
  95. pyinfra/operations/dnf.py +210 -0
  96. pyinfra/operations/docker.py +446 -0
  97. pyinfra/operations/files.py +1939 -0
  98. pyinfra/operations/flatpak.py +94 -0
  99. pyinfra/operations/freebsd/__init__.py +12 -0
  100. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  101. pyinfra/operations/freebsd/pkg.py +219 -0
  102. pyinfra/operations/freebsd/service.py +116 -0
  103. pyinfra/operations/freebsd/sysrc.py +92 -0
  104. pyinfra/operations/gem.py +47 -0
  105. pyinfra/operations/git.py +419 -0
  106. pyinfra/operations/iptables.py +311 -0
  107. pyinfra/operations/launchd.py +45 -0
  108. pyinfra/operations/lxd.py +68 -0
  109. pyinfra/operations/mysql.py +609 -0
  110. pyinfra/operations/npm.py +57 -0
  111. pyinfra/operations/openrc.py +63 -0
  112. pyinfra/operations/opkg.py +88 -0
  113. pyinfra/operations/pacman.py +81 -0
  114. pyinfra/operations/pip.py +205 -0
  115. pyinfra/operations/pipx.py +102 -0
  116. pyinfra/operations/pkg.py +70 -0
  117. pyinfra/operations/pkgin.py +91 -0
  118. pyinfra/operations/postgres.py +436 -0
  119. pyinfra/operations/postgresql.py +30 -0
  120. pyinfra/operations/puppet.py +40 -0
  121. pyinfra/operations/python.py +72 -0
  122. pyinfra/operations/runit.py +184 -0
  123. pyinfra/operations/selinux.py +189 -0
  124. pyinfra/operations/server.py +1099 -0
  125. pyinfra/operations/snap.py +117 -0
  126. pyinfra/operations/ssh.py +216 -0
  127. pyinfra/operations/systemd.py +149 -0
  128. pyinfra/operations/sysvinit.py +141 -0
  129. pyinfra/operations/upstart.py +68 -0
  130. pyinfra/operations/util/__init__.py +12 -0
  131. pyinfra/operations/util/docker.py +251 -0
  132. pyinfra/operations/util/files.py +247 -0
  133. pyinfra/operations/util/packaging.py +336 -0
  134. pyinfra/operations/util/service.py +46 -0
  135. pyinfra/operations/vzctl.py +137 -0
  136. pyinfra/operations/xbps.py +77 -0
  137. pyinfra/operations/yum.py +210 -0
  138. pyinfra/operations/zfs.py +175 -0
  139. pyinfra/operations/zypper.py +192 -0
  140. pyinfra/progress.py +44 -32
  141. pyinfra/py.typed +0 -0
  142. pyinfra/version.py +9 -1
  143. pyinfra-3.5.1.dist-info/METADATA +141 -0
  144. pyinfra-3.5.1.dist-info/RECORD +159 -0
  145. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
  146. pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
  147. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
  148. pyinfra_cli/__init__.py +1 -0
  149. pyinfra_cli/cli.py +780 -0
  150. pyinfra_cli/commands.py +66 -0
  151. pyinfra_cli/exceptions.py +155 -65
  152. pyinfra_cli/inventory.py +233 -89
  153. pyinfra_cli/log.py +39 -43
  154. pyinfra_cli/main.py +26 -495
  155. pyinfra_cli/prints.py +215 -156
  156. pyinfra_cli/util.py +172 -105
  157. pyinfra_cli/virtualenv.py +25 -20
  158. pyinfra/api/connectors/__init__.py +0 -21
  159. pyinfra/api/connectors/ansible.py +0 -99
  160. pyinfra/api/connectors/docker.py +0 -178
  161. pyinfra/api/connectors/local.py +0 -169
  162. pyinfra/api/connectors/ssh.py +0 -402
  163. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  164. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  165. pyinfra/api/connectors/util.py +0 -63
  166. pyinfra/api/connectors/vagrant.py +0 -155
  167. pyinfra/facts/init.py +0 -176
  168. pyinfra/facts/util/files.py +0 -102
  169. pyinfra/hook.py +0 -41
  170. pyinfra/modules/__init__.py +0 -11
  171. pyinfra/modules/apk.py +0 -64
  172. pyinfra/modules/apt.py +0 -272
  173. pyinfra/modules/brew.py +0 -122
  174. pyinfra/modules/files.py +0 -711
  175. pyinfra/modules/gem.py +0 -30
  176. pyinfra/modules/git.py +0 -115
  177. pyinfra/modules/init.py +0 -344
  178. pyinfra/modules/iptables.py +0 -271
  179. pyinfra/modules/lxd.py +0 -45
  180. pyinfra/modules/mysql.py +0 -347
  181. pyinfra/modules/npm.py +0 -47
  182. pyinfra/modules/pacman.py +0 -60
  183. pyinfra/modules/pip.py +0 -99
  184. pyinfra/modules/pkg.py +0 -43
  185. pyinfra/modules/postgresql.py +0 -245
  186. pyinfra/modules/puppet.py +0 -20
  187. pyinfra/modules/python.py +0 -37
  188. pyinfra/modules/server.py +0 -524
  189. pyinfra/modules/ssh.py +0 -150
  190. pyinfra/modules/util/files.py +0 -52
  191. pyinfra/modules/util/packaging.py +0 -118
  192. pyinfra/modules/vzctl.py +0 -133
  193. pyinfra/modules/yum.py +0 -171
  194. pyinfra/pseudo_modules.py +0 -64
  195. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  196. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  197. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  198. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  199. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  200. pyinfra_cli/__main__.py +0 -40
  201. pyinfra_cli/config.py +0 -92
  202. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  203. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
@@ -0,0 +1,238 @@
1
+ import os
2
+ from shutil import which
3
+ from tempfile import mkstemp
4
+ from typing import TYPE_CHECKING, Tuple
5
+
6
+ import click
7
+ from typing_extensions import Unpack, override
8
+
9
+ from pyinfra import logger
10
+ from pyinfra.api.command import QuoteString, StringCommand
11
+ from pyinfra.api.exceptions import InventoryError
12
+ from pyinfra.api.util import get_file_io
13
+
14
+ from .base import BaseConnector
15
+ from .util import (
16
+ CommandOutput,
17
+ execute_command_with_sudo_retry,
18
+ make_unix_command_for_host,
19
+ run_local_process,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from pyinfra.api.arguments import ConnectorArguments
24
+
25
+
26
+ class LocalConnector(BaseConnector):
27
+ """
28
+ The ``@local`` connector executes changes on the local machine using
29
+ subprocesses. **This connector is only compatible with MacOS & Linux hosts**.
30
+
31
+ Examples:
32
+
33
+ .. code::
34
+
35
+ # Install nginx
36
+ pyinfra inventory.py apt.packages nginx update=true _sudo=true
37
+ """
38
+
39
+ handles_execution = True
40
+
41
+ @override
42
+ @staticmethod
43
+ def make_names_data(name=None):
44
+ if name is not None:
45
+ raise InventoryError("Cannot have more than one @local")
46
+
47
+ yield "@local", {}, ["@local"]
48
+
49
+ @override
50
+ def run_shell_command(
51
+ self,
52
+ command: StringCommand,
53
+ print_output: bool = False,
54
+ print_input: bool = False,
55
+ **arguments: Unpack["ConnectorArguments"],
56
+ ) -> Tuple[bool, CommandOutput]:
57
+ """
58
+ Execute a command on the local machine.
59
+
60
+ Args:
61
+ command (StringCommand): actual command to execute
62
+ print_output (bool): whether to print command output
63
+ print_input (bool): whether to print command input
64
+ arguments: (ConnectorArguments): connector global arguments
65
+
66
+ Returns:
67
+ tuple: (bool, CommandOutput)
68
+ Bool indicating success and CommandOutput with stdout/stderr lines.
69
+ """
70
+
71
+ arguments.pop("_get_pty", False)
72
+ _timeout = arguments.pop("_timeout", None)
73
+ _stdin = arguments.pop("_stdin", None)
74
+ _success_exit_codes = arguments.pop("_success_exit_codes", None)
75
+
76
+ def execute_command() -> Tuple[int, CommandOutput]:
77
+ unix_command = make_unix_command_for_host(self.state, self.host, command, **arguments)
78
+ actual_command = unix_command.get_raw_value()
79
+
80
+ logger.debug("--> Running command on localhost: %s", unix_command)
81
+
82
+ if print_input:
83
+ click.echo("{0}>>> {1}".format(self.host.print_prefix, unix_command), err=True)
84
+
85
+ return run_local_process(
86
+ actual_command,
87
+ stdin=_stdin,
88
+ timeout=_timeout,
89
+ print_output=print_output,
90
+ print_prefix=self.host.print_prefix,
91
+ )
92
+
93
+ return_code, combined_output = execute_command_with_sudo_retry(
94
+ self.host,
95
+ arguments,
96
+ execute_command,
97
+ )
98
+
99
+ if _success_exit_codes:
100
+ status = return_code in _success_exit_codes
101
+ else:
102
+ status = return_code == 0
103
+
104
+ return status, combined_output
105
+
106
+ @override
107
+ def put_file(
108
+ self,
109
+ filename_or_io,
110
+ remote_filename,
111
+ remote_temp_filename=None, # ignored
112
+ print_output: bool = False,
113
+ print_input: bool = False,
114
+ **arguments,
115
+ ) -> bool:
116
+ """
117
+ Upload a local file or IO object by copying it to a temporary directory
118
+ and then writing it to the upload location.
119
+
120
+ Returns:
121
+ bool: Indicating success or failure
122
+ """
123
+
124
+ _, temp_filename = mkstemp()
125
+
126
+ try:
127
+ # Load our file or IO object and write it to the temporary file
128
+ with get_file_io(filename_or_io) as file_io:
129
+ with open(temp_filename, "wb") as temp_f:
130
+ data = file_io.read()
131
+
132
+ if isinstance(data, str):
133
+ data = data.encode()
134
+
135
+ temp_f.write(data)
136
+
137
+ # Copy the file using `cp` such that we support sudo/su
138
+ status, output = self.run_shell_command(
139
+ StringCommand("cp", temp_filename, QuoteString(remote_filename)),
140
+ print_output=print_output,
141
+ print_input=print_input,
142
+ **arguments,
143
+ )
144
+
145
+ if not status:
146
+ raise IOError(output.stderr)
147
+ finally:
148
+ os.remove(temp_filename)
149
+
150
+ if print_output:
151
+ click.echo(
152
+ "{0}file copied: {1}".format(self.host.print_prefix, remote_filename),
153
+ err=True,
154
+ )
155
+
156
+ return status
157
+
158
+ @override
159
+ def get_file(
160
+ self,
161
+ remote_filename,
162
+ filename_or_io,
163
+ remote_temp_filename=None, # ignored
164
+ print_output: bool = False,
165
+ print_input: bool = False,
166
+ **arguments,
167
+ ) -> bool:
168
+ """
169
+ Download a local file by copying it to a temporary location and then writing
170
+ it to our filename or IO object.
171
+
172
+ Returns:
173
+ bool: Indicating success or failure
174
+ """
175
+
176
+ _, temp_filename = mkstemp()
177
+
178
+ try:
179
+ # Copy the file using `cp` such that we support sudo/su
180
+ status, output = self.run_shell_command(
181
+ StringCommand("cp", remote_filename, temp_filename),
182
+ print_output=print_output,
183
+ print_input=print_input,
184
+ **arguments,
185
+ )
186
+
187
+ if not status:
188
+ raise IOError(output.stderr)
189
+
190
+ # Load our file or IO object and write it to the temporary file
191
+ with open(temp_filename, "rb") as temp_f:
192
+ with get_file_io(filename_or_io, "wb") as file_io:
193
+ data_bytes: bytes
194
+
195
+ data = temp_f.read()
196
+ if isinstance(data, str):
197
+ data_bytes = data.encode()
198
+ else:
199
+ data_bytes = data
200
+
201
+ file_io.write(data_bytes)
202
+ finally:
203
+ os.remove(temp_filename)
204
+
205
+ if print_output:
206
+ click.echo(
207
+ "{0}file copied: {1}".format(self.host.print_prefix, remote_filename),
208
+ err=True,
209
+ )
210
+
211
+ return True
212
+
213
+ @override
214
+ def check_can_rsync(self) -> None:
215
+ if not which("rsync"):
216
+ raise NotImplementedError("The `rsync` binary is not available on this system.")
217
+
218
+ @override
219
+ def rsync(
220
+ self,
221
+ src,
222
+ dest,
223
+ flags,
224
+ print_output: bool = False,
225
+ print_input: bool = False,
226
+ **arguments,
227
+ ) -> bool:
228
+ status, output = self.run_shell_command(
229
+ StringCommand("rsync", " ".join(flags), src, dest),
230
+ print_output=print_output,
231
+ print_input=print_input,
232
+ **arguments,
233
+ )
234
+
235
+ if not status:
236
+ raise IOError(output.stderr)
237
+
238
+ return True
@@ -0,0 +1 @@
1
+ from .client import SCPClient # noqa: F401
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import ntpath
4
+ import os
5
+ from pathlib import PurePath
6
+ from shlex import quote
7
+ from socket import timeout as SocketTimeoutError
8
+ from typing import IO, AnyStr
9
+
10
+ from paramiko import Channel
11
+ from paramiko.transport import Transport
12
+
13
+ SCP_COMMAND = b"scp"
14
+
15
+
16
+ # Unicode conversion functions; assume UTF-8
17
+ def asbytes(s: bytes | str | PurePath) -> bytes:
18
+ """Turns unicode into bytes, if needed.
19
+
20
+ Assumes UTF-8.
21
+ """
22
+ if isinstance(s, bytes):
23
+ return s
24
+ elif isinstance(s, PurePath):
25
+ return bytes(s)
26
+ else:
27
+ return s.encode("utf-8")
28
+
29
+
30
+ def asunicode(s: bytes | str) -> str:
31
+ """Turns bytes into unicode, if needed.
32
+
33
+ Uses UTF-8.
34
+ """
35
+ if isinstance(s, bytes):
36
+ return s.decode("utf-8", "replace")
37
+ else:
38
+ return s
39
+
40
+
41
+ class SCPClient:
42
+ """
43
+ An scp1 implementation, compatible with openssh scp.
44
+ Raises SCPException for all transport related errors. Local filesystem
45
+ and OS errors pass through.
46
+
47
+ Main public methods are .putfo and .getfo
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ transport: Transport,
53
+ buff_size: int = 16384,
54
+ socket_timeout: float = 10.0,
55
+ ):
56
+ self.transport = transport
57
+ self.buff_size = buff_size
58
+ self.socket_timeout = socket_timeout
59
+ self._channel: Channel | None = None
60
+ self.scp_command = SCP_COMMAND
61
+
62
+ @property
63
+ def channel(self) -> Channel:
64
+ """Return an open Channel, (re)opening if needed."""
65
+ if self._channel is None or self._channel.closed:
66
+ self._channel = self.transport.open_session()
67
+ return self._channel
68
+
69
+ def __enter__(self):
70
+ _ = self.channel # triggers opening if not already open
71
+ return self
72
+
73
+ def __exit__(self, type, value, traceback):
74
+ self.close()
75
+
76
+ def putfo(
77
+ self,
78
+ fl: IO[AnyStr],
79
+ remote_path: str | bytes,
80
+ mode: str | bytes = "0644",
81
+ size: int | None = None,
82
+ ) -> None:
83
+ if size is None:
84
+ pos = fl.tell()
85
+ fl.seek(0, os.SEEK_END) # Seek to end
86
+ size = fl.tell() - pos
87
+ fl.seek(pos, os.SEEK_SET) # Seek back
88
+
89
+ self.channel.settimeout(self.socket_timeout)
90
+ self.channel.exec_command(
91
+ self.scp_command + b" -t " + asbytes(quote(asunicode(remote_path)))
92
+ )
93
+ self._recv_confirm()
94
+ self._send_file(fl, remote_path, mode, size=size)
95
+ self.close()
96
+
97
+ def getfo(self, remote_path: str, fl: IO):
98
+ remote_path_sanitized = quote(remote_path)
99
+ if os.name == "nt":
100
+ remote_file_name = ntpath.basename(remote_path_sanitized)
101
+ else:
102
+ remote_file_name = os.path.basename(remote_path_sanitized)
103
+ self.channel.settimeout(self.socket_timeout)
104
+ self.channel.exec_command(self.scp_command + b" -f " + asbytes(remote_path_sanitized))
105
+ self._recv_all(fl, remote_file_name)
106
+ self.close()
107
+ return fl
108
+
109
+ def close(self):
110
+ """close scp channel"""
111
+ if self._channel is not None:
112
+ self._channel.close()
113
+ self._channel = None
114
+
115
+ def _send_file(self, fl, name, mode, size):
116
+ basename = asbytes(os.path.basename(name))
117
+ # The protocol can't handle \n in the filename.
118
+ # Quote them as the control sequence \^J for now,
119
+ # which is how openssh handles it.
120
+ self.channel.sendall(
121
+ ("C%s %d " % (mode, size)).encode("ascii") + basename.replace(b"\n", b"\\^J") + b"\n"
122
+ )
123
+ self._recv_confirm()
124
+ file_pos = 0
125
+ buff_size = self.buff_size
126
+ chan = self.channel
127
+ while file_pos < size:
128
+ chan.sendall(fl.read(buff_size))
129
+ file_pos = fl.tell()
130
+ chan.sendall(b"\x00")
131
+ self._recv_confirm()
132
+
133
+ def _recv_confirm(self):
134
+ # read scp response
135
+ msg = b""
136
+ try:
137
+ msg = self.channel.recv(512)
138
+ except SocketTimeoutError:
139
+ raise SCPException("Timeout waiting for scp response")
140
+ # slice off the first byte, so this compare will work in py2 and py3
141
+ if msg and msg[0:1] == b"\x00":
142
+ return
143
+ elif msg and msg[0:1] == b"\x01":
144
+ raise SCPException(asunicode(msg[1:]))
145
+ elif self.channel.recv_stderr_ready():
146
+ msg = self.channel.recv_stderr(512)
147
+ raise SCPException(asunicode(msg))
148
+ elif not msg:
149
+ raise SCPException("No response from server")
150
+ else:
151
+ raise SCPException("Invalid response from server", msg)
152
+
153
+ def _recv_all(self, fh: IO, remote_file_name: str) -> None:
154
+ # loop over scp commands, and receive as necessary
155
+ commands = (b"C",)
156
+ while not self.channel.closed:
157
+ # wait for command as long as we're open
158
+ self.channel.sendall(b"\x00")
159
+ msg = self.channel.recv(1024)
160
+ if not msg: # chan closed while receiving
161
+ break
162
+ assert msg[-1:] == b"\n"
163
+ msg = msg[:-1]
164
+ code = msg[0:1]
165
+ if code not in commands:
166
+ raise SCPException(asunicode(msg[1:]))
167
+ self._recv_file(msg[1:], fh, remote_file_name)
168
+
169
+ def _recv_file(self, cmd: bytes, fh: IO, remote_file_name: str) -> None:
170
+ chan = self.channel
171
+ parts = cmd.strip().split(b" ", 2)
172
+
173
+ try:
174
+ size = int(parts[1])
175
+ except (ValueError, IndexError):
176
+ chan.send(b"\x01")
177
+ chan.close()
178
+ raise SCPException("Bad file format")
179
+
180
+ buff_size = self.buff_size
181
+ pos = 0
182
+ chan.send(b"\x00")
183
+ try:
184
+ while pos < size:
185
+ # we have to make sure we don't read the final byte
186
+ if size - pos <= buff_size:
187
+ buff_size = size - pos
188
+ data = chan.recv(buff_size)
189
+ if not data:
190
+ raise SCPException("Underlying channel was closed")
191
+ fh.write(data)
192
+ pos = fh.tell()
193
+ msg = chan.recv(512)
194
+ if msg and msg[0:1] != b"\x00":
195
+ raise SCPException(asunicode(msg[1:]))
196
+ except SocketTimeoutError:
197
+ chan.close()
198
+ raise SCPException("Error receiving, socket.timeout")
199
+
200
+
201
+ class SCPException(Exception):
202
+ """SCP exception class"""
203
+
204
+ pass