~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

first cut at merge from integration.

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
"""
26
26
 
27
27
from cStringIO import StringIO
28
 
import asyncore
29
28
import errno
30
29
import ftplib
31
30
import os
32
31
import urllib
33
32
import urlparse
34
33
import stat
35
 
import threading
36
 
import time
37
 
import random
38
34
from warnings import warn
39
35
 
40
 
from bzrlib.transport import (
41
 
    Transport,
42
 
    Server,
43
 
    split_url,
44
 
    )
45
 
import bzrlib.errors as errors
46
 
from bzrlib.trace import mutter, warning
47
 
import bzrlib.ui
48
 
 
49
 
_have_medusa = False
50
 
 
51
 
 
52
 
class FtpPathError(errors.PathError):
53
 
    """FTP failed for path: %(path)s%(extra)s"""
54
 
 
55
 
 
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]    
 
36
 
 
37
from bzrlib.transport import Transport
 
38
from bzrlib.errors import (TransportNotPossible, TransportError,
 
39
                           NoSuchFile, FileExists)
 
40
from bzrlib.trace import mutter
76
41
 
77
42
 
78
43
class FtpStatResult(object):
89
54
                f.cwd(pwd)
90
55
 
91
56
 
92
 
_number_of_retries = 2
93
 
_sleep_between_retries = 5
94
 
 
95
57
class FtpTransport(Transport):
96
58
    """This is the transport agent for ftp:// access."""
97
59
 
98
60
    def __init__(self, base, _provided_instance=None):
99
61
        """Set the base path where files will be stored."""
100
62
        assert base.startswith('ftp://') or base.startswith('aftp://')
101
 
 
 
63
        super(FtpTransport, self).__init__(base)
102
64
        self.is_active = base.startswith('aftp://')
103
65
        if self.is_active:
104
 
            # urlparse won't handle aftp://
105
66
            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)
 
67
        (self._proto, self._host,
 
68
            self._path, self._parameters,
 
69
            self._query, self._fragment) = urlparse.urlparse(self.base)
114
70
        self._FTP_instance = _provided_instance
115
71
 
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, '', '', ''))
126
72
 
127
73
    def _get_FTP(self):
128
74
        """Return the ftplib.FTP instance for this object."""
130
76
            return self._FTP_instance
131
77
        
132
78
        try:
133
 
            self._FTP_instance = _find_FTP(self._host, self._port,
134
 
                                           self._username, self._password,
135
 
                                           self.is_active)
 
79
            username = ''
 
80
            password = ''
 
81
            hostname = self._host
 
82
            if '@' in hostname:
 
83
                username, hostname = hostname.split("@", 1)
 
84
            if ':' in username:
 
85
                username, password = username.split(":", 1)
 
86
 
 
87
            mutter("Constructing FTP instance")
 
88
            self._FTP_instance = ftplib.FTP(hostname, username, password)
 
89
            self._FTP_instance.set_pasv(not self.is_active)
136
90
            return self._FTP_instance
137
91
        except ftplib.error_perm, e:
