~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-10-24 12:49:17 UTC
  • mfrom: (2935.1.1 ianc-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20071024124917-xb75eckyxx6vkrlg
Makefile fixes - hooks.html generation & allow python to be overridden (Ian Clatworthy)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
29
29
import errno
30
30
import ftplib
31
31
import os
 
32
import os.path
32
33
import urllib
33
34
import urlparse
 
35
import select
34
36
import stat
35
37
import threading
36
38
import time
37
39
import random
38
40
from warnings import warn
39
41
 
 
42
from bzrlib import (
 
43
    errors,
 
44
    osutils,
 
45
    urlutils,
 
46
    )
 
47
from bzrlib.trace import mutter, warning
40
48
from bzrlib.transport import (
41
 
    Transport,
 
49
    AppendBasedFileStream,
 
50
    _file_streams,
42
51
    Server,
43
 
    split_url,
 
52
    ConnectedTransport,
44
53
    )
45
 
import bzrlib.errors as errors
46
 
from bzrlib.trace import mutter, warning
 
54
from bzrlib.transport.local import LocalURLServer
47
55
import bzrlib.ui
48
56
 
49
57
_have_medusa = False
53
61
    """FTP failed for path: %(path)s%(extra)s"""
54
62
 
55
63
 
56
 
_FTP_cache = {}
57
 
def _find_FTP(hostname, port, username, password, is_active):
58
 
    """Find an ftplib.FTP instance attached to this triplet."""
59
 
    key = (hostname, port, username, password, is_active)
60
 
    alt_key = (hostname, port, username, '********', is_active)
61
 
    if key not in _FTP_cache:
62
 
        mutter("Constructing FTP instance against %r" % (alt_key,))
63
 
        conn = ftplib.FTP()
64
 
 
65
 
        conn.connect(host=hostname, port=port)
66
 
        if username and username != 'anonymous' and not password:
67
 
            password = bzrlib.ui.ui_factory.get_password(
68
 
                prompt='FTP %(user)s@%(host)s password',
69
 
                user=username, host=hostname)
70
 
        conn.login(user=username, passwd=password)
71
 
        conn.set_pasv(not is_active)
72
 
 
73
 
        _FTP_cache[key] = conn
74
 
 
75
 
    return _FTP_cache[key]    
76
 
 
77
 
 
78
64
class FtpStatResult(object):
79
65
    def __init__(self, f, relpath):
80
66
        try:
92
78
_number_of_retries = 2
93
79
_sleep_between_retries = 5
94
80
 
95
 
class FtpTransport(Transport):
 
81
# FIXME: there are inconsistencies in the way temporary errors are
 
82
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
 
83
# be taken to analyze the implications for write operations (read operations
 
84
# are safe to retry). Overall even some read operations are never
 
85
# retried. --vila 20070720 (Bug #127164)
 
86
class FtpTransport(ConnectedTransport):
96
87
    """This is the transport agent for ftp:// access."""
97
88
 
98
 
    def __init__(self, base, _provided_instance=None):
 
89
    def __init__(self, base, _from_transport=None):
99
90
        """Set the base path where files will be stored."""
100
91
        assert base.startswith('ftp://') or base.startswith('aftp://')
101
 
 
102
 
        self.is_active = base.startswith('aftp://')
103
 
        if self.is_active:
104
 
            # urlparse won't handle aftp://
105
 
            base = base[1:]
106
 
        if not base.endswith('/'):
107
 
            base += '/'
108
 
        (self._proto, self._username,
109
 
            self._password, self._host,
110
 
            self._port, self._path) = split_url(base)
111
 
        base = self._unparse_url()
112
 
 
113
 
        super(FtpTransport, self).__init__(base)
114
 
        self._FTP_instance = _provided_instance
115
 
 
116
 
    def _unparse_url(self, path=None):
117
 
        if path is None:
118
 
            path = self._path
119
 
        path = urllib.quote(path)
120
 
        netloc = urllib.quote(self._host)
121
 
        if self._username is not None:
122
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
123
 
        if self._port is not None:
124
 
            netloc = '%s:%d' % (netloc, self._port)
125
 
        return urlparse.urlunparse(('ftp', netloc, path, '', '', ''))
 
92
        super(FtpTransport, self).__init__(base,
 
93
                                           _from_transport=_from_transport)
 
94
        self._unqualified_scheme = 'ftp'
 
95
        if self._scheme == 'aftp':
 
96
            self.is_active = True
 
97
        else:
 
98
            self.is_active = False
126
99
 
127
100
    def _get_FTP(self):
128
101
        """Return the ftplib.FTP instance for this object."""
129
 
        if self._FTP_instance is not None:
130
 
            return self._FTP_instance
131
 
        
 
102
        # Ensures that a connection is established
 
103
        connection = self._get_connection()
 
104
        if connection is None:
 
105
            # First connection ever
 
106
            connection, credentials = self._create_connection()
 
107
            self._set_connection(connection, credentials)
 
108
        return connection
 
109
 
 
110
    def _create_connection(self, credentials=None):
 
111
        """Create a new connection with the provided credentials.
 
112
 
 
113
        :param credentials: The credentials needed to establish the connection.
 
114
 
 
115
        :return: The created connection and its associated credentials.
 
116
 
 
117
        The credentials are only the password as it may have been entered
 
118
        interactively by the user and may be different from the one provided
 
119
        in base url at transport creation time.
 
120
        """
 
121
        if credentials is None:
 
122
            password = self._password
 
123
        else:
 
124
            password = credentials
 
125
 
 
126
        mutter("Constructing FTP instance against %r" %
 
127
               ((self._host, self._port, self._user, '********',
 
128
                self.is_active),))
132
129
        try:
133
 
            self._FTP_instance = _find_FTP(self._host, self._port,
134
 
                                           self._username, self._password,
135
 
                                           self.is_active)
136
 
            return self._FTP_instance
 
130
            connection = ftplib.FTP()
 
131
            connection.connect(host=self._host, port=self._port)
 
132
            if self._user and self._user != 'anonymous' and \
 
133
                    password is None: # '' is a valid password
 
134
                get_password = bzrlib.ui.ui_factory.get_password
 
135
                password = get_password(prompt='FTP %(user)s@%(host)s password',
 
136
                                        user=self._user, host=self._host)
 
137
            connection.login(user=self._user, passwd=password)
 
138
            connection.set_pasv(not self.is_active)
137
139
        except ftplib.error_perm, e:
138
 
            raise errors.TransportError(msg="Error setting up connection: %s"
139
 
                                    % str(e), orig_error=e)
140
 
 
141
 
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
 
140
            raise errors.TransportError(msg="Error setting up connection:"
 
141
                                        " %s" % str(e), orig_error=e)
 
142
        return connection, password
 
143
 
 
144
    def _reconnect(self):
 
145
        """Create a new connection with the previously used credentials"""
 
146
        credentials = self._get_credentials()
 
147
        connection, credentials = self._create_connection(credentials)
 
148
        self._set_connection(connection, credentials)
 
149
 
 
150
    def _translate_perm_error(self, err, path, extra=None,
 
151
                              unknown_exc=FtpPathError):
142
152
        """Try to translate an ftplib.error_perm exception.
143
153
 
144
154
        :param err: The error to translate into a bzr error
155
165
        if ('no such file' in s
156
166
            or 'could not open' in s
157
167
            or 'no such dir' in s
 
168
            or 'could not create file' in s # vsftpd
 
169
            or 'file doesn\'t exist' in s
 
170
            or 'file/directory not found' in s # filezilla server
158
171
            ):
159
172
            raise errors.NoSuchFile(path, extra=extra)
160
173
        if ('file exists' in s):
173
186
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
174
187
        raise
175
188
 
176
 
    def should_cache(self):
177
 
        """Return True if the data pulled across should be cached locally.
178
 
        """
179
 
        return True
180
 
 
181
 
    def clone(self, offset=None):
182
 
        """Return a new FtpTransport with root at self.base + offset.
183
 
        """
184
 
        mutter("FTP clone")
185
 
        if offset is None:
186
 
            return FtpTransport(self.base, self._FTP_instance)
187
 
        else:
188
 
            return FtpTransport(self.abspath(offset), self._FTP_instance)
189
 
 
190
 
    def _abspath(self, relpath):
191
 
        assert isinstance(relpath, basestring)
192
 
        relpath = urllib.unquote(relpath)
193
 
        relpath_parts = relpath.split('/')
194
 
        if len(relpath_parts) > 1:
195
 
            if relpath_parts[0] == '':
196
 
                raise ValueError("path %r within branch %r seems to be absolute"
197
 
                                 % (relpath, self._path))
198
 
        basepath = self._path.split('/')
199
 
        if len(basepath) > 0 and basepath[-1] == '':
200
 
            basepath = basepath[:-1]
201
 
        for p in relpath_parts:
202
 
            if p == '..':
203
 
                if len(basepath) == 0:
204
 
                    # In most filesystems, a request for the parent
205
 
                    # of root, just returns root.
206
 
                    continue
207
 
                basepath.pop()
208
 
            elif p == '.' or p == '':
209
 
                continue # No-op
210
 
            else:
211
 
                basepath.append(p)
212
 
        # Possibly, we could use urlparse.urljoin() here, but
213
 
        # I'm concerned about when it chooses to strip the last
214
 
        # portion of the path, and when it doesn't.
215
 
        return '/'.join(basepath) or '/'
216
 
    
217
 
    def abspath(self, relpath):
218
 
        """Return the full url to the given relative path.
219
 
        This can be supplied with a string or a list
220
 
        """
221
 
        path = self._abspath(relpath)
222
 
        return self._unparse_url(path)
 
189
    def _remote_path(self, relpath):
 
190
        # XXX: It seems that ftplib does not handle Unicode paths
 
191
        # at the same time, medusa won't handle utf8 paths So if
 
192
        # we .encode(utf8) here (see ConnectedTransport
 
193
        # implementation), then we get a Server failure.  while
 
194
        # if we use str(), we get a UnicodeError, and the test
 
195
        # suite just skips testing UnicodePaths.
 
196
        relative = str(urlutils.unescape(relpath))
 
197
        remote_path = self._combine_paths(self._path, relative)
 
198
        return remote_path
223
199
 
224
200
    def has(self, relpath):
225
201
        """Does the target location exist?"""
228
204
        # XXX: I assume we're never asked has(dirname) and thus I use
229
205
        # the FTP size command and assume that if it doesn't raise,
230
206
        # all is good.
231
 
        abspath = self._abspath(relpath)
 
207
        abspath = self._remote_path(relpath)
232
208
        try:
233
209
            f = self._get_FTP()
234
210
            mutter('FTP has check: %s => %s', relpath, abspath)
254
230
        """
255
231
        # TODO: decode should be deprecated
256
232
        try:
257
 
            mutter("FTP get: %s", self._abspath(relpath))
 
233
            mutter("FTP get: %s", self._remote_path(relpath))
258
234
            f = self._get_FTP()
259
235
            ret = StringIO()
260
 
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
 
236
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
261
237
            ret.seek(0)
262
238
            return ret
263
239
        except ftplib.error_perm, e:
269
245
                                     orig_error=e)
