fabric / fabric
1
"""
2
File transfer via SFTP and/or SCP.
3
"""
4

5 6
import os
6 6
import posixpath
7 6
import stat
8

9 6
from .util import debug  # TODO: actual logging! LOL
10

11
# TODO: figure out best way to direct folks seeking rsync, to patchwork's rsync
12
# call (which needs updating to use invoke.run() & fab 2 connection methods,
13
# but is otherwise suitable).
14
# UNLESS we want to try and shoehorn it into this module after all? Delegate
15
# any recursive get/put to it? Requires users to have rsync available of
16
# course.
17

18

19 6
class Transfer(object):
20
    """
21
    `.Connection`-wrapping class responsible for managing file upload/download.
22

23
    .. versionadded:: 2.0
24
    """
25

26
    # TODO: SFTP clear default, but how to do SCP? subclass? init kwarg?
27

28 6
    def __init__(self, connection):
29 6
        self.connection = connection
30

31 6
    @property
32 2
    def sftp(self):
33 6
        return self.connection.sftp()
34

35 6
    def is_remote_dir(self, path):
36 6
        try:
37 6
            return stat.S_ISDIR(self.sftp.stat(path).st_mode)
38 6
        except IOError:
39 6
            return False
40

41 6
    def get(self, remote, local=None, preserve_mode=True):
42
        """
43
        Download a file from the current connection to the local filesystem.
44

45
        :param str remote:
46
            Remote file to download.
47

48
            May be absolute, or relative to the remote working directory.
49

50
            .. note::
51
                Most SFTP servers set the remote working directory to the
52
                connecting user's home directory, and (unlike most shells) do
53
                *not* expand tildes (``~``).
54

55
                For example, instead of saying ``get("~/tmp/archive.tgz")``,
56
                say ``get("tmp/archive.tgz")``.
57

58
        :param local:
59
            Local path to store downloaded file in, or a file-like object.
60

61
            **If None or another 'falsey'/empty value is given** (the default),
62
            the remote file is downloaded to the current working directory (as
63
            seen by `os.getcwd`) using its remote filename.
64

65
            **If a string is given**, it should be a path to a local directory
66
            or file and is subject to similar behavior as that seen by common
67
            Unix utilities or OpenSSH's ``sftp`` or ``scp`` tools.
68

69
            For example, if the local path is a directory, the remote path's
70
            base filename will be added onto it (so ``get('foo/bar/file.txt',
71
            '/tmp/')`` would result in creation or overwriting of
72
            ``/tmp/file.txt``).
73

74
            .. note::
75
                When dealing with nonexistent file paths, normal Python file
76
                handling concerns come into play - for example, a ``local``
77
                path containing non-leaf directories which do not exist, will
78
                typically result in an `OSError`.
79

80
            **If a file-like object is given**, the contents of the remote file
81
            are simply written into it.
82

83
        :param bool preserve_mode:
84
            Whether to `os.chmod` the local file so it matches the remote
85
            file's mode (default: ``True``).
86

87
        :returns: A `.Result` object.
88

89
        .. versionadded:: 2.0
90
        """
91
        # TODO: how does this API change if we want to implement
92
        # remote-to-remote file transfer? (Is that even realistic?)
93
        # TODO: handle v1's string interpolation bits, especially the default
94
        # one, or at least think about how that would work re: split between
95
        # single and multiple server targets.
96
        # TODO: callback support
97
        # TODO: how best to allow changing the behavior/semantics of
98
        # remote/local (e.g. users might want 'safer' behavior that complains
99
        # instead of overwriting existing files) - this likely ties into the
100
        # "how to handle recursive/rsync" and "how to handle scp" questions
101

102
        # Massage remote path
103 6
        if not remote:
104 6
            raise ValueError("Remote path must not be empty!")
105 6
        orig_remote = remote
106 6
        remote = posixpath.join(
107
            self.sftp.getcwd() or self.sftp.normalize("."), remote
108
        )
109

110
        # Massage local path:
111
        # - handle file-ness
112
        # - if path, fill with remote name if empty, & make absolute
113 6
        orig_local = local
