~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
"""Implementation of Transport over SFTP, using paramiko."""
19
19
 
 
20
# TODO: Remove the transport-based lock_read and lock_write methods.  They'll
 
21
# then raise TransportNotPossible, which will break remote access to any
 
22
# formats which rely on OS-level locks.  That should be fine as those formats
 
23
# are pretty old, but these combinations may have to be removed from the test
 
24
# suite.  Those formats all date back to 0.7; so we should be able to remove
 
25
# these methods when we officially drop support for those formats.
 
26
 
20
27
import errno
21
28
import os
22
29
import random
23
30
import select
24
31
import socket
25
32
import stat
26
 
import subprocess
27
33
import sys
28
34
import time
29
35
import urllib
30
36
import urlparse
31
37
import weakref
32
38
 
33
 
from bzrlib.errors import (FileExists, 
 
39
from bzrlib import (
 
40
    errors,
 
41
    urlutils,
 
42
    )
 
43
from bzrlib.errors import (FileExists,
34
44
                           NoSuchFile, PathNotChild,
35
45
                           TransportError,
36
 
                           LockError, 
 
46
                           LockError,
37
47
                           PathError,
38
48
                           ParamikoNotPresent,
39
 
                           UnknownSSH,
40
49
                           )
41
50
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
42
51
from bzrlib.trace import mutter, warning
47
56
    ssh,
48
57
    Transport,
49
58
    )
50
 
import bzrlib.urlutils as urlutils
51
59
 
52
60
try:
53
61
    import paramiko
85
93
 
86
94
 
87
95
class SFTPLock(object):
88
 
    """This fakes a lock in a remote location."""
 
96
    """This fakes a lock in a remote location.
 
97
    
 
98
    A present lock is indicated just by the existence of a file.  This
 
99
    doesn't work well on all transports and they are only used in 
 
100
    deprecated storage formats.
 
101
    """
 
102
    
89
103
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
 
104
 
90
105
    def __init__(self, path, transport):
91
106
        assert isinstance(transport, SFTPTransport)
92
107
 
119
134
            pass
120
135
 
121
136
 
122
 
class SFTPTransport(Transport):
 
137
class SFTPUrlHandling(Transport):
 
138
    """Mix-in that does common handling of SSH/SFTP URLs."""
 
139
 
 
140
    def __init__(self, base):
 
141
        self._parse_url(base)
 
142
        base = self._unparse_url(self._path)
 
143
        if base[-1] != '/':
 
144
            base += '/'
 
145
        super(SFTPUrlHandling, self).__init__(base)
 
146
 
 
147
    def _parse_url(self, url):
 
148
        (self._scheme,
 
149
         self._username, self._password,
 
150
         self._host, self._port, self._path) = self._split_url(url)
 
151
 
 
152
    def _unparse_url(self, path):
 
153
        """Return a URL for a path relative to this transport.
 
154
        """
 
155
        path = urllib.quote(path)
 
156
        # handle homedir paths
 
157
        if not path.startswith('/'):
 
158
            path = "/~/" + path
 
159
        netloc = urllib.quote(self._host)
 
160
        if self._username is not None:
 
161
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
162
        if self._port is not None:
 
163
            netloc = '%s:%d' % (netloc, self._port)
 
164
        return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
 
165
 
 
166
    def _split_url(self, url):
 
167
        (scheme, username, password, host, port, path) = split_url(url)
 
168
        ## assert scheme == 'sftp'
 
169
 
 
170
        # the initial slash should be removed from the path, and treated
 
171
        # as a homedir relative path (the path begins with a double slash
 
172
        # if it is absolute).
 
173
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
174
        # RBC 20060118 we are not using this as its too user hostile. instead
 
175
        # we are following lftp and using /~/foo to mean '~/foo'.
 
176
        # handle homedir paths
 
177
        if path.startswith('/~/'):
 
178
            path = path[3:]
 
179
        elif path == '/~':
 
180
            path = ''
 
181
        return (scheme, username, password, host, port, path)
 
182
 
 
183
    def abspath(self, relpath):
 