270
246
            else:
271
247
                warning("FTP temporary error: %s. Retrying.", str(e))
272
 
                self._FTP_instance = None
 
248
                self._reconnect()
273
249
                return self.get(relpath, decode, retries+1)
274
250
        except EOFError, e:
275
251
            if retries > _number_of_retries:
279
255
            else:
280
256
                warning("FTP control connection closed. Trying to reopen.")
281
257
                time.sleep(_sleep_between_retries)
282
 
                self._FTP_instance = None
 
258
                self._reconnect()
283
259
                return self.get(relpath, decode, retries+1)
284
260
 
285
 
    def put(self, relpath, fp, mode=None, retries=0):
 
261
    def put_file(self, relpath, fp, mode=None, retries=0):
286
262
        """Copy the file-like or string object into the location.
287
263
 
288
264
        :param relpath: Location to put the contents, relative to base.
290
266
        :param retries: Number of retries after temporary failures so far
291
267
                        for this operation.
292
268
 
293
 
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
 
269
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
 
270
        ftplib does not
294
271
        """
295
 
        abspath = self._abspath(relpath)
 
272
        abspath = self._remote_path(relpath)
296
273
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
297
274
                        os.getpid(), random.randint(0,0x7FFFFFFF))
298
 
        if not hasattr(fp, 'read'):