114 6
        is_file_like = hasattr(local, "write") and callable(local.write)
115 6
        if not local:
116 6
            local = posixpath.basename(remote)
117 6
        if not is_file_like:
118 6
            local = os.path.abspath(local)
119

120
        # Run Paramiko-level .get() (side-effects only. womp.)
121
        # TODO: push some of the path handling into Paramiko; it should be
122
        # responsible for dealing with path cleaning etc.
123
        # TODO: probably preserve warning message from v1 when overwriting
124
        # existing files. Use logging for that obviously.
125
        #
126
        # If local appears to be a file-like object, use sftp.getfo, not get
127 6
        if is_file_like:
128 6
            self.sftp.getfo(remotepath=remote, fl=local)
129
        else:
130 6
            self.sftp.get(remotepath=remote, localpath=local)
131
            # Set mode to same as remote end
132
            # TODO: Push this down into SFTPClient sometime (requires backwards
133
            # incompat release.)
134 6
            if preserve_mode:
135 6
                remote_mode = self.sftp.stat(remote).st_mode
136 6
                mode = stat.S_IMODE(remote_mode)
137 6
                os.chmod(local, mode)
138
        # Return something useful
139 6
        return Result(
140
            orig_remote=orig_remote,
141
            remote=remote,
142
            orig_local=orig_local,
143
            local=local,
144
            connection=self.connection,
145
        )
146

147 6
    def put(self, local, remote=None, preserve_mode=True):
148
        """
149
        Upload a file from the local filesystem to the current connection.
150

151
        :param local:
152
            Local path of file to upload, or a file-like object.
153

154
            **If a string is given**, it should be a path to a local (regular)
155
            file (not a directory).
156

157
            .. note::
158
                When dealing with nonexistent file paths, normal Python file
159
                handling concerns come into play - for example, trying to
160
                upload a nonexistent ``local`` path will typically result in an
161
                `OSError`.
162

163
            **If a file-like object is given**, its contents are written to the
164
            remote file path.
165

166
        :param str remote:
167
            Remote path to which the local file will be written.
168

169
            .. note::
170
                Most SFTP servers set the remote working directory to the
171
                connecting user's home directory, and (unlike most shells) do
172
                *not* expand tildes (``~``).
173

174
                For example, instead of saying ``put("archive.tgz",
175
                "~/tmp/")``, say ``put("archive.tgz", "tmp/")``.
176

177
                In addition, this means that 'falsey'/empty values (such as the
178
                default value, ``None``) are allowed and result in uploading to
179
                the remote home directory.
180

181
            .. note::
182
                When ``local`` is a file-like object, ``remote`` is required
183
                and must refer to a valid file path (not a directory).
184

185
        :param bool preserve_mode:
186
            Whether to ``chmod`` the remote file so it matches the local file's
187
            mode (default: ``True``).
188

189
        :returns: A `.Result` object.
190

191
        .. versionadded:: 2.0
192
        """
193 6
        if not local:
194 6
            raise ValueError("Local path must not be empty!")
195

196 6
        is_file_like = hasattr(local, "write") and callable(local.write)
197

198
        # Massage remote path
199 6
        orig_remote = remote
200 6
        if is_file_like:
201 6
            local_base = getattr(local, "name", None)
202
        else:
203 6
            local_base = os.path.basename(local)
204 6
        if not remote:
205 6
            if is_file_like:
206 6
                raise ValueError(
207
                    "Must give non-empty remote path when local is a file-like object!"  # noqa
208
                )
209
            else:
210 6
                remote = local_base
211 6
                debug("Massaged empty remote path into {!r}".format(remote))
212 6
        elif self.is_remote_dir(remote):
213
            # non-empty local_base implies a) text file path or b) FLO which
214
            # had a non-empty .name attribute. huzzah!
215 6
            if local_base:
216 6
                remote = posixpath.join(remote, local_base)
217
            else:
218 6
                if is_file_like:
219 6
                    raise ValueError(
220
                        "Can't put a file-like-object into a directory unless it has a non-empty .name attribute!"  # noqa
221
                    )
222
                else:
223
                    # TODO: can we ever really end up here? implies we want to
