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

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

9 1
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 1
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 1
    def __init__(self, connection):
29 1
        self.connection = connection
30

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

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

41 1
    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 1
        if not remote:
104 1
            raise ValueError("Remote path must not be empty!")
105 1
        orig_remote = remote
106 1
        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 1
        orig_local = local
114 1
        is_file_like = hasattr(local, "write") and callable(local.write)
115 1
        if not local:
116 1
            local = posixpath.basename(remote)
117 1
        if not is_file_like:
118 1
            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 1
        if is_file_like:
128 1
            self.sftp.getfo(remotepath=remote, fl=local)
129
        else:
130 1
            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 1
            if preserve_mode:
135 1
                remote_mode = self.sftp.stat(remote).st_mode
136 1
                mode = stat.S_IMODE(remote_mode)
137 1
                os.chmod(local, mode)
138
        # Return something useful
139 1
        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 1
    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 1
        if not local:
194 1
            raise ValueError("Local path must not be empty!")
195

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

198
        # Massage remote path
199 1
        orig_remote = remote
200 1
        if is_file_like:
201 1
            local_base = getattr(local, "name", None)
202
        else:
203 1
            local_base = os.path.basename(local)
204 1
        if not remote:
205 1
            if is_file_like:
206 1
                raise ValueError(
207
                    "Must give non-empty remote path when local is a file-like object!"  # noqa
208
                )
209
            else:
210 1
                remote = local_base
211 1
                debug("Massaged empty remote path into {!r}".format(remote))
212 1
        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 1
            if local_base:
216 1
                remote = posixpath.join(remote, local_base)
217
            else:
218 1
                if is_file_like:
219 1
                    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 1
        prejoined_remote = remote
232 1
        remote = posixpath.join(
233
            self.sftp.getcwd() or self.sftp.normalize("."), remote
234
        )
235 1
        if remote != prejoined_remote:
236 1
            msg = "Massaged relative remote path {!r} into {!r}"
237 1
            debug(msg.format(prejoined_remote, remote))
238

239
        # Massage local path
240 1
        orig_local = local
241 1
        if not is_file_like:
242 1
            local = os.path.abspath(local)
243 1
            if local != orig_local:
244 1
                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 1
        if is_file_like:
258 1
            msg = "Uploading file-like object {!r} to {!r}"
259 1
            debug(msg.format(local, remote))
260 1
            pointer = local.tell()
261 1
            try:
262 1
                local.seek(0)
263 1
                self.sftp.putfo(fl=local, remotepath=remote)
264
            finally:
265 1
                local.seek(pointer)
266
        else:
267 1
            debug("Uploading {!r} to {!r}".format(local, remote))
268 1
            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 1
            if preserve_mode:
273 1
                local_mode = os.stat(local).st_mode
274 1
                mode = stat.S_IMODE(local_mode)
275 1
                self.sftp.chmod(remote, mode)
276
        # Return something useful
277 1
        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 1
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 1
    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 1
        self.local = local
311
        #: The original value given as the returning method's ``local``
312
        #: argument.
313 1
        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 1
        self.remote = remote
317
        #: The original argument value given as the returning method's
318
        #: ``remote`` argument.
319 1
        self.orig_remote = orig_remote
320
        #: The `.Connection` object this result was obtained from.
321 1
        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