299
 
            fp = StringIO(fp)
 
275
        bytes = None
 
276
        if getattr(fp, 'read', None) is None:
 
277
            # hand in a string IO
 
278
            bytes = fp
 
279
            fp = StringIO(bytes)
 
280
        else:
 
281
            # capture the byte count; .read() may be read only so
 
282
            # decorate it.
 
283
            class byte_counter(object):
 
284
                def __init__(self, fp):
 
285
                    self.fp = fp
 
286
                    self.counted_bytes = 0
 
287
                def read(self, count):
 
288
                    result = self.fp.read(count)
 
289
                    self.counted_bytes += len(result)
 
290
                    return result
 
291
            fp = byte_counter(fp)
300
292
        try:
301
293
            mutter("FTP put: %s", abspath)
302
294
            f = self._get_FTP()
303
295
            try:
304
296
                f.storbinary('STOR '+tmp_abspath, fp)
305
 
                f.rename(tmp_abspath, abspath)
 
297
                self._rename_and_overwrite(tmp_abspath, abspath, f)
 
298
                if bytes is not None:
 
299
                    return len(bytes)
 
300
                else:
 
301
                    return fp.counted_bytes
306
302
            except (ftplib.error_temp,EOFError), e:
307
303
                warning("Failure during ftp PUT. Deleting temporary file.")
