~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-06-28 07:08:27 UTC
  • mfrom: (2553.1.3 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20070628070827-h5s313dg5tnag9vj
(robertc) Show the names of commit hooks during commit.

Show diffs side-by-side

added added

removed removed

Lines of Context:
47
47
from bzrlib.trace import mutter, warning
48
48
from bzrlib.transport import (
49
49
    Server,
50
 
    ConnectedTransport,
 
50
    split_url,
 
51
    Transport,
51
52
    )
52
53
from bzrlib.transport.local import LocalURLServer
53
54
import bzrlib.ui
59
60
    """FTP failed for path: %(path)s%(extra)s"""
60
61
 
61
62
 
 
63
_FTP_cache = {}
 
64
def _find_FTP(hostname, port, username, password, is_active):
 
65
    """Find an ftplib.FTP instance attached to this triplet."""
 
66
    key = (hostname, port, username, password, is_active)
 
67
    alt_key = (hostname, port, username, '********', is_active)
 
68
    if key not in _FTP_cache:
 
69
        mutter("Constructing FTP instance against %r" % (alt_key,))
 
70
        conn = ftplib.FTP()
 
71
 
 
72
        conn.connect(host=hostname, port=port)
 
73
        if username and username != 'anonymous' and not password:
 
74
            password = bzrlib.ui.ui_factory.get_password(
 
75
                prompt='FTP %(user)s@%(host)s password',
 
76
                user=username, host=hostname)
 
77
        conn.login(user=username, passwd=password)
 
78
        conn.set_pasv(not is_active)
 
79
 
 
80
        _FTP_cache[key] = conn
 
81
 
 
82
    return _FTP_cache[key]    
 
83
 
 
84
 
62
85
class FtpStatResult(object):
63
86
    def __init__(self, f, relpath):
64
87
        try:
76
99
_number_of_retries = 2
77
100
_sleep_between_retries = 5
78
101
 
79
 
# FIXME: there are inconsistencies in the way temporary errors are
80
 
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
81
 
# be taken to analyze the implications for write operations (read operations
82
 
# are safe to retry). Overall even some read operations are never
83
 
# retried. --vila 20070720 (Bug #127164)
84
 
class FtpTransport(ConnectedTransport):
 
102
class FtpTransport(Transport):
85
103
    """This is the transport agent for ftp:// access."""
86
104
 
87
 
    def __init__(self, base, _from_transport=None):
 
105
    def __init__(self, base, _provided_instance=None):
88
106
        """Set the base path where files will be stored."""
89
107
        assert base.startswith('ftp://') or base.startswith('aftp://')
90
 
        super(FtpTransport, self).__init__(base,
91
 
                                           _from_transport=_from_transport)
92
 
        self._unqualified_scheme = 'ftp'
93
 
        if self._scheme == 'aftp':
94
 
            self.is_active = True
95
 
        else:
96
 
            self.is_active = False
 
108
 
 
109
        self.is_active = base.startswith('aftp://')
 
110
        if self.is_active:
 
111
            # urlparse won't handle aftp://
 
112
            base = base[1:]
 
113
        if not base.endswith('/'):
 
114
            base += '/'
 
115
        (self._proto, self._username,
 
116
            self._password, self._host,
 
117
            self._port, self._path) = split_url(base)
 
118
        base = self._unparse_url()
 
119
 
 
120
        super(FtpTransport, self).__init__(base)
 
121
        self._FTP_instance = _provided_instance
 
122
 
 
123
    def _unparse_url(self, path=None):
 
124
        if path is None:
 
125
            path = self._path
 
126
        path = urllib.quote(path)
 
127
        netloc = urllib.quote(self._host)
 
128
        if self._username is not None:
 
129
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
130
        if self._port is not None:
 
131
            netloc = '%s:%d' % (netloc, self._port)
 
132
        proto = 'ftp'
 
133
        if self.is_active:
 
134
            proto = 'aftp'
 
135
        return urlparse.urlunparse((proto, netloc, path, '', '', ''))
97
136
 
98
137
    def _get_FTP(self):
99
138
        """Return the ftplib.FTP instance for this object."""
100
 
        # Ensures that a connection is established
101
 
        connection = self._get_connection()
102
 
        if connection is None:
103
 
            # First connection ever
104
 
            connection, credentials = self._create_connection()
105
 
            self._set_connection(connection, credentials)
106
 
        return connection
107
 
 
108
 
    def _create_connection(self, credentials=None):
109
 
        """Create a new connection with the provided credentials.
110
 
 
111
 
        :param credentials: The credentials needed to establish the connection.
112
 
 
113
 
        :return: The created connection and its associated credentials.
114
 
 
115
 
        The credentials are only the password as it may have been entered
116
 
        interactively by the user and may be different from the one provided
117
 
        in base url at transport creation time.
118
 
        """
119
 
        if credentials is None:
120
 
            password = self._password
121
 
        else:
122
 
            password = credentials
123
 
 
124
 
        mutter("Constructing FTP instance against %r" %
125
 
               ((self._host, self._port, self._user, '********',
126
 
                self.is_active),))
 
139
        if self._FTP_instance is not None:
 
140
            return self._FTP_instance
 
141
        
127
142
        try:
128
 
            connection = ftplib.FTP()
129
 
            connection.connect(host=self._host, port=self._port)
130
 
            if self._user and self._user != 'anonymous' and \
131
 
                    password is not None: # '' is a valid password
132
 
                get_password = bzrlib.ui.ui_factory.get_password
133
 
                password = get_password(prompt='FTP %(user)s@%(host)s password',
134
 
                                        user=self._user, host=self._host)
135
 
            connection.login(user=self._user, passwd=password)
136
 
            connection.set_pasv(not self.is_active)
 
143
            self._FTP_instance = _find_FTP(self._host, self._port,
 
144
                                           self._username, self._password,
 
145
                                           self.is_active)
 
146
            return self._FTP_instance
137
147
        except ftplib.error_perm, e:
138
 
            raise errors.TransportError(msg="Error setting up connection:"
139
 
                                        " %s" % str(e), orig_error=e)
140
 
        return connection, password
141
 
 
142
 
    def _reconnect(self):
143
 
        """Create a new connection with the previously used credentials"""
144
 
        credentials = self.get_credentials()
145
 
        connection, credentials = self._create_connection(credentials)
146
 
        self._set_connection(connection, credentials)
147
 
 
148
 
    def _translate_perm_error(self, err, path, extra=None,
149
 
                              unknown_exc=FtpPathError):
 
148
            raise errors.TransportError(msg="Error setting up connection: %s"
 
149
                                    % str(e), orig_error=e)
 
150
 
 
151
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
150
152
        """Try to translate an ftplib.error_perm exception.
151
153
 
152
154
        :param err: The error to translate into a bzr error
164
166
            or 'could not open' in s
165
167
            or 'no such dir' in s
166
168
            or 'could not create file' in s # vsftpd
167
 
            or 'file doesn\'t exist' in s
168
169
            ):
169
170
            raise errors.NoSuchFile(path, extra=extra)
170
171
        if ('file exists' in s):
188
189
        """
189
190
        return True
190
191
 
191
 
    def _remote_path(self, relpath):
 
192
    def clone(self, offset=None):
 
193
        """Return a new FtpTransport with root at self.base + offset.
 
194
        """
 
195
        mutter("FTP clone")
 
196
        if offset is None:
 
197
            return FtpTransport(self.base, self._FTP_instance)
 
198
        else:
 
199
            return FtpTransport(self.abspath(offset), self._FTP_instance)
 
200
 
 
201
    def _abspath(self, relpath):
 
202
        assert isinstance(relpath, basestring)
 
203
        relpath = urlutils.unescape(relpath)
 
204
        if relpath.startswith('/'):
 
205
            basepath = []
 
206
        else:
 
207
            basepath = self._path.split('/')
 
208
        if len(basepath) > 0 and basepath[-1] == '':
 
209
            basepath = basepath[:-1]
 
210
        for p in relpath.split('/'):
 
211
            if p == '..':
 
212
                if len(basepath) == 0:
 
213
                    # In most filesystems, a request for the parent
 
214
                    # of root, just returns root.
 
215
                    continue
 
216
                basepath.pop()
 
217
            elif p == '.' or p == '':
 
218
                continue # No-op
 
219
            else:
 
220
                basepath.append(p)
 
221
        # Possibly, we could use urlparse.urljoin() here, but
 
222
        # I'm concerned about when it chooses to strip the last
 
223
        # portion of the path, and when it doesn't.
 
224
 
192
225
        # XXX: It seems that ftplib does not handle Unicode paths
193
 
        # at the same time, medusa won't handle utf8 paths So if
194
 
        # we .encode(utf8) here (see ConnectedTransport
195
 
        # implementation), then we get a Server failure.  while
196
 
        # if we use str(), we get a UnicodeError, and the test
197
 
        # suite just skips testing UnicodePaths.
198
 
        relative = str(urlutils.unescape(relpath))
199
 
        remote_path = self._combine_paths(self._path, relative)
200
 
        return remote_path
 
226
        # at the same time, medusa won't handle utf8 paths
 
227
        # So if we .encode(utf8) here, then we get a Server failure.
 
228
        # while if we use str(), we get a UnicodeError, and the test suite
 
229
        # just skips testing UnicodePaths.
 
230
        return str('/'.join(basepath) or '/')
 
231
    
 
232
    def abspath(self, relpath):
 
233
        """Return the full url to the given relative path.
 
234
        This can be supplied with a string or a list
 
235
        """
 
236
        path = self._abspath(relpath)
 
237
        return self._unparse_url(path)
201
238
 
202
239
    def has(self, relpath):
203
240
        """Does the target location exist?"""
206
243
        # XXX: I assume we're never asked has(dirname) and thus I use
207
244
        # the FTP size command and assume that if it doesn't raise,
208
245
        # all is good.
209
 
        abspath = self._remote_path(relpath)
 
246
        abspath = self._abspath(relpath)
210
247
        try:
211
248
            f = self._get_FTP()
212
249
            mutter('FTP has check: %s => %s', relpath, abspath)
232
269
        """
233
270
        # TODO: decode should be deprecated
234
271
        try:
235
 
            mutter("FTP get: %s", self._remote_path(relpath))
 
272
            mutter("FTP get: %s", self._abspath(relpath))
236
273
            f = self._get_FTP()
237
274
            ret = StringIO()
238
 
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
 
275
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
239
276
            ret.seek(0)
240
277
            return ret
241
278
        except ftplib.error_perm, e:
247
284
                                     orig_error=e)
248
285
            else:
249
286
                warning("FTP temporary error: %s. Retrying.", str(e))
250
 
                self._reconnect()
 
287
                self._FTP_instance = None
251
288
                return self.get(relpath, decode, retries+1)
252
289
        except EOFError, e:
253
290
            if retries > _number_of_retries:
257
294
            else:
258
295
                warning("FTP control connection closed. Trying to reopen.")
259
296
                time.sleep(_sleep_between_retries)
260
 
                self._reconnect()
 
297
                self._FTP_instance = None
261
298
                return self.get(relpath, decode, retries+1)
262
299
 
263
300
    def put_file(self, relpath, fp, mode=None, retries=0):
271
308
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
272
309
        ftplib does not
273
310
        """
274
 
        abspath = self._remote_path(relpath)
 
311
        abspath = self._abspath(relpath)
275
312
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
276
313
                        os.getpid(), random.randint(0,0x7FFFFFFF))