138
 
            raise errors.TransportError(msg="Error setting up connection: %s"
 
92
            raise TransportError(msg="Error setting up connection: %s"
139
93
                                    % str(e), orig_error=e)
140
94
 
141
 
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
142
 
        """Try to translate an ftplib.error_perm exception.
143
 
 
144
 
        :param err: The error to translate into a bzr error
145
 
        :param path: The path which had problems
146
 
        :param extra: Extra information which can be included
147
 
        :param unknown_exc: If None, we will just raise the original exception
148
 
                    otherwise we raise unknown_exc(path, extra=extra)
149
 
        """
150
 
        s = str(err).lower()
151
 
        if not extra:
152
 
            extra = str(err)
153
 
        else:
154
 
            extra += ': ' + str(err)
155
 
        if ('no such file' in s
156
 
            or 'could not open' in s
157
 
            or 'no such dir' in s
158
 
            ):
159
 
            raise errors.NoSuchFile(path, extra=extra)
160
 
        if ('file exists' in s):
161
 
            raise errors.FileExists(path, extra=extra)
162
 
        if ('not a directory' in s):
163
 
            raise errors.PathError(path, extra=extra)
164
 
 
165
 
        mutter('unable to understand error for path: %s: %s', path, err)
166
 
 
167
 
        if unknown_exc:
168
 
            raise unknown_exc(path, extra=extra)
169
 
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
170
 
        #       something like TransportError, but this loses the traceback
171
 
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
172
 
        #       to handle. Consider doing something like that here.
173
 
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
174
 
        raise
175
 
 
176
95
    def should_cache(self):
177
96
        """Return True if the data pulled across should be cached locally.
178
97
        """
190
109
    def _abspath(self, relpath):
191
110
        assert isinstance(relpath, basestring)
192
111
        relpath = urllib.unquote(relpath)
193
 
        relpath_parts = relpath.split('/')
 
112
        if isinstance(relpath, basestring):
 
113
            relpath_parts = relpath.split('/')
 
114
        else:
 
115
            # TODO: Don't call this with an array - no magic interfaces
 
116
            relpath_parts = relpath[:]
194
117
        if len(relpath_parts) > 1:
195
118
            if relpath_parts[0] == '':
196
119
                raise ValueError("path %r within branch %r seems to be absolute"
212
135
        # Possibly, we could use urlparse.urljoin() here, but
213
136
        # I'm concerned about when it chooses to strip the last
214
137
        # portion of the path, and when it doesn't.
215
 
        return '/'.join(basepath) or '/'
 
138
        return '/'.join(basepath)
216
139
    
217
140
    def abspath(self, relpath):
218
141
        """Return the full url to the given relative path.
219
142
        This can be supplied with a string or a list
220
143
        """
221
144
        path = self._abspath(relpath)
222
 
        return self._unparse_url(path)
 
145
        return urlparse.urlunparse((self._proto,
 
146
                self._host, path, '', '', ''))
223
147
 
224
148
    def has(self, relpath):
225
 
        """Does the target location exist?"""
226
 
        # FIXME jam 20060516 We *do* ask about directories in the test suite
227
 
        #       We don't seem to in the actual codebase
228
 
        # XXX: I assume we're never asked has(dirname) and thus I use
229
 
        # the FTP size command and assume that if it doesn't raise,
230
 
        # all is good.
231
 
        abspath = self._abspath(relpath)
 
149
        """Does the target location exist?
 
150
 
 
151
        XXX: I assume we're never asked has(dirname) and thus I use
 
152
        the FTP size command and assume that if it doesn't raise,
 
153
        all is good.
 
154
        """
232
155
        try:
233
156
            f = self._get_FTP()
234
 
            mutter('FTP has check: %s => %s', relpath, abspath)
235
 
            s = f.size(abspath)
236
 
            mutter("FTP has: %s", abspath)
 
157
            s = f.size(self._abspath(relpath))
 
158
            mutter("FTP has: %s" % self._abspath(relpath))
237
159
            return True
238
 
        except ftplib.error_perm, e:
239
 
            if ('is a directory' in str(e).lower()):
240
 
                mutter("FTP has dir: %s: %s", abspath, e)
241
 
                return True
242
 
            mutter("FTP has not: %s: %s", abspath, e)
 
160
        except ftplib.error_perm:
 
161
            mutter("FTP has not: %s" % self._abspath(relpath))
243
162
            return False
244
163
 
245
 
    def get(self, relpath, decode=False, retries=0):
 
164
    def get(self, relpath, decode=False):
246
165
        """Get the file at the given relative path.
247
166
 
248
167
        :param relpath: The relative path to the file
249
 
        :param retries: Number of retries after temporary failures so far
250
 
                        for this operation.
251
168
 
252
169
        We're meant to return a file-like object which bzr will
253
170
        then read from. For now we do this via the magic of StringIO
254
171
        """
255
 
        # TODO: decode should be deprecated
256
172
        try:
257
 
            mutter("FTP get: %s", self._abspath(relpath))
 
173
            mutter("FTP get: %s" % self._abspath(relpath))
258
174
            f = self._get_FTP()
259
175
            ret = StringIO()
260
176
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
261
177
            ret.seek(0)
262
178
            return ret
263
179
        except ftplib.error_perm, e:
264
 
            raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
265
 
        except ftplib.error_temp, e:
266
 
            if retries > _number_of_retries:
267
 
                raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
268
 
                                     % self.abspath(relpath),
269
 
                                     orig_error=e)
270
 
            else:
271
 
                warning("FTP temporary error: %s. Retrying.", str(e))
272
 
                self._FTP_instance = None
273
 
                return self.get(relpath, decode, retries+1)
274
 
        except EOFError, e:
275
 
            if retries > _number_of_retries:
276
 
                raise errors.TransportError("FTP control connection closed during GET %s."
277
 
                                     % self.abspath(relpath),
278
 
                                     orig_error=e)
279
 
            else:
280
 
                warning("FTP control connection closed. Trying to reopen.")
281
 
                time.sleep(_sleep_between_retries)
282
 
                self._FTP_instance = None
283
 
                return self.get(relpath, decode, retries+1)
 
180
            raise NoSuchFile(self.abspath(relpath), extra=str(e))
284
181
 
285
 
    def put(self, relpath, fp, mode=None, retries=0):
 
182
    def put(self, relpath, fp, mode=None):
286
183
        """Copy the file-like or string object into the location.
287
184
 
288
185
        :param relpath: Location to put the contents, relative to base.
289
 
        :param fp:       File-like or string object.
290
 
        :param retries: Number of retries after temporary failures so far
291
 
                        for this operation.
292
 
 
 
186
        :param f:       File-like or string object.
 
187
        TODO: jam 20051215 This should be an atomic put, not overwritting files in place
293
188
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
294
189
        """
295
 
        abspath = self._abspath(relpath)
296
 
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
297
 
                        os.getpid(), random.randint(0,0x7FFFFFFF))