308
304
                try:
313
309
                    raise e
314
310
                raise
315
311
        except ftplib.error_perm, e:
316
 
            self._translate_perm_error(e, abspath, extra='could not store')
 
312
            self._translate_perm_error(e, abspath, extra='could not store',
 
313
                                       unknown_exc=errors.NoSuchFile)
317
314
        except ftplib.error_temp, e:
318
315
            if retries > _number_of_retries:
319
316
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
320
317
                                     % self.abspath(relpath), orig_error=e)
321
318
            else:
322
319
                warning("FTP temporary error: %s. Retrying.", str(e))
323
 
                self._FTP_instance = None
324
 
                self.put(relpath, fp, mode, retries+1)
 
320
                self._reconnect()
 
321
                self.put_file(relpath, fp, mode, retries+1)
325
322
        except EOFError:
326
323
            if retries > _number_of_retries:
327
324
                raise errors.TransportError("FTP control connection closed during PUT %s."
329
326
            else:
330
327
                warning("FTP control connection closed. Trying to reopen.")
331
328
                time.sleep(_sleep_between_retries)
332
 
                self._FTP_instance = None
333
 
                self.put(relpath, fp, mode, retries+1)
 
329
                self._reconnect()
 
330
                self.put_file(relpath, fp, mode, retries+1)
334
331
 
335
332
    def mkdir(self, relpath, mode=None):
336
333
        """Create a directory at the given path."""
337
 
        abspath = self._abspath(relpath)
 
334
        abspath = self._remote_path(relpath)
338
335
        try:
339
336
            mutter("FTP mkd: %s", abspath)
340
337
            f = self._get_FTP()
343
340
            self._translate_perm_error(e, abspath,
344
341
                unknown_exc=errors.FileExists)
345
342
 
 
343
    def open_write_stream(self, relpath, mode=None):
 
344
        """See Transport.open_write_stream."""
 
345
        self.put_bytes(relpath, "", mode)
 
346
        result = AppendBasedFileStream(self, relpath)
 
347
        _file_streams[self.abspath(relpath)] = result
 
348
        return result
 
349
 
 
350
    def recommended_page_size(self):
 
351
        """See Transport.recommended_page_size().
 
352
 
 
353
        For FTP we suggest a large page size to reduce the overhead
 
354
        introduced by latency.
 
355
        """
 
356
        return 64 * 1024
 
357
 
346
358
    def rmdir(self, rel_path):
347
359
        """Delete the directory at rel_path"""
348
 
        abspath = self._abspath(rel_path)
 
360
        abspath = self._remote_path(rel_path)
349
361
        try:
350
362
            mutter("FTP rmd: %s", abspath)
351
363
            f = self._get_FTP()
353
365
        except ftplib.error_perm, e:
354
366
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
355
367
 
356
 
    def append(self, relpath, f, mode=None):
 
368
    def append_file(self, relpath, f, mode=None):
357
369
        """Append the text in the file-like object into the final
358
370
        location.
359
371
        """
360
 
        abspath = self._abspath(relpath)
 
372
        abspath = self._remote_path(relpath)
361
373
        if self.has(relpath):
362
374
            ftp = self._get_FTP()
363
375
            result = ftp.size(abspath)
376
388
        number of retries is exceeded.
377
389
        """
378
390
        try:
379
 
            abspath = self._abspath(relpath)
 
391
            abspath = self._remote_path(relpath)
380
392
            mutter("FTP appe (try %d) to %s", retries, abspath)
381
393
            ftp = self._get_FTP()
382
394
            ftp.voidcmd("TYPE I")
396
408
                        "Aborting." % abspath, orig_error=e)
397
409
            else:
398
410
                warning("FTP temporary error: %s. Retrying.", str(e))
399
 
                self._FTP_instance = None
 
411
                self._reconnect()
400
412
                self._try_append(relpath, text, mode, retries+1)
401
413
 
402
414
    def _setmode(self, relpath, mode):
407
419
        """
408
420
        try:
409
421
            mutter("FTP site chmod: setting permissions to %s on %s",
410
 
                str(mode), self._abspath(relpath))
 
422
                str(mode), self._remote_path(relpath))