224
                    # reorganize all this logic so it has fewer potential holes
225 0
                    raise ValueError(
226
                        "Somehow got an empty local file basename ({!r}) when uploading to a directory ({!r})!".format(  # noqa
227
                            local_base, remote
228
                        )
229
                    )
230

231 6
        prejoined_remote = remote
232 6
        remote = posixpath.join(
233
            self.sftp.getcwd() or self.sftp.normalize("."), remote
234
        )
235 6
        if remote != prejoined_remote:
236 6
            msg = "Massaged relative remote path {!r} into {!r}"
237 6
            debug(msg.format(prejoined_remote, remote))
238

239
        # Massage local path
240 6
        orig_local = local
241 6
        if not is_file_like:
242 6
            local = os.path.abspath(local)
243 6
            if local != orig_local:
244 6
                debug(
245
                    "Massaged relative local path {!r} into {!r}".format(
246
                        orig_local, local
247
                    )
248
                )  # noqa
249

250
        # Run Paramiko-level .put() (side-effects only. womp.)
251
        # TODO: push some of the path handling into Paramiko; it should be
252
        # responsible for dealing with path cleaning etc.
253
        # TODO: probably preserve warning message from v1 when overwriting
254
        # existing files. Use logging for that obviously.
255
        #
256
        # If local appears to be a file-like object, use sftp.putfo, not put
257 6
        if is_file_like:
258 6
            msg = "Uploading file-like object {!r} to {!r}"
259 6
            debug(msg.format(local, remote))
260 6
            pointer = local.tell()
261 6
            try:
262 6
                local.seek(0)
263 6
                self.sftp.putfo(fl=local, remotepath=remote)
264
            finally:
265 6
                local.seek(pointer)
266
        else:
267 6
            debug("Uploading {!r} to {!r}".format(local, remote))
268 6
            self.sftp.put(localpath=local, remotepath=remote)
269
            # Set mode to same as local end
270
            # TODO: Push this down into SFTPClient sometime (requires backwards
271
            # incompat release.)
272 6
            if preserve_mode:
273 6
                local_mode = os.stat(local).st_mode
274 6
                mode = stat.S_IMODE(local_mode)
275 6
                self.sftp.chmod(remote, mode)
276
        # Return something useful
277 6
        return Result(
278
            orig_remote=orig_remote,
279
            remote=remote,
280
            orig_local=orig_local,
281
            local=local,
282
            connection=self.connection,
283
        )
284

285

286 6
class Result(object):
287
    """
288
    A container for information about the result of a file transfer.
289

290
    See individual attribute/method documentation below for details.
291

292
    .. note::
293
        Unlike similar classes such as `invoke.runners.Result` or
294
        `fabric.runners.Result` (which have a concept of "warn and return
295
        anyways on failure") this class has no useful truthiness behavior. If a
296
        file transfer fails, some exception will be raised, either an `OSError`
297
        or an error from within Paramiko.
298

299
    .. versionadded:: 2.0
300
    """
301

302
    # TODO: how does this differ from put vs get? field stating which? (feels
303
    # meh) distinct classes differing, for now, solely by name? (also meh)
304 6
    def __init__(self, local, orig_local, remote, orig_remote, connection):
305
        #: The local path the file was saved as, or the object it was saved
306
        #: into if a file-like object was given instead.
307
        #:
308
        #: If a string path, this value is massaged to be absolute; see
309
        #: `.orig_local` for the original argument value.
310 6
        self.local = local
311
        #: The original value given as the returning method's ``local``
312
        #: argument.
313 6
        self.orig_local = orig_local
314
        #: The remote path downloaded from. Massaged to be absolute; see
315
        #: `.orig_remote` for the original argument value.
316 6
        self.remote = remote
317
        #: The original argument value given as the returning method's
318
        #: ``remote`` argument.
319 6
        self.orig_remote = orig_remote
320
        #: The `.Connection` object this result was obtained from.
321 6
        self.connection = connection
322

323
    # TODO: ensure str/repr makes it easily differentiable from run() or
324
    # local() result objects (and vice versa).

Read our documentation on viewing source code .

Loading