~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Aaron Bentley
  • Date: 2006-09-19 16:26:17 UTC
  • mfrom: (2022 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2162.
  • Revision ID: abentley@panoramicfeedback.com-20060919162617-e6099afb05b6a9ef
Merge from bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
import select
31
31
import socket
32
32
import stat
33
 
import subprocess
34
33
import sys
35
34
import time
36
35
import urllib
37
36
import urlparse
38
37
import weakref
39
38
 
40
 
from bzrlib.errors import (FileExists, 
 
39
from bzrlib import (
 
40
    errors,
 
41
    urlutils,
 
42
    )
 
43
from bzrlib.errors import (FileExists,
41
44
                           NoSuchFile, PathNotChild,
42
45
                           TransportError,
43
 
                           LockError, 
 
46
                           LockError,
44
47
                           PathError,
45
48
                           ParamikoNotPresent,
46
 
                           UnknownSSH,
47
49
                           )
48
50
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
49
51
from bzrlib.trace import mutter, warning
54
56
    ssh,
55
57
    Transport,
56
58
    )
57
 
import bzrlib.urlutils as urlutils
58
59
 
59
60
try:
60
61
    import paramiko
179
180
            path = ''
180
181
        return (scheme, username, password, host, port, path)
181
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
    
182
191
    def _remote_path(self, relpath):
183
192
        """Return the path to be passed along the sftp protocol for relpath.
184
193
        
234
243
        else:
235
244
            return SFTPTransport(self.abspath(offset), self)
236
245
 
237
 
    def abspath(self, relpath):
238
 
        """
239
 
        Return the full url to the given relative path.
240
 
        
241
 
        @param relpath: the relative path or path components
242
 
        @type relpath: str or list
243
 
        """
244
 
        return self._unparse_url(self._remote_path(relpath))
245
 
    
246
246
    def _remote_path(self, relpath):
247
247
        """Return the path to be passed along the sftp protocol for relpath.
248
248
        
251
251
        :return: a path prefixed with / for regular abspath-based urls, or a
252
252
            path that does not begin with / for urls which begin with /~/.
253
253
        """
254
 
        # 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.
255
299
        assert isinstance(relpath, basestring)
256
 
        basepath = self._path.split('/')
 
300
        relpath = urlutils.unescape(relpath)
 
301
 
 
302
        # case 1)
257
303
        if relpath.startswith('/'):
258
 
            basepath = ['', '']
259
 
        relpath = urlutils.unescape(relpath).split('/')
260
 
        if len(basepath) > 0 and basepath[-1] == '':
261
 
            basepath = basepath[:-1]
262
 
 
263
 
        for p in relpath:
264
 
            if p == '..':
265
 
                if len(basepath) == 0:
266
 
                    # In most filesystems, a request for the parent
267
 
                    # of root, just returns root.
268
 
                    continue
269
 
                basepath.pop()
270
 
            elif p == '.':
271
 
                continue # No-op
272
 
            elif p != '':
273
 
                basepath.append(p)
274
 
 
275
 
        path = '/'.join(basepath)
276
 
        # mutter('relpath => remotepath %s => %s', relpath, path)
 
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)
277
353
        return path
278
354
 
279
355
    def relpath(self, abspath):
331
407
            fp = self._sftp.file(path, mode='rb')
332
408
            readv = getattr(fp, 'readv', None)
333
409
            if readv:
334
 
                return self._sftp_readv(fp, offsets)
 
410
                return self._sftp_readv(fp, offsets, relpath)
335
411
            mutter('seek and read %s offsets', len(offsets))
336
 
            return self._seek_and_read(fp, offsets)
 
412
            return self._seek_and_read(fp, offsets, relpath)
337
413
        except (IOError, paramiko.SSHException), e:
338
414
            self._translate_io_exception(e, path, ': error retrieving')
339
415
 
340
 
    def _sftp_readv(self, fp, offsets):
 
416
    def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
341
417
        """Use the readv() member of fp to do async readv.
342
418
 
343
419
        And then read them using paramiko.readv(). paramiko.readv()
430
506
                yield cur_offset_and_size[0], this_data
431
507
                cur_offset_and_size = offset_stack.next()
432
508
 
 
509
            # We read a coalesced entry, so mark it as done
 
510
            cur_coalesced = None
433
511
            # Now that we've read all of the data for this coalesced section
434
512
            # on to the next
435
513
            cur_coalesced = cur_coalesced_stack.next()
436
514
 
 
515
        if cur_coalesced is not None:
 
516
            raise errors.ShortReadvError(relpath, cur_coalesced.start,
 
517
                cur_coalesced.length, len(data))
 
518
 
437
519
    def put_file(self, relpath, f, mode=None):
438
520
        """
439
521
        Copy the file-like object into the location.
940
1022
class SFTPServer(Server):
941
1023
    """Common code for SFTP server facilities."""
942
1024
 
943
 
    def __init__(self):
 
1025
    def __init__(self, server_interface=StubServer):
944
1026
        self._original_vendor = None
945
1027
        self._homedir = None
946
1028
        self._server_homedir = None
947
1029
        self._listener = None
948
1030
        self._root = None
949
1031
        self._vendor = ssh.ParamikoVendor()
 
1032
        self._server_interface = server_interface
950
1033
        # sftp server logs
951
1034
        self.logs = []
952
1035
        self.add_latency = 0
977
1060
        f.close()
978
1061
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
979
1062
        ssh_server.add_server_key(host_key)
980
 
        server = StubServer(self)
 
1063
        server = self._server_interface(self)
981
1064
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
982
1065
                                         StubSFTPServer, root=self._root,
983
1066
                                         home=self._server_homedir)
1037
1120
        # Re-import these as locals, so that they're still accessible during
1038
1121
        # interpreter shutdown (when all module globals get set to None, leading
1039
1122
        # to confusing errors like "'NoneType' object has no attribute 'error'".
1040
 
        import socket, errno
1041
1123
        class FakeChannel(object):
1042
1124
            def get_transport(self):
1043
1125
                return self