411
423
            ftp = self._get_FTP()
412
 
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
 
424
            cmd = "SITE CHMOD %s %s" % (self._remote_path(relpath), str(mode))
413
425
            ftp.sendcmd(cmd)
414
426
        except ftplib.error_perm, e:
415
427
            # Command probably not available on this server
416
428
            warning("FTP Could not set permissions to %s on %s. %s",
417
 
                    str(mode), self._abspath(relpath), str(e))
 
429
                    str(mode), self._remote_path(relpath), str(e))
418
430
 
419
431
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
420
432
    #       to copy something to another machine. And you may be able
421
433
    #       to give it its own address as the 'to' location.
422
434
    #       So implement a fancier 'copy()'
423
435
 
 
436
    def rename(self, rel_from, rel_to):
 
437
        abs_from = self._remote_path(rel_from)
 
438
        abs_to = self._remote_path(rel_to)
 
439
        mutter("FTP rename: %s => %s", abs_from, abs_to)
 
440
        f = self._get_FTP()
 
441
        return self._rename(abs_from, abs_to, f)
 
442
 
 
443
    def _rename(self, abs_from, abs_to, f):
 
444
        try:
 
445
            f.rename(abs_from, abs_to)
 
446
        except ftplib.error_perm, e:
 
447
            self._translate_perm_error(e, abs_from,
 
448
                ': unable to rename to %r' % (abs_to))
 
449
 
424
450
    def move(self, rel_from, rel_to):
425
451
        """Move the item at rel_from to the location at rel_to"""
426
 
        abs_from = self._abspath(rel_from)
427
 
        abs_to = self._abspath(rel_to)
 
452
        abs_from = self._remote_path(rel_from)
 
453
        abs_to = self._remote_path(rel_to)
428
454
        try:
429
455
            mutter("FTP mv: %s => %s", abs_from, abs_to)
430
456
            f = self._get_FTP()
431
 
            f.rename(abs_from, abs_to)
 
457
            self._rename_and_overwrite(abs_from, abs_to, f)
432
458
        except ftplib.error_perm, e:
433
459
            self._translate_perm_error(e, abs_from,
434
460
                extra='unable to rename to %r' % (rel_to,), 
435
461
                unknown_exc=errors.PathError)
436
462
 
437
 
    rename = move
 
463
    def _rename_and_overwrite(self, abs_from, abs_to, f):
 
464
        """Do a fancy rename on the remote server.
 
465
 
 
466
        Using the implementation provided by osutils.
 
467
        """
 
468
        osutils.fancy_rename(abs_from, abs_to,
 
469
            rename_func=lambda p1, p2: self._rename(p1, p2, f),
 
470
            unlink_func=lambda p: self._delete(p, f))
438
471
 
439
472
    def delete(self, relpath):
440
473
        """Delete the item at relpath"""
441
 
        abspath = self._abspath(relpath)
 
474
        abspath = self._remote_path(relpath)
 