298
190
        if not hasattr(fp, 'read'):
299
191
            fp = StringIO(fp)
300
192
        try:
301
 
            mutter("FTP put: %s", abspath)
 
193
            mutter("FTP put: %s" % self._abspath(relpath))
302
194
            f = self._get_FTP()
303
 
            try:
304
 
                f.storbinary('STOR '+tmp_abspath, fp)
305
 
                f.rename(tmp_abspath, abspath)
306
 
            except (ftplib.error_temp,EOFError), e:
307
 
                warning("Failure during ftp PUT. Deleting temporary file.")
308
 
                try:
309
 
                    f.delete(tmp_abspath)
310
 
                except:
311
 
                    warning("Failed to delete temporary file on the"
312
 
                            " server.\nFile: %s", tmp_abspath)
313
 
                    raise e
314
 
                raise
 
195
            f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
315
196
        except ftplib.error_perm, e:
316
 
            self._translate_perm_error(e, abspath, extra='could not store')
317
 
        except ftplib.error_temp, e:
318
 
            if retries > _number_of_retries:
319
 
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
320
 
                                     % self.abspath(relpath), orig_error=e)
321
 
            else:
322
 
                warning("FTP temporary error: %s. Retrying.", str(e))
323
 
                self._FTP_instance = None
324
 
                self.put(relpath, fp, mode, retries+1)
325
 
        except EOFError:
326
 
            if retries > _number_of_retries:
327
 
                raise errors.TransportError("FTP control connection closed during PUT %s."
328
 
                                     % self.abspath(relpath), orig_error=e)
329
 
            else:
330
 
                warning("FTP control connection closed. Trying to reopen.")
331
 
                time.sleep(_sleep_between_retries)
332
 
                self._FTP_instance = None
333
 
                self.put(relpath, fp, mode, retries+1)
 
