/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

  • Committer: Robert Collins
  • Date: 2006-09-16 06:44:47 UTC
  • mfrom: (2014 +trunk)
  • mto: (2017.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 2018.
  • Revision ID: robertc@robertcollins.net-20060916064447-99a2987e5485b5ea
Merge from bzr.dev, fixing found bugs handling 'has('/')' in MemoryTransport and SFTP transports.

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.errors import (FileExists,
41
40
                           NoSuchFile, PathNotChild,
42
41
                           TransportError,
43
 
                           LockError, 
 
42
                           LockError,
44
43
                           PathError,
45
44
                           ParamikoNotPresent,
46
 
                           UnknownSSH,
47
45
                           )
48
46
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
49
47
from bzrlib.trace import mutter, warning
133
131
            pass
134
132
 
135
133
 
136
 
class SFTPTransport(Transport):
 
134
class SFTPUrlHandling(Transport):
 
135
    """Mix-in that does common handling of SSH/SFTP URLs."""
 
136
 
 
137
    def __init__(self, base):
 
138
        self._parse_url(base)
 
139
        base = self._unparse_url(self._path)
 
140
        if base[-1] != '/':
 
141
            base += '/'
 
142
        super(SFTPUrlHandling, self).__init__(base)
 
143
 
 
144
    def _parse_url(self, url):
 
145
        (self._scheme,
 
146
         self._username, self._password,
 
147
         self._host, self._port, self._path) = self._split_url(url)
 
148
 
 
149
    def _unparse_url(self, path):
 
150
        """Return a URL for a path relative to this transport.
 
151
        """
 
152
        path = urllib.quote(path)
 
153
        # handle homedir paths
 
154
        if not path.startswith('/'):
 
155
            path = "/~/" + path
 
156
        netloc = urllib.quote(self._host)
 
157
        if self._username is not None:
 
158
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
159
        if self._port is not None:
 
160
            netloc = '%s:%d' % (netloc, self._port)
 
161
        return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
 
162
 
 
163
    def _split_url(self, url):
 
164
        (scheme, username, password, host, port, path) = split_url(url)
 
165
        ## assert scheme == 'sftp'
 
166
 
 
167
        # the initial slash should be removed from the path, and treated
 
168
        # as a homedir relative path (the path begins with a double slash
 
169
        # if it is absolute).
 
170
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
171
        # RBC 20060118 we are not using this as its too user hostile. instead
 
172
        # we are following lftp and using /~/foo to mean '~/foo'.
 
173
        # handle homedir paths
 
174
        if path.startswith('/~/'):
 
175
            path = path[3:]
 
176
        elif path == '/~':
 
177
            path = ''
 
178
        return (scheme, username, password, host, port, path)
 
179
 
 
180
    def _remote_path(self, relpath):
 
181
        """Return the path to be passed along the sftp protocol for relpath.
 
182
        
 
183
        :param relpath: is a urlencoded string.
 
184
        """
 
185
        return self._combine_paths(self._path, relpath)
 
186
 
 
187
 
 
188
class SFTPTransport(SFTPUrlHandling):
137
189
    """Transport implementation for SFTP access."""
138
190
 
139
191
    _do_prefetch = _default_do_prefetch
155
207
    _max_request_size = 32768
156
208
 
157
209
    def __init__(self, base, clone_from=None):
158
 
        assert base.startswith('sftp://')
159
 
        self._parse_url(base)
160
 
        base = self._unparse_url()
161
 
        if base[-1] != '/':
162
 
            base += '/'
163
210
        super(SFTPTransport, self).__init__(base)
164
211
        if clone_from is None:
165
212
            self._sftp_connect()
198
245
        """Return the path to be passed along the sftp protocol for relpath.
199
246
        
200
247
        relpath is a urlencoded string.
 
248
 
 
249
        :return: a path prefixed with / for regular abspath-based urls, or a
 
250
            path that does not begin with / for urls which begin with /~/.
201
251
        """
202
 
        # FIXME: share the common code across transports
 
252
        # how does this work? 
 
253
        # it processes relpath with respect to 
 
254
        # our state:
 
255
        # firstly we create a path to evaluate: 
 
256
        # if relpath is an abspath or homedir path, its the entire thing
 
257
        # otherwise we join our base with relpath
 
258
        # then we eliminate all empty segments (double //'s) outside the first
 
259
        # two elements of the list. This avoids problems with trailing 
 
260
        # slashes, or other abnormalities.
 
261
        # finally we evaluate the entire path in a single pass
 
262
        # '.'s are stripped,
 
263
        # '..' result in popping the left most already 
 
264
        # processed path (which can never be empty because of the check for
 
265
        # abspath and homedir meaning that its not, or that we've used our
 
266
        # path. If the pop would pop the root, we ignore it.
 
267
 
 
268
        # Specific case examinations:
 
269
        # remove the special casefor ~: if the current root is ~/ popping of it
 
270
        # = / thus our seed for a ~ based path is ['', '~']
 
271
        # and if we end up with [''] then we had basically ('', '..') (which is
 
272
        # '/..' so we append '' if the length is one, and assert that the first
 
273
        # element is still ''. Lastly, if we end with ['', '~'] as a prefix for
 
274
        # the output, we've got a homedir path, so we strip that prefix before
 
275
        # '/' joining the resulting list.
 
276
        #
 
277
        # case one: '/' -> ['', ''] cannot shrink
 
278
        # case two: '/' + '../foo' -> ['', 'foo'] (take '', '', '..', 'foo')
 
279
        #           and pop the second '' for the '..', append 'foo'
 
280
        # case three: '/~/' -> ['', '~', ''] 
 
281
        # case four: '/~/' + '../foo' -> ['', '~', '', '..', 'foo'],
 
282
        #           and we want to get '/foo' - the empty path in the middle
 
283
        #           needs to be stripped, then normal path manipulation will 
 
284
        #           work.
 
285
        # case five: '/..' ['', '..'], we want ['', '']
 
286
        #            stripping '' outside the first two is ok
 
287
        #            ignore .. if its too high up
 
288
        #
 
289
        # lastly this code is possibly reusable by FTP, but not reusable by
 
290
        # local paths: ~ is resolvable correctly, nor by HTTP or the smart
 
291
        # server: ~ is resolved remotely.
 
292
        # 
 
293
        # however, a version of this that acts on self.base is possible to be
 
294
        # written which manipulates the URL in canonical form, and would be
 
295
        # reusable for all transports, if a flag for allowing ~/ at all was
 
296
        # provided.
203
297
        assert isinstance(relpath, basestring)
204
 
        relpath = urlutils.unescape(relpath).split('/')
205
 
        basepath = self._path.split('/')
206
 
        if len(basepath) > 0 and basepath[-1] == '':
207
 
            basepath = basepath[:-1]
208
 
 
209
 
        for p in relpath:
210
 
            if p == '..':
211
 
                if len(basepath) == 0:
212
 
                    # In most filesystems, a request for the parent
213
 
                    # of root, just returns root.
214
 
                    continue
215
 
                basepath.pop()
216
 
            elif p == '.':
217
 
                continue # No-op
218
 
            else:
219
 
                basepath.append(p)
220
 
 
221
 
        path = '/'.join(basepath)
222
 
        # mutter('relpath => remotepath %s => %s', relpath, path)
 
298
        relpath = urlutils.unescape(relpath)
 
299
 
 
300
        # case 1)
 
301
        if relpath.startswith('/'):
 
302
            # abspath - normal split is fine.
 
303
            current_path = relpath.split('/')
 
304
        elif relpath.startswith('~/'):
 
305
            # root is homedir based: normal split and prefix '' to remote the
 
306
            # special case
 
307
            current_path = [''].extend(relpath.split('/'))
 
308
        else:
 
309
            # root is from the current directory:
 
310
            if self._path.startswith('/'):
 
311
                # abspath, take the regular split
 
312
                current_path = []
 
313
            else:
 
314
                # homedir based, add the '', '~' not present in self._path
 
315
                current_path = ['', '~']
 
316
            # add our current dir
 
317
            current_path.extend(self._path.split('/'))
 
318
            # add the users relpath
 
319
            current_path.extend(relpath.split('/'))
 
320
        # strip '' segments that are not in the first one - the leading /.
 
321
        to_process = current_path[:1]
 
322
        for segment in current_path[1:]:
 
323
            if segment != '':
 
324
                to_process.append(segment)
 
325
 
 
326
        # process '.' and '..' segments into output_path.
 
327
        output_path = []
 
328
        for segment in to_process:
 
329
            if segment == '..':
 
330
                # directory pop. Remove a directory 
 
331
                # as long as we are not at the root
 
332
                if len(output_path) > 1:
 
333
                    output_path.pop()
 
334
                # else: pass
 
335
                # cannot pop beyond the root, so do nothing
 
336
            elif segment == '.':
 
337
                continue # strip the '.' from the output.
 
338
            else:
 
339
                # this will append '' to output_path for the root elements,
 
340
                # which is appropriate: its why we strip '' in the first pass.
 
341
                output_path.append(segment)
 
342
 
 
343
        # check output special cases:
 
344
        if output_path == ['']:
 
345
            # [''] -> ['', '']
 
346
            output_path = ['', '']
 
347
        elif output_path[:2] == ['', '~']:
 
348
            # ['', '~', ...] -> ...
 
349
            output_path = output_path[2:]
 
350
        path = '/'.join(output_path)
223
351
        return path
224
352
 
225
353
    def relpath(self, abspath):
226
 
        username, password, host, port, path = self._split_url(abspath)
 
354
        scheme, username, password, host, port, path = self._split_url(abspath)
227
355
        error = []
228
356
        if (username != self._username):
229
357
            error.append('username mismatch')
687
815
        # that we have taken the lock.
688
816
        return SFTPLock(relpath, self)
689
817
 
690
 
    def _unparse_url(self, path=None):
691
 
        if path is None:
692
 
            path = self._path
693
 
        path = urllib.quote(path)
694
 
        # handle homedir paths
695
 
        if not path.startswith('/'):
696
 
            path = "/~/" + path
697
 
        netloc = urllib.quote(self._host)
698
 
        if self._username is not None:
699
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
700
 
        if self._port is not None:
701
 
            netloc = '%s:%d' % (netloc, self._port)
702
 
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
703
 
 
704
 
    def _split_url(self, url):
705
 
        (scheme, username, password, host, port, path) = split_url(url)
706
 
        assert scheme == 'sftp'
707
 
 
708
 
        # the initial slash should be removed from the path, and treated
709
 
        # as a homedir relative path (the path begins with a double slash
710
 
        # if it is absolute).
711
 
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
712
 
        # RBC 20060118 we are not using this as its too user hostile. instead
713
 
        # we are following lftp and using /~/foo to mean '~/foo'.
714
 
        # handle homedir paths
715
 
        if path.startswith('/~/'):
716
 
            path = path[3:]
717
 
        elif path == '/~':
718
 
            path = ''
719
 
        return (username, password, host, port, path)
720
 
 
721
 
    def _parse_url(self, url):
722
 
        (self._username, self._password,
723
 
         self._host, self._port, self._path) = self._split_url(url)
724
 
 
725
818
    def _sftp_connect(self):
726
819
        """Connect to the remote sftp server.
727
820
        After this, self._sftp should have a valid connection (or
1018
1111
        # Re-import these as locals, so that they're still accessible during
1019
1112
        # interpreter shutdown (when all module globals get set to None, leading
1020
1113
        # to confusing errors like "'NoneType' object has no attribute 'error'".
1021
 
        import socket, errno
1022
1114
        class FakeChannel(object):
1023
1115
            def get_transport(self):
1024
1116
                return self