184
        """Return the full url to the given relative path.
 
185
        
 
186
        @param relpath: the relative path or path components
 
187
        @type relpath: str or list
 
188
        """
 
189
        return self._unparse_url(self._remote_path(relpath))
 
190
    
 
191
    def _remote_path(self, relpath):
 
192
        """Return the path to be passed along the sftp protocol for relpath.
 
193
        
 
194
        :param relpath: is a urlencoded string.
 
195
        """
 
196
        return self._combine_paths(self._path, relpath)
 
197
 
 
198
 
 
199
class SFTPTransport(SFTPUrlHandling):
123
200
    """Transport implementation for SFTP access."""
124
201
 
125
202
    _do_prefetch = _default_do_prefetch
141
218
    _max_request_size = 32768
142
219
 
143
220
    def __init__(self, base, clone_from=None):
144
 
        assert base.startswith('sftp://')
145
 
        self._parse_url(base)
146
 
        base = self._unparse_url()
147
 
        if base[-1] != '/':
148
 
            base += '/'
149
221
        super(SFTPTransport, self).__init__(base)
150
222
        if clone_from is None:
151
223
            self._sftp_connect()
171
243
        else:
172
244
            return SFTPTransport(self.abspath(offset), self)
173
245
 
174
 
    def abspath(self, relpath):
175
 
        """
176
 
        Return the full url to the given relative path.
177
 
        
178
 
        @param relpath: the relative path or path components
179
 
        @type relpath: str or list
180
 
        """
181
 
        return self._unparse_url(self._remote_path(relpath))
182
 
    
183
246
    def _remote_path(self, relpath):
184
247
        """Return the path to be passed along the sftp protocol for relpath.
185
248
        
186
249
        relpath is a urlencoded string.
 
250
 
 
251
        :return: a path prefixed with / for regular abspath-based urls, or a
 
252
            path that does not begin with / for urls which begin with /~/.
187
253
        """
188
 
        # FIXME: share the common code across transports
 
254
        # how does this work? 
 
255
        # it processes relpath with respect to 
 
256
        # our state:
 
257
        # firstly we create a path to evaluate: 
 
258
        # if relpath is an abspath or homedir path, its the entire thing
 
259
        # otherwise we join our base with relpath
 
260
        # then we eliminate all empty segments (double //'s) outside the first
 
261
        # two elements of the list. This avoids problems with trailing 
 
262
        # slashes, or other abnormalities.
 
263
        # finally we evaluate the entire path in a single pass
 
264
        # '.'s are stripped,
 
265
        # '..' result in popping the left most already 
 
266
        # processed path (which can never be empty because of the check for
 
267
        # abspath and homedir meaning that its not, or that we've used our
 
268
        # path. If the pop would pop the root, we ignore it.
 
269
 
 
270
        # Specific case examinations:
 
271
        # remove the special casefor ~: if the current root is ~/ popping of it
 
272
        # = / thus our seed for a ~ based path is ['', '~']
 
273
        # and if we end up with [''] then we had basically ('', '..') (which is
 
274
        # '/..' so we append '' if the length is one, and assert that the first
 
275
        # element is still ''. Lastly, if we end with ['', '~'] as a prefix for
 
276
        # the output, we've got a homedir path, so we strip that prefix before
 
277
        # '/' joining the resulting list.
 
278
        #
 
279
        # case one: '/' -> ['', ''] cannot shrink
 
280
        # case two: '/' + '../foo' -> ['', 'foo'] (take '', '', '..', 'foo')
 
281
        #           and pop the second '' for the '..', append 'foo'
 
282
        # case three: '/~/' -> ['', '~', ''] 
 
283
        # case four: '/~/' + '../foo' -> ['', '~', '', '..', 'foo'],
 
284
        #           and we want to get '/foo' - the empty path in the middle
 
285
        #           needs to be stripped, then normal path manipulation will 
 
286
        #           work.
 
287
        # case five: '/..' ['', '..'], we want ['', '']
 
288
        #            stripping '' outside the first two is ok
 
289
        #            ignore .. if its too high up
 
290
        #
 
291
        # lastly this code is possibly reusable by FTP, but not reusable by
 