277
314
        if getattr(fp, 'read', None) is None:
292
329
                    raise e
293
330
                raise
294
331
        except ftplib.error_perm, e:
295
 
            self._translate_perm_error(e, abspath, extra='could not store',
296
 
                                       unknown_exc=errors.NoSuchFile)
 
332
            self._translate_perm_error(e, abspath, extra='could not store')
297
333
        except ftplib.error_temp, e:
298
334
            if retries > _number_of_retries:
299
335
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
300
336
                                     % self.abspath(relpath), orig_error=e)
301
337
            else:
302
338
                warning("FTP temporary error: %s. Retrying.", str(e))
303
 
                self._reconnect()
 
339
                self._FTP_instance = None
304
340
                self.put_file(relpath, fp, mode, retries+1)
305
341
        except EOFError:
306
342
            if retries > _number_of_retries:
309
345
            else:
310
346
                warning("FTP control connection closed. Trying to reopen.")
311
347
                time.sleep(_sleep_between_retries)
312
 
                self._reconnect()
 
348
                self._FTP_instance = None
313
349
                self.put_file(relpath, fp, mode, retries+1)
314
350
 
315
351
    def mkdir(self, relpath, mode=None):
316
352
        """Create a directory at the given path."""
317
 
        abspath = self._remote_path(relpath)
 