197
            raise TransportError(orig_error=e)
334
198
 
335
199
    def mkdir(self, relpath, mode=None):
336
200
        """Create a directory at the given path."""
337
 
        abspath = self._abspath(relpath)
338
 
        try:
339
 
            mutter("FTP mkd: %s", abspath)
340
 
            f = self._get_FTP()
341
 
            f.mkd(abspath)
342
 
        except ftplib.error_perm, e:
343
 
            self._translate_perm_error(e, abspath,
344
 
                unknown_exc=errors.FileExists)
345
 
 
346
 
    def rmdir(self, rel_path):
347
 
        """Delete the directory at rel_path"""
348
 
        abspath = self._abspath(rel_path)
349
 
        try:
350
 
            mutter("FTP rmd: %s", abspath)
351
 
            f = self._get_FTP()
352
 
            f.rmd(abspath)
353
 
        except ftplib.error_perm, e:
354
 
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
355
 
 
356
 
    def append(self, relpath, f, mode=None):
 
201
        try:
 
202
            mutter("FTP mkd: %s" % self._abspath(relpath))
 
203
            f = self._get_FTP()
 
204
            try:
 
205
                f.mkd(self._abspath(relpath))
 
206
            except ftplib.error_perm, e:
 
207
                s = str(e)
 
208
                if 'File exists' in s:
 
209
                    raise FileExists(self.abspath(relpath), extra=s)
 
210
                else:
 
211
                    raise
 
212
        except ftplib.error_perm, e:
 
213
            raise TransportError(orig_error=e)
 
214
 
 
215
    def append(self, relpath, f):
357
216
        """Append the text in the file-like object into the final
358
217
        location.
359
218
        """
360
 
        abspath = self._abspath(relpath)
361
 
        if self.has(relpath):
362
 
            ftp = self._get_FTP()
363
 
            result = ftp.size(abspath)
364
 
        else:
365
 
            result = 0
366
 
 
367
 
        mutter("FTP appe to %s", abspath)
368
 
        self._try_append(relpath, f.read(), mode)
369
 
 
370
 
        return result
371
 
 
372
 
    def _try_append(self, relpath, text, mode=None, retries=0):
373
 
        """Try repeatedly to append the given text to the file at relpath.
374
 
        
375
 
        This is a recursive function. On errors, it will be called until the
376
 
        number of retries is exceeded.
377
 
        """
378
 
        try:
379
 
            abspath = self._abspath(relpath)
380
 
            mutter("FTP appe (try %d) to %s", retries, abspath)
381
 
            ftp = self._get_FTP()
382
 
            ftp.voidcmd("TYPE I")
383
 
            cmd = "APPE %s" % abspath
384
 
            conn = ftp.transfercmd(cmd)
385
 
            conn.sendall(text)
386
 
            conn.close()
387
 
            if mode:
388
 
                self._setmode(relpath, mode)
389
 
            ftp.getresp()
390
 
        except ftplib.error_perm, e:
391
 
            self._translate_perm_error(e, abspath, extra='error appending',
392
 
                unknown_exc=errors.NoSuchFile)
393
 
        except ftplib.error_temp, e:
394
 
            if retries > _number_of_retries:
395
 
                raise errors.TransportError("FTP temporary error during APPEND %s." \
396
 
                        "Aborting." % abspath, orig_error=e)
397
 
            else:
398
 
                warning("FTP temporary error: %s. Retrying.", str(e))
399
 
                self._FTP_instance = None
400
 
                self._try_append(relpath, text, mode, retries+1)
401
 
 
402
 
    def _setmode(self, relpath, mode):
403
 
        """Set permissions on a path.
404
 
 
405
 
        Only set permissions if the FTP server supports the 'SITE CHMOD'
406
 
        extension.
407
 
        """
408
 
        try:
409
 
            mutter("FTP site chmod: setting permissions to %s on %s",
410
 
                str(mode), self._abspath(relpath))