292
        # local paths: ~ is resolvable correctly, nor by HTTP or the smart
 
293
        # server: ~ is resolved remotely.
 
294
        # 
 
295
        # however, a version of this that acts on self.base is possible to be
 
296
        # written which manipulates the URL in canonical form, and would be
 
297
        # reusable for all transports, if a flag for allowing ~/ at all was
 
298
        # provided.
189
299
        assert isinstance(relpath, basestring)
190
 
        relpath = urlutils.unescape(relpath).split('/')
191
 
        basepath = self._path.split('/')
192
 
        if len(basepath) > 0 and basepath[-1] == '':
193
 
            basepath = basepath[:-1]
194
 
 
195
 
        for p in relpath:
196
 
            if p == '..':
197
 
                if len(basepath) == 0:
198
 
                    # In most filesystems, a request for the parent
199
 
                    # of root, just returns root.
200
 
                    continue
201
 
                basepath.pop()
202
 
            elif p == '.':
203
 
                continue # No-op
204
 
            else:
205
 
                basepath.append(p)
206
 
 
207
 
        path = '/'.join(basepath)
208
 
        # mutter('relpath => remotepath %s => %s', relpath, path)
 
300
        relpath = urlutils.unescape(relpath)
 
301
 
 
302
        # case 1)
 
303
        if relpath.startswith('/'):
 
304
            # abspath - normal split is fine.
 
305
            current_path = relpath.split('/')
 
306
        elif relpath.startswith('~/'):
 
307
            # root is homedir based: normal split and prefix '' to remote the
 
308
            # special case
 
309
            current_path = [''].extend(relpath.split('/'))
 
310
        else:
 
311
            # root is from the current directory:
 
312
            if self._path.startswith('/'):
 
313
                # abspath, take the regular split
 
314
                current_path = []
 
315
            else:
 
316
                # homedir based, add the '', '~' not present in self._path
 
317
                current_path = ['', '~']
 
318
            # add our current dir
 
319
            current_path.extend(self._path.split('/'))
 
320
            # add the users relpath
 
321
            current_path.extend(relpath.split('/'))
 
322
        # strip '' segments that are not in the first one - the leading /.
 
323
        to_process = current_path[:1]
 
324
        for segment in current_path[1:]:
 
325
            if segment != '':
 
326
                to_process.append(segment)
 
327
 
 
328
        # process '.' and '..' segments into output_path.
 
329
        output_path = []
 
330
        for segment in to_process:
 
331
            if segment == '..':
 
332
                # directory pop. Remove a directory 
 
333
                # as long as we are not at the root
 
334
                if len(output_path) > 1:
 
335
                    output_path.pop()
 
336
                # else: pass
 
337
                # cannot pop beyond the root, so do nothing
 
338
            elif segment == '.':
 
339
                continue # strip the '.' from the output.
 
340
            else:
 
341
                # this will append '' to output_path for the root elements,
 
342
                # which is appropriate: its why we strip '' in the first pass.
 
343
                output_path.append(segment)
 
344
 
 
345
        # check output special cases:
 
346
        if output_path == ['']:
 
347
            # [''] -> ['', '']
 
348
            output_path = ['', '']
 
349
        elif output_path[:2] == ['', '~']:
 
350
            # ['', '~', ...] -> ...
 
351
            output_path = output_path[2:]
 
352
        path = '/'.join(output_path)
209
353
        return path
210
354
 
211
355
    def relpath(self, abspath):
212
 
        username, password, host, port, path = self._split_url(abspath)
 
356
        scheme, username, password, host, port, path = self._split_url(abspath)
213
357
        error = []
214
358
        if (username != self._username):
215
359
            error.append('username mismatch')
263
407
            fp = self._sftp.file(path, mode='rb')
264
408
            readv = getattr(fp, 'readv', None)
265
409
            if readv:
266
 
                return self._sftp_readv(fp, offsets)
 
410
                return self._sftp_readv(fp, offsets, relpath)
267
411
            mutter('seek and read %s offsets', len(offsets))
268
 
            return self._seek_and_read(fp, offsets)
 