353
        abspath = self._abspath(relpath)
318
354
        try:
319
355
            mutter("FTP mkd: %s", abspath)
320
356
            f = self._get_FTP()
325
361
 
326
362
    def rmdir(self, rel_path):
327
363
        """Delete the directory at rel_path"""
328
 
        abspath = self._remote_path(rel_path)
 
364
        abspath = self._abspath(rel_path)
329
365
        try:
330
366
            mutter("FTP rmd: %s", abspath)
331
367
            f = self._get_FTP()
337
373
        """Append the text in the file-like object into the final
338
374
        location.
339
375
        """
340
 
        abspath = self._remote_path(relpath)
 
376
        abspath = self._abspath(relpath)
341
377
        if self.has(relpath):
342
378
            ftp = self._get_FTP()
343
379
            result = ftp.size(abspath)
356
392
        number of retries is exceeded.
357
393
        """
358
394
        try:
359
 
            abspath = self._remote_path(relpath)
 
395
            abspath = self._abspath(relpath)
360
396
            mutter("FTP appe (try %d) to %s", retries, abspath)
361
397
            ftp = self._get_FTP()
362
398
            ftp.voidcmd("TYPE I")
376
412
                        "Aborting." % abspath, orig_error=e)
377
413
            else:
378
414
                warning("FTP temporary error: %s. Retrying.", str(e))
379
 
                self._reconnect()
 
415
                self._FTP_instance = None
380
416
                self._try_append(relpath, text, mode, retries+1)
381
417
 
382
418
    def _setmode(self, relpath, mode):
387
423
        """