475
        f = self._get_FTP()
 
476
        self._delete(abspath, f)
 
477
 
 
478
    def _delete(self, abspath, f):
442
479
        try:
443
480
            mutter("FTP rm: %s", abspath)
444
 
            f = self._get_FTP()
445
481
            f.delete(abspath)
446
482
        except ftplib.error_perm, e:
447
483
            self._translate_perm_error(e, abspath, 'error deleting',
448
484
                unknown_exc=errors.NoSuchFile)
449
485
 
 
486
    def external_url(self):
 
487
        """See bzrlib.transport.Transport.external_url."""
 
488
        # FTP URL's are externally usable.
 
489
        return self.base
 
490
 
450
491
    def listable(self):
451
492
        """See Transport.listable."""
452
493
        return True
453
494
 
454
495
    def list_dir(self, relpath):
455
496
        """See Transport.list_dir."""
 
497
        basepath = self._remote_path(relpath)
 
498
        mutter("FTP nlst: %s", basepath)
 
499
        f = self._get_FTP()
456
500
        try:
457
 
            mutter("FTP nlst: %s", self._abspath(relpath))
458
 
            f = self._get_FTP()
459
 
            basepath = self._abspath(relpath)
460
501
            paths = f.nlst(basepath)
461
 
            # If FTP.nlst returns paths prefixed by relpath, strip 'em
462
 
            if paths and paths[0].startswith(basepath):
463
 
                paths = [path[len(basepath)+1:] for path in paths]
464
 
            # Remove . and .. if present, and return
465
 
            return [path for path in paths if path not in (".", "..")]
466
502
        except ftplib.error_perm, e:
467
503
            self._translate_perm_error(e, relpath, extra='error with list_dir')
 
504
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
 
505
        if paths and paths[0].startswith(basepath):
 
506
            entries = [path[len(basepath)+1:] for path in paths]
 
507
        else:
 
508
            entries = paths
 
509
        # Remove . and .. if present
 
510
        return [urlutils.escape(entry) for entry in entries
 
511
                if entry not in ('.', '..')]
468
512
 
469
513
    def iter_files_recursive(self):
470
514
        """See Transport.iter_files_recursive.
473
517
        mutter("FTP iter_files_recursive")
474
518
        queue = list(self.list_dir("."))
475
519
        while queue:
476
 
            relpath = urllib.quote(queue.pop(0))
 
520
            relpath = queue.pop(0)
477
521
            st = self.stat(relpath)
478
522
            if stat.S_ISDIR(st.st_mode):
479
523
                for i, basename in enumerate(self.list_dir(relpath)):
483
527
 
484
528
    def stat(self, relpath):
485
529
        """Return the stat information for a file."""
486
 
        abspath = self._abspath(relpath)
 
530
        abspath = self._remote_path(relpath)
487
531
        try:
488
532
            mutter("FTP stat: %s", abspath)
489
533
            f = self._get_FTP()
514
558
 
515
559
 
516
560
class FtpServer(Server):
517
 
    """Common code for SFTP server facilities."""
 
561
    """Common code for FTP server facilities."""
518
562
 
519
563
    def __init__(self):
520
564
        self._root = None
536
580
        """This is used by medusa.ftp_server to log connections, etc."""
537
581
        self.logs.append(message)
538
582
 
539
 
    def setUp(self):
540
 
 
 
583
    def setUp(self, vfs_server=None):
541
584
        if not _have_medusa:
542
585
            raise RuntimeError('Must have medusa to run the FtpServer')
543
586
 
 
587
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
 
588
            "FtpServer currently assumes local transport, got %s" % vfs_server
 
589
 
544
590
        self._root = os.getcwdu()
545
591
        self._ftp_server = _ftp_server(
546
592
            authorizer=_test_authorizer(root=self._root),
551
597
            )
552
598
        self._port = self._ftp_server.getsockname()[1]
553
599
        # Don't let it loop forever, or handle an infinite number of requests.
554
 
        # In this case it will run for 100s, or 1000 requests
555
 
        self._async_thread = threading.Thread(target=asyncore.loop,
556
 
                kwargs={'timeout':0.1, 'count':1000})
 
600
        # In this case it will run for 1000s, or 10000 requests
 
601
        self._async_thread = threading.Thread(
 
602
                target=FtpServer._asyncore_loop_ignore_EBADF,
 
603
                kwargs={'timeout':0.1, 'count':10000})
557
604
        self._async_thread.setDaemon(True)
558
605
        self._async_thread.start()
559
606
 
560
607
    def tearDown(self):
561
608
        """See bzrlib.transport.Server.tearDown."""
562
 
        # have asyncore release the channel
563
 
        self._ftp_server.del_channel()
 
609
        self._ftp_server.close()
564
610
        asyncore.close_all()
565
611
        self._async_thread.join()
566
612
 
 
613
    @staticmethod
 
614
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
 
615
        """Ignore EBADF during server shutdown.
 