411
 
            ftp = self._get_FTP()
412
 
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
413
 
            ftp.sendcmd(cmd)
414
 
        except ftplib.error_perm, e:
415
 
            # Command probably not available on this server
416
 
            warning("FTP Could not set permissions to %s on %s. %s",
417
 
                    str(mode), self._abspath(relpath), str(e))
418
 
 
419
 
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
420
 
    #       to copy something to another machine. And you may be able
421
 
    #       to give it its own address as the 'to' location.
422
 
    #       So implement a fancier 'copy()'
 
219
        raise TransportNotPossible('ftp does not support append()')
 
220
 
 
221
    def copy(self, rel_from, rel_to):
 
222
        """Copy the item at rel_from to the location at rel_to"""
 
223
        raise TransportNotPossible('ftp does not (yet) support copy()')
423
224
 
424
225
    def move(self, rel_from, rel_to):
425
226
        """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)
428
227
        try:
429
 
            mutter("FTP mv: %s => %s", abs_from, abs_to)
 
228
            mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
 
229
                                         self._abspath(rel_to)))
430
230
            f = self._get_FTP()
431
 
            f.rename(abs_from, abs_to)
 
231
            f.rename(self._abspath(rel_from), self._abspath(rel_to))
432
232
        except ftplib.error_perm, e:
433
 
            self._translate_perm_error(e, abs_from,
434
 
                extra='unable to rename to %r' % (rel_to,), 
435
 
                unknown_exc=errors.PathError)
436
 
 
437
 
    rename = move
 
233
            raise TransportError(orig_error=e)
438
234
 
439
235
    def delete(self, relpath):
440
236
        """Delete the item at relpath"""
441
 
        abspath = self._abspath(relpath)
442
237
        try:
443
 
            mutter("FTP rm: %s", abspath)
 
238
            mutter("FTP rm: %s" % self._abspath(relpath))
444
239
            f = self._get_FTP()
445
 
            f.delete(abspath)
 
240
            f.delete(self._abspath(relpath))
446
241
        except ftplib.error_perm, e:
447
 
            self._translate_perm_error(e, abspath, 'error deleting',
448
 
                unknown_exc=errors.NoSuchFile)
 
242
            raise TransportError(orig_error=e)
449
243
 
450
244
    def listable(self):
451
245
        """See Transport.listable."""
454
248
    def list_dir(self, relpath):
455
249
        """See Transport.list_dir."""
456
250
        try:
457
 
            mutter("FTP nlst: %s", self._abspath(relpath))
 
251
            mutter("FTP nlst: %s" % self._abspath(relpath))
458
252
            f = self._get_FTP()
459
253
            basepath = self._abspath(relpath)
460
 
            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]
 
254
            # FTP.nlst returns paths prefixed by relpath, strip 'em
 
255
            the_list = f.nlst(basepath)
 
256
            stripped = [path[len(basepath)+1:] for path in the_list]
464
257
            # Remove . and .. if present, and return
465
 
            return [path for path in paths if path not in (".", "..")]
 
258
            return [path for path in stripped if path not in (".", "..")]
466
259
        except ftplib.error_perm, e:
467
 
            self._translate_perm_error(e, relpath, extra='error with list_dir')
 
260
            raise TransportError(orig_error=e)
468
261
 
469
262
    def iter_files_recursive(self):
470
263
        """See Transport.iter_files_recursive.
482
275
                yield relpath
483
276
 
484
277
    def stat(self, relpath):
485
 
        """Return the stat information for a file."""
486
 
        abspath = self._abspath(relpath)
 
278
        """Return the stat information for a file.
 
279
        """
487
280
        try:
488
 
            mutter("FTP stat: %s", abspath)
 
281
            mutter("FTP stat: %s" % self._abspath(relpath))
489
282
            f = self._get_FTP()
490
 
            return FtpStatResult(f, abspath)
 
283
            return FtpStatResult(f, self._abspath(relpath))
491
284
        except ftplib.error_perm, e:
492
 
            self._translate_perm_error(e, abspath, extra='error w/ stat')
 
285
            raise TransportError(orig_error=e)
493
286
 
494
287
    def lock_read(self, relpath):
495
288
        """Lock the given file for shared (read) access.
513
306
        return self.lock_read(relpath)
514
307
 
515
308
 
516
 
class FtpServer(Server):
517
 
    """Common code for SFTP server facilities."""
518
 
 
519
 
    def __init__(self):
520
 
        self._root = None
521
 
        self._ftp_server = None
522
 
        self._port = None
523
 
        self._async_thread = None
524
 
        # ftp server logs
525
 
        self.logs = []
526
 
 
527
 
    def get_url(self):
528
 
        """Calculate an ftp url to this server."""
529
 
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
530
 
 
531
 
#    def get_bogus_url(self):
532
 
#        """Return a URL which cannot be connected to."""
533
 
#        return 'ftp://127.0.0.1:1'
534
 
 
535
 
    def log(self, message):
536
 
        """This is used by medusa.ftp_server to log connections, etc."""
537
 
        self.logs.append(message)
538
 
 
539
 
    def setUp(self):
540
 
 
541
 
        if not _have_medusa:
542
 
            raise RuntimeError('Must have medusa to run the FtpServer')
543
 
 
544
 
        self._root = os.getcwdu()
545
 
        self._ftp_server = _ftp_server(
546
 
            authorizer=_test_authorizer(root=self._root),
547
 
            ip='localhost',
548
 
            port=0, # bind to a random port
549
 
            resolver=None,
550
 
            logger_object=self # Use FtpServer.log() for messages
551
 
            )
552
 
        self._port = self._ftp_server.getsockname()[1]
553
 
        # 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})
557
 
        self._async_thread.setDaemon(True)
558
 
        self._async_thread.start()
559
 
 
560
 
    def tearDown(self):
561
 
        """See bzrlib.transport.Server.tearDown."""
562
 
        # have asyncore release the channel
563
 
        self._ftp_server.del_channel()
564
 
        asyncore.close_all()
565
 
        self._async_thread.join()
566
 
 
567
 
 
568
 
_ftp_channel = None
569
 
_ftp_server = None
570
 
_test_authorizer = None
571
 
 
572
 
 
573
 
def _setup_medusa():
574
 
    global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
575
 
    try:
576
 
        import medusa
577
 
        import medusa.filesys
578
 
        import medusa.ftp_server
579
 
    except ImportError:
580
 
        return False
581
 
 
582
 
    _have_medusa = True
583
 
 
584
 
    class test_authorizer(object):
585
 
        """A custom Authorizer object for running the test suite.
586
 
 
587
 
        The reason we cannot use dummy_authorizer, is because it sets the
588
 
        channel to readonly, which we don't always want to do.
589
 
        """
590
 
 
591
 
        def __init__(self, root):
592
 
            self.root = root
593
 
 
594
 
        def authorize(self, channel, username, password):
595
 
            """Return (success, reply_string, filesystem)"""
596
 
            if not _have_medusa:
597
 
                return 0, 'No Medusa.', None
598
 
 
599
 
            channel.persona = -1, -1
600
 
            if username == 'anonymous':
601
 
                channel.read_only = 1
602
 
            else:
603
 
                channel.read_only = 0
604
 
 
605
 
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
606
 
 
607
 
 
608
 
    class ftp_channel(medusa.ftp_server.ftp_channel):
609
 
        """Customized ftp channel"""
610
 
 
611
 
        def log(self, message):
612
 
            """Redirect logging requests."""
613
 
            mutter('_ftp_channel: %s', message)
614
 
            
615
 
        def log_info(self, message, type='info'):
616
 
            """Redirect logging requests."""
617
 
            mutter('_ftp_channel %s: %s', type, message)
618
 
            
619
 
        def cmd_rnfr(self, line):
620
 
            """Prepare for renaming a file."""
621
 
            self._renaming = line[1]
622
 
            self.respond('350 Ready for RNTO')
623
 
            # TODO: jam 20060516 in testing, the ftp server seems to
624
 
            #       check that the file already exists, or it sends
625
 
            #       550 RNFR command failed
626
 
 
627
 
        def cmd_rnto(self, line):
628
 
            """Rename a file based on the target given.