388
424
        try:
389
425
            mutter("FTP site chmod: setting permissions to %s on %s",
390
 
                str(mode), self._remote_path(relpath))
 
426
                str(mode), self._abspath(relpath))
391
427
            ftp = self._get_FTP()
392
 
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
 
428
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
393
429
            ftp.sendcmd(cmd)
394
430
        except ftplib.error_perm, e:
395
431
            # Command probably not available on this server
396
432
            warning("FTP Could not set permissions to %s on %s. %s",
397
 
                    str(mode), self._remote_path(relpath), str(e))
 
433
                    str(mode), self._abspath(relpath), str(e))
398
434
 
399
435
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
400
436
    #       to copy something to another machine. And you may be able
402
438
    #       So implement a fancier 'copy()'
403
439
 
404
440
    def rename(self, rel_from, rel_to):
405
 
        abs_from = self._remote_path(rel_from)
406
 
        abs_to = self._remote_path(rel_to)
 
441
        abs_from = self._abspath(rel_from)
 
442
        abs_to = self._abspath(rel_to)
407
443
        mutter("FTP rename: %s => %s", abs_from, abs_to)
408
444
        f = self._get_FTP()
409
445
        return self._rename(abs_from, abs_to, f)
417
453
 
418
454
    def move(self, rel_from, rel_to):
