/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« 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.
426
576
            raise
427
577
 
428
578
    def _put_non_atomic_helper(self, relpath, writer, mode=None,
429
 
                               create_parent_dir=False):
 
579
                               create_parent_dir=False,
 
580
                               dir_mode=None):
430
581
        abspath = self._remote_path(relpath)
431
582
 
432
583
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
465
616
            # Try to create the parent directory, and then go back to
466
617
            # writing the file
467
618
            parent_dir = os.path.dirname(abspath)
468
 
            try:
469
 
                self._sftp.mkdir(parent_dir)
470
 
            except (paramiko.SSHException, IOError), e:
471
 
                self._translate_io_exception(e, abspath, ': unable to open')
 
619
            self._mkdir(parent_dir, dir_mode)
472
620
            _open_and_write_file()
473
621
 
474
622
    def put_file_non_atomic(self, relpath, f, mode=None,
475
 
                            create_parent_dir=False):
 
623
                            create_parent_dir=False,
 
624
                            dir_mode=None):
476
625
        """Copy the file-like object into the target location.
477
626
 
478
627
        This function is not strictly safe to use. It is only meant to
491
640
        def writer(fout):
492
641
            self._pump(f, fout)
493
642
        self._put_non_atomic_helper(relpath, writer, mode=mode,
494
 
                                    create_parent_dir=create_parent_dir)
 
643
                                    create_parent_dir=create_parent_dir,
 
644
                                    dir_mode=dir_mode)
495
645
 
496
646
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
497
 
                             create_parent_dir=False):
 
647
                             create_parent_dir=False,
 
648
                             dir_mode=None):
498
649
        def writer(fout):
499
650
            fout.write(bytes)
500
651
        self._put_non_atomic_helper(relpath, writer, mode=mode,
501
 
                                    create_parent_dir=create_parent_dir)
 
652
                                    create_parent_dir=create_parent_dir,
 
653
                                    dir_mode=dir_mode)
502
654
 
503
655
    def iter_files_recursive(self):
504
656
        """Walk the relative paths of all files in this transport."""
512
664
            else:
513
665
                yield relpath
514
666
 
 
667
    def _mkdir(self, abspath, mode=None):
 
668
        if mode is None:
 
669
            local_mode = 0777
 
670
        else:
 
671
            local_mode = mode
 
672
        try:
 
673
            self._sftp.mkdir(abspath, local_mode)
 
674
            if mode is not None:
 
675
                self._sftp.chmod(abspath, mode=mode)
 
676
        except (paramiko.SSHException, IOError), e:
 
677
            self._translate_io_exception(e, abspath, ': unable to mkdir',
 
678
                failure_exc=FileExists)
 
679
 
515
680
    def mkdir(self, relpath, mode=None):
516
681
        """Create a directory at the given path."""
517
 
        path = self._remote_path(relpath)
518
 
        try:
519
 
            self._sftp.mkdir(path)
520
 
            if mode is not None:
521
 
                self._sftp.chmod(path, mode=mode)
522
 
        except (paramiko.SSHException, IOError), e:
523
 
            self._translate_io_exception(e, path, ': unable to mkdir',
524
 
                failure_exc=FileExists)
 
682
        self._mkdir(self._remote_path(relpath), mode=mode)
525
683
 
526
684
    def _translate_io_exception(self, e, path, more_info='', 
527
685
                                failure_exc=PathError):
539
697
        """
540
698
        # paramiko seems to generate detailless errors.
541
699
        self._translate_error(e, path, raise_generic=False)
542
 
        if hasattr(e, 'args'):
 
700
        if getattr(e, 'args', None) is not None:
543
701
            if (e.args == ('No such file or directory',) or
544
702
                e.args == ('No such file',)):
545
703
                raise NoSuchFile(path, str(e) + more_info)
549
707
            if (e.args == ('Failure',)):
550
708
                raise failure_exc(path, str(e) + more_info)
551
709
            mutter('Raising exception with args %s', e.args)
552
 
        if hasattr(e, 'errno'):
 
710
        if getattr(e, 'errno', None) is not None:
553
711
            mutter('Raising exception with errno %s', e.errno)
554
712
        raise e
555
713
 
665
823
        # that we have taken the lock.
666
824
        return SFTPLock(relpath, self)
667
825
 
668
 
    def _unparse_url(self, path=None):
669
 
        if path is None:
670
 
            path = self._path
671
 
        path = urllib.quote(path)
672
 
        # handle homedir paths
673
 
        if not path.startswith('/'):
674
 
            path = "/~/" + path
675
 
        netloc = urllib.quote(self._host)
676
 
        if self._username is not None:
677
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
678
 
        if self._port is not None:
679
 
            netloc = '%s:%d' % (netloc, self._port)
680
 
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
681
 
 
682
 
    def _split_url(self, url):
683
 
        (scheme, username, password, host, port, path) = split_url(url)
684
 
        assert scheme == 'sftp'
685
 
 
686
 
        # the initial slash should be removed from the path, and treated
687
 
        # as a homedir relative path (the path begins with a double slash
688
 
        # if it is absolute).
689
 
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
690
 
        # RBC 20060118 we are not using this as its too user hostile. instead
691
 
        # we are following lftp and using /~/foo to mean '~/foo'.
692
 
        # handle homedir paths
693
 
        if path.startswith('/~/'):
694
 
            path = path[3:]
695
 
        elif path == '/~':
696
 
            path = ''
697
 
        return (username, password, host, port, path)
698
 
 
699
 
    def _parse_url(self, url):
700
 
        (self._username, self._password,
701
 
         self._host, self._port, self._path) = self._split_url(url)
702
 
 
703
826
    def _sftp_connect(self):
704
827
        """Connect to the remote sftp server.
705
828
        After this, self._sftp should have a valid connection (or
745
868
            self._translate_io_exception(e, abspath, ': unable to open',
746
869
                failure_exc=FileExists)
747
870
 
 
871
    def _can_roundtrip_unix_modebits(self):
 
872
        if sys.platform == 'win32':
 
873
            # anyone else?
 
874
            return False
 
875
        else:
 
876
            return True
748
877
 
749
878
# ------------- server test implementation --------------
750
879
import threading
893
1022
class SFTPServer(Server):
894
1023
    """Common code for SFTP server facilities."""
895
1024
 
896
 
    def __init__(self):
 
1025
    def __init__(self, server_interface=StubServer):
897
1026
        self._original_vendor = None
898
1027
        self._homedir = None
899
1028
        self._server_homedir = None
900
1029
        self._listener = None
901
1030
        self._root = None
902
1031
        self._vendor = ssh.ParamikoVendor()
 
1032
        self._server_interface = server_interface
903
1033
        # sftp server logs
904
1034
        self.logs = []
905
1035
        self.add_latency = 0
930
1060
        f.close()
931
1061
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
932
1062
        ssh_server.add_server_key(host_key)
933
 
        server = StubServer(self)
 
1063
        server = self._server_interface(self)
934
1064
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
935
1065
                                         StubSFTPServer, root=self._root,
936
1066
                                         home=self._server_homedir)
990
1120
        # Re-import these as locals, so that they're still accessible during
991
1121
        # interpreter shutdown (when all module globals get set to None, leading
992
1122
        # to confusing errors like "'NoneType' object has no attribute 'error'".
993
 
        import socket, errno
994
1123
        class FakeChannel(object):
995
1124
            def get_transport(self):
996
1125
                return self