412
            return self._seek_and_read(fp, offsets, relpath)
269
413
        except (IOError, paramiko.SSHException), e:
270
414
            self._translate_io_exception(e, path, ': error retrieving')
271
415
 
272
 
    def _sftp_readv(self, fp, offsets):
 
416
    def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
273
417
        """Use the readv() member of fp to do async readv.
274
418
 
275
419
        And then read them using paramiko.readv(). paramiko.readv()
362
506
                yield cur_offset_and_size[0], this_data
363
507
                cur_offset_and_size = offset_stack.next()
364
508
 
 
509
            # We read a coalesced entry, so mark it as done
 
510
            cur_coalesced = None
365
511
            # Now that we've read all of the data for this coalesced section
366
512
            # on to the next
367
513
            cur_coalesced = cur_coalesced_stack.next()
368
514
 
 
515
        if cur_coalesced is not None:
 
516
            raise errors.ShortReadvError(relpath, cur_coalesced.start,
 
517
                cur_coalesced.length, len(data))
 
518
 
369
519
    def put_file(self, relpath, f, mode=None):
370
520
        """
371
521
        Copy the file-like object into the location.
673
823
        # that we have taken the lock.
674
824
        return SFTPLock(relpath, self)
675
825
 
676
 
    def _unparse_url(self, path=None):
677
 
        if path is None:
678
 
            path = self._path
679
 
        path = urllib.quote(path)
680
 
        # handle homedir paths
681
 
        if not path.startswith('/'):
682
 
            path = "/~/" + path
683
 
        netloc = urllib.quote(self._host)
684
 
        if self._username is not None:
685
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
686
 
        if self._port is not None:
687
 
            netloc = '%s:%d' % (netloc, self._port)
688
 
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
689
 
 
690
 
    def _split_url(self, url):
691
 
        (scheme, username, password, host, port, path) = split_url(url)
692
 
        assert scheme == 'sftp'
693
 
 
694
 
        # the initial slash should be removed from the path, and treated
695
 
        # as a homedir relative path (the path begins with a double slash
696
 
        # if it is absolute).
697
 
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
698
 
        # RBC 20060118 we are not using this as its too user hostile. instead
699
 
        # we are following lftp and using /~/foo to mean '~/foo'.
700
 
        # handle homedir paths
701
 
        if path.startswith('/~/'):
702
 
            path = path[3:]
703
 
        elif path == '/~':
704
 
            path = ''
705
 
        return (username, password, host, port, path)
706
 
 
707
 
    def _parse_url(self, url):
708
 
        (self._username, self._password,
709
 
         self._host, self._port, self._path) = self._split_url(url)
710
 
 
711
826
    def _sftp_connect(self):
712
827
        """Connect to the remote sftp server.
713
828
        After this, self._sftp should have a valid connection (or
907
1022
class SFTPServer(Server):
908
1023
    """Common code for SFTP server facilities."""
909
1024
 
910
 
    def __init__(self):
 
1025
    def __init__(self, server_interface=StubServer):
911
1026
        self._original_vendor = None
912
1027
        self._homedir = None
913
1028
        self._server_homedir = None
914
1029
        self._listener = None
915
1030
        self._root = None
916
1031
        self._vendor = ssh.ParamikoVendor()
 
1032
        self._server_interface = server_interface
917
1033
        # sftp server logs
918
1034
        self.logs = []
919
1035
        self.add_latency = 0
944
1060
        f.close()
945
1061
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
946
1062
        ssh_server.add_server_key(host_key)
947
 
        server = StubServer(self)
 
1063
        server = self._server_interface(self)
948
1064
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
949
1065
                                         StubSFTPServer, root=self._root,
950
1066
                                         home=self._server_homedir)
1004
1120
        # Re-import these as locals, so that they're still accessible during
1005
1121
        # interpreter shutdown (when all module globals get set to None, leading
1006
1122
        # to confusing errors like "'NoneType' object has no attribute 'error'".
1007
 
        import socket, errno
1008
1123
        class FakeChannel(object):
1009
1124
            def get_transport(self):
1010
1125
                return self