419
455
        """Move the item at rel_from to the location at rel_to"""
420
 
        abs_from = self._remote_path(rel_from)
421
 
        abs_to = self._remote_path(rel_to)
 
456
        abs_from = self._abspath(rel_from)
 
457
        abs_to = self._abspath(rel_to)
422
458
        try:
423
459
            mutter("FTP mv: %s => %s", abs_from, abs_to)
424
460
            f = self._get_FTP()
439
475
 
440
476
    def delete(self, relpath):
441
477
        """Delete the item at relpath"""
442
 
        abspath = self._remote_path(relpath)
 
478
        abspath = self._abspath(relpath)
443
479
        f = self._get_FTP()
444
480
        self._delete(abspath, f)
445
481
 
451
487
            self._translate_perm_error(e, abspath, 'error deleting',
452
488
                unknown_exc=errors.NoSuchFile)
453
489
 
454
 
    def external_url(self):
455
 
        """See bzrlib.transport.Transport.external_url."""
456
 
        # FTP URL's are externally usable.
457
 
        return self.base
458
 
 
459
490
    def listable(self):
460
491
        """See Transport.listable."""
461
492
        return True
462
493
 
463
494
    def list_dir(self, relpath):
464
495
        """See Transport.list_dir."""
465
 
        basepath = self._remote_path(relpath)
 
496
        basepath = self._abspath(relpath)
466
497
        mutter("FTP nlst: %s", basepath)
467
498
        f = self._get_FTP()
468
499
        try:
495
526
 
496
527
    def stat(self, relpath):
497
528
        """Return the stat information for a file."""
498
 
        abspath = self._remote_path(relpath)
 
529
        abspath = self._abspath(relpath)
499
530
        try:
500
531
            mutter("FTP stat: %s", abspath)
501
532
            f = self._get_FTP()
526
557
 
527
558
 
528
559
class FtpServer(Server):
529
 
    """Common code for FTP server facilities."""
 
560
    """Common code for SFTP server facilities."""
530
561
 
531
562
    def __init__(self):
532
563
        self._root = None
565
596
            )
566
597
        self._port = self._ftp_server.getsockname()[1]
567
598
        # Don't let it loop forever, or handle an infinite number of requests.
568
 
        # In this case it will run for 1000s, or 10000 requests
 
599
        # In this case it will run for 100s, or 1000 requests
569
600
        self._async_thread = threading.Thread(
570
601
                target=FtpServer._asyncore_loop_ignore_EBADF,
571
 
                kwargs={'timeout':0.1, 'count':10000})
 
602
                kwargs={'timeout':0.1, 'count':1000})
572
603
        self._async_thread.setDaemon(True)
573
604
        self._async_thread.start()
574
605
 
588
619
        """
589
620
        try:
590
621
            asyncore.loop(*args, **kwargs)
591
 
            # FIXME: If we reach that point, we should raise an exception
592
 
            # explaining that the 'count' parameter in setUp is too low or
593
 
            # testers may wonder why their test just sits there waiting for a
594
 
            # server that is already dead. Note that if the tester waits too
595
 
            # long under pdb the server will also die.
596
622
        except select.error, e:
597
623
            if e.args[0] != errno.EBADF:
598
624
                raise
644
670
        def log(self, message):
645
671
            """Redirect logging requests."""
646
672
            mutter('_ftp_channel: %s', message)
647
 
 
 
673
            
648
674
        def log_info(self, message, type='info'):
649
675
            """Redirect logging requests."""
650
676
            mutter('_ftp_channel %s: %s', type, message)
651
 
 
 
677
            
652
678
        def cmd_rnfr(self, line):
653
679
            """Prepare for renaming a file."""
654
680
            self._renaming = line[1]