629
 
 
630
 
            rnto must be called after calling rnfr.
631
 
            """
632
 
            if not self._renaming:
633
 
                self.respond('503 RNFR required first.')
634
 
            pfrom = self.filesystem.translate(self._renaming)
635
 
            self._renaming = None
636
 
            pto = self.filesystem.translate(line[1])
637
 
            try:
638
 
                os.rename(pfrom, pto)
639
 
            except (IOError, OSError), e:
640
 
                # TODO: jam 20060516 return custom responses based on
641
 
                #       why the command failed
642
 
                self.respond('550 RNTO failed: %s' % (e,))
643
 
            except:
644
 
                self.respond('550 RNTO failed')
645
 
                # For a test server, we will go ahead and just die
646
 
                raise
647
 
            else:
648
 
                self.respond('250 Rename successful.')
649
 
 
650
 
        def cmd_size(self, line):
651
 
            """Return the size of a file
652
 
 
653
 
            This is overloaded to help the test suite determine if the 
654
 
            target is a directory.
655
 
            """
656
 
            filename = line[1]
657
 
            if not self.filesystem.isfile(filename):
658
 
                if self.filesystem.isdir(filename):
659
 
                    self.respond('550 "%s" is a directory' % (filename,))
660
 
                else:
661
 
                    self.respond('550 "%s" is not a file' % (filename,))
662
 
            else:
663
 
                self.respond('213 %d' 
664
 
                    % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
665
 
 
666
 
        def cmd_mkd(self, line):
667
 
            """Create a directory.
668
 
 
669
 
            Overloaded because default implementation does not distinguish
670
 
            *why* it cannot make a directory.
671
 
            """
672
 
            if len (line) != 2:
673
 
                self.command_not_understood (string.join (line))
674
 
            else:
675
 
                path = line[1]
676
 
                try:
677
 
                    self.filesystem.mkdir (path)
678
 
                    self.respond ('257 MKD command successful.')
679
 
                except (IOError, OSError), e:
680
 
                    self.respond ('550 error creating directory: %s' % (e,))
681
 
                except:
682
 
                    self.respond ('550 error creating directory.')
683
 
 
684
 
 
685
 
    class ftp_server(medusa.ftp_server.ftp_server):
686
 
        """Customize the behavior of the Medusa ftp_server.
687
 
 
688
 
        There are a few warts on the ftp_server, based on how it expects
689
 
        to be used.
690
 
        """
691
 
        _renaming = None
692
 
        ftp_channel_class = ftp_channel
693
 
 
694
 
        def __init__(self, *args, **kwargs):
695
 
            mutter('Initializing _ftp_server: %r, %r', args, kwargs)
696
 
            medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
697
 
 
698
 
        def log(self, message):
699
 
            """Redirect logging requests."""
700
 
            mutter('_ftp_server: %s', message)
701
 
 
702
 
        def log_info(self, message, type='info'):
703
 
            """Override the asyncore.log_info so we don't stipple the screen."""
704
 
            mutter('_ftp_server %s: %s', type, message)
705
 
 
706
 
    _test_authorizer = test_authorizer
707
 
    _ftp_channel = ftp_channel
708
 
    _ftp_server = ftp_server
709
 
 
710
 
    return True
711
 
 
712
 
 
713
309
def get_test_permutations():
714
310
    """Return the permutations to be used in testing."""
715
 
    if not _setup_medusa():
716
 
        warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
717
 
        return []
718
 
    else:
719
 
        return [(FtpTransport, FtpServer)]
 
311
    warn("There are no FTP transport provider tests yet.")
 
312
    return []