616
 
 
617
        We close the socket to get the server to shutdown, but this causes
 
618
        select.select() to raise EBADF.
 
619
        """
 
620
        try:
 
621
            asyncore.loop(*args, **kwargs)
 
622
            # FIXME: If we reach that point, we should raise an exception
 
623
            # explaining that the 'count' parameter in setUp is too low or
 
624
            # testers may wonder why their test just sits there waiting for a
 
625
            # server that is already dead. Note that if the tester waits too
 
626
            # long under pdb the server will also die.
 
627
        except select.error, e:
 
628
            if e.args[0] != errno.EBADF:
 
629
                raise
 
630
 
567
631
 
568
632
_ftp_channel = None
569
633
_ftp_server = None
590
654
 
591
655
        def __init__(self, root):
592
656
            self.root = root
 
657
            # If secured_user is set secured_password will be checked
 
658
            self.secured_user = None
 
659
            self.secured_password = None
593
660
 
594
661
        def authorize(self, channel, username, password):
595
662
            """Return (success, reply_string, filesystem)"""
602
669
            else:
603
670
                channel.read_only = 0
604
671
 
605
 
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
 
672
            # Check secured_user if set
 
673
            if (self.secured_user is not None
 
674
                and username == self.secured_user
 
675
                and password != self.secured_password):
 
676
                return 0, 'Password invalid.', None
 
677
            else:
 
678
                return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
606
679
 
607
680
 
608
681
    class ftp_channel(medusa.ftp_server.ftp_channel):
611
684
        def log(self, message):
612
685
            """Redirect logging requests."""
613
686
            mutter('_ftp_channel: %s', message)
614
 
            
 
687
 
615
688
        def log_info(self, message, type='info'):
616
689
            """Redirect logging requests."""
617
690
            mutter('_ftp_channel %s: %s', type, message)
618
 
            
 
691
 
619
692
        def cmd_rnfr(self, line):
620
693
            """Prepare for renaming a file."""
621
694
            self._renaming = line[1]
634
707
            pfrom = self.filesystem.translate(self._renaming)
635
708
            self._renaming = None
636
709
            pto = self.filesystem.translate(line[1])
 
710
            if os.path.exists(pto):
 
711
                self.respond('550 RNTO failed: file exists')
 
712
                return
637
713
            try:
638
714
                os.rename(pfrom, pto)
639
715
            except (IOError, OSError), e:
640
716
                # TODO: jam 20060516 return custom responses based on
641
717
                #       why the command failed
642
 
                self.respond('550 RNTO failed: %s' % (e,))
 
718
                # (bialix 20070418) str(e) on Python 2.5 @ Windows
 
719
                # sometimes don't provide expected error message;
 
720
                # so we obtain such message via os.strerror()
 
721
                self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
643
722
            except:
644
723
                self.respond('550 RNTO failed')
645
724
                # For a test server, we will go ahead and just die
677
756
                    self.filesystem.mkdir (path)
678
757
                    self.respond ('257 MKD command successful.')
679
758
                except (IOError, OSError), e:
680
 
                    self.respond ('550 error creating directory: %s' % (e,))
 
759
                    # (bialix 20070418) str(e) on Python 2.5 @ Windows
 
760
                    # sometimes don't provide expected error message;
 
761
                    # so we obtain such message via os.strerror()
 
762
                    self.respond ('550 error creating directory: %s' %
 
763
                                  os.strerror(e.errno))
681
764
                except:
682
765
                    self.respond ('550 error creating directory.')
683
766