~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Vincent Ladeuil
  • Date: 2008-08-26 08:25:27 UTC
  • mto: (3668.1.1 trunk) (3703.1.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 3669.
  • Revision ID: v.ladeuil+lp@free.fr-20080826082527-109yyxzc0u24oeel
Fix all calls to tempfile.mkdtemp to osutils.mkdtemp.

* bzrlib/transform.py:
(TransformPreview.__init__): Use osutils.mkdtemp instead of
tempfile.mkdtemp.

* bzrlib/tests/test_whitebox.py:
(MoreTests.test_relpath): Use osutils.mkdtemp instead of
tempfile.mkdtemp.

* bzrlib/tests/test_setup.py:
(TestSetup.test_build_and_install): Use osutils.mkdtemp instead of
tempfile.mkdtemp.

* bzrlib/tests/test_bundle.py:
(BundleTester.get_checkout): Use osutils.mkdtemp instead of
tempfile.mkdtemp.

* bzrlib/tests/blackbox/test_outside_wt.py:
(TestOutsideWT.test_cwd_log,
TestOutsideWT.test_diff_outside_tree): Use osutils.mkdtemp instead
of tempfile.mkdtemp.

* bzrlib/smart/repository.py:
(SmartServerRepositoryTarball._copy_to_tempdir): Use
osutils.mkdtemp instead of tempfile.mkdtemp.
(SmartServerRepositoryTarball._tarfile_response): Line too long.

* bzrlib/remote.py:
(RemoteRepository._copy_repository_tarball): Use osutils.mkdtemp
instead of tempfile.mkdtemp.

* bzrlib/osutils.py:
(_mac_mkdtemp): Add docstring.

* bzrlib/mail_client.py:
(ExternalMailClient.compose): Use osutils.mkdtemp instead of
tempfile.mkdtemp.

* bzrlib/diff.py:
(DiffFromTool.__init__): Use osutils.mkdtemp instead of
tempfile.mkdtemp.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25
25
"""
26
26
 
27
27
from cStringIO import StringIO
28
 
import asyncore
29
28
import errno
30
29
import ftplib
 
30
import getpass
31
31
import os
32
 
import urllib
 
32
import os.path
33
33
import urlparse
 
34
import socket
34
35
import stat
35
 
import threading
36
36
import time
37
37
import random
38
38
from warnings import warn
39
39
 
 
40
from bzrlib import (
 
41
    config,
 
42
    errors,
 
43
    osutils,
 
44
    urlutils,
 
45
    )
 
46
from bzrlib.trace import mutter, warning
40
47
from bzrlib.transport import (
41
 
    Transport,
 
48
    AppendBasedFileStream,
 
49
    ConnectedTransport,
 
50
    _file_streams,
 
51
    register_urlparse_netloc_protocol,
42
52
    Server,
43
 
    split_url,
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
 
_have_medusa = False
 
57
 
 
58
register_urlparse_netloc_protocol('aftp')
50
59
 
51
60
 
52
61
class FtpPathError(errors.PathError):
53
62
    """FTP failed for path: %(path)s%(extra)s"""
54
63
 
55
64
 
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
65
class FtpStatResult(object):
79
66
    def __init__(self, f, relpath):
80
67
        try:
92
79
_number_of_retries = 2
93
80
_sleep_between_retries = 5
94
81
 
95
 
class FtpTransport(Transport):
 
82
# FIXME: there are inconsistencies in the way temporary errors are
 
83
# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
 
84
# be taken to analyze the implications for write operations (read operations
 
85
# are safe to retry). Overall even some read operations are never
 
86
# retried. --vila 20070720 (Bug #127164)
 
87
class FtpTransport(ConnectedTransport):
96
88
    """This is the transport agent for ftp:// access."""
97
89
 
98
 
    def __init__(self, base, _provided_instance=None):
 
90
    def __init__(self, base, _from_transport=None):
99
91
        """Set the base path where files will be stored."""
100
 
        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
        if not (base.startswith('ftp://') or base.startswith('aftp://')):
 
93
            raise ValueError(base)
 
94
        super(FtpTransport, self).__init__(base,
 
95
                                           _from_transport=_from_transport)
 
96
        self._unqualified_scheme = 'ftp'
 
97
        if self._scheme == 'aftp':
 
98
            self.is_active = True
 
99
        else:
 
100
            self.is_active = False
126
101
 
127
102
    def _get_FTP(self):
128
103
        """Return the ftplib.FTP instance for this object."""
129
 
        if self._FTP_instance is not None:
130
 
            return self._FTP_instance
131
 
        
 
104
        # Ensures that a connection is established
 
105
        connection = self._get_connection()
 
106
        if connection is None:
 
107
            # First connection ever
 
108
            connection, credentials = self._create_connection()
 
109
            self._set_connection(connection, credentials)
 
110
        return connection
 
111
 
 
112
    def _create_connection(self, credentials=None):
 
113
        """Create a new connection with the provided credentials.
 
114
 
 
115
        :param credentials: The credentials needed to establish the connection.
 
116
 
 
117
        :return: The created connection and its associated credentials.
 
118
 
 
119
        The credentials are only the password as it may have been entered
 
120
        interactively by the user and may be different from the one provided
 
121
        in base url at transport creation time.
 
122
        """
 
123
        if credentials is None:
 
124
            user, password = self._user, self._password
 
125
        else:
 
126
            user, password = credentials
 
127
 
 
128
        auth = config.AuthenticationConfig()
 
129
        if user is None:
 
130
            user = auth.get_user('ftp', self._host, port=self._port)
 
131
            if user is None:
 
132
                # Default to local user
 
133
                user = getpass.getuser()
 
134
 
 
135
        mutter("Constructing FTP instance against %r" %
 
136
               ((self._host, self._port, user, '********',
 
137
                self.is_active),))
132
138
        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
 
139
            connection = ftplib.FTP()
 
140
            connection.connect(host=self._host, port=self._port)
 
141
            if user and user != 'anonymous' and \
 
142
                    password is None: # '' is a valid password
 
143
                password = auth.get_password('ftp', self._host, user,
 
144
                                             port=self._port)
 
145
            connection.login(user=user, passwd=password)
 
146
            connection.set_pasv(not self.is_active)
 
147
        except socket.error, e:
 
148
            raise errors.SocketConnectionError(self._host, self._port,
 
149
                                               msg='Unable to connect to',
 
150
                                               orig_error= e)
137
151
        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):
 
152
            raise errors.TransportError(msg="Error setting up connection:"
 
153
                                        " %s" % str(e), orig_error=e)
 
154
        return connection, (user, password)
 
155
 
 
156
    def _reconnect(self):
 
157
        """Create a new connection with the previously used credentials"""
 
158
        credentials = self._get_credentials()
 
159
        connection, credentials = self._create_connection(credentials)
 
160
        self._set_connection(connection, credentials)
 
161
 
 
162
    def _translate_perm_error(self, err, path, extra=None,
 
163
                              unknown_exc=FtpPathError):
142
164
        """Try to translate an ftplib.error_perm exception.
143
165
 
144
166
        :param err: The error to translate into a bzr error
155
177
        if ('no such file' in s
156
178
            or 'could not open' in s
157
179
            or 'no such dir' in s
 
180
            or 'could not create file' in s # vsftpd
 
181
            or 'file doesn\'t exist' in s
 
182
            or 'rnfr command failed.' in s # vsftpd RNFR reply if file not found
 
183
            or 'file/directory not found' in s # filezilla server
 
184
            # Microsoft FTP-Service RNFR reply if file not found
 
185
            or (s.startswith('550 ') and 'unable to rename to' in extra)
158
186
            ):
159
187
            raise errors.NoSuchFile(path, extra=extra)
160
188
        if ('file exists' in s):
173
201
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
174
202
        raise
175
203
 
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)
 
204
    def _remote_path(self, relpath):
 
205
        # XXX: It seems that ftplib does not handle Unicode paths
 
206
        # at the same time, medusa won't handle utf8 paths So if
 
207
        # we .encode(utf8) here (see ConnectedTransport
 
208
        # implementation), then we get a Server failure.  while
 
209
        # if we use str(), we get a UnicodeError, and the test
 
210
        # suite just skips testing UnicodePaths.
 
211
        relative = str(urlutils.unescape(relpath))
 
212
        remote_path = self._combine_paths(self._path, relative)
 
213
        return remote_path
223
214
 
224
215
    def has(self, relpath):
225
216
        """Does the target location exist?"""
228
219
        # XXX: I assume we're never asked has(dirname) and thus I use
229
220
        # the FTP size command and assume that if it doesn't raise,
230
221
        # all is good.
231
 
        abspath = self._abspath(relpath)
 
222
        abspath = self._remote_path(relpath)
232
223
        try:
233
224
            f = self._get_FTP()
234
225
            mutter('FTP has check: %s => %s', relpath, abspath)
254
245
        """
255
246
        # TODO: decode should be deprecated
256
247
        try:
257
 
            mutter("FTP get: %s", self._abspath(relpath))
 
248
            mutter("FTP get: %s", self._remote_path(relpath))
258
249
            f = self._get_FTP()
259
250
            ret = StringIO()
260
 
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
 
251
            f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
261
252
            ret.seek(0)
262
253
            return ret
263
254
        except ftplib.error_perm, e:
269
260
                                     orig_error=e)
270
261
            else:
271
262
                warning("FTP temporary error: %s. Retrying.", str(e))
272
 
                self._FTP_instance = None
 
263
                self._reconnect()
273
264
                return self.get(relpath, decode, retries+1)
274
265
        except EOFError, e:
275
266
            if retries > _number_of_retries:
279
270
            else:
280
271
                warning("FTP control connection closed. Trying to reopen.")
281
272
                time.sleep(_sleep_between_retries)
282
 
                self._FTP_instance = None
 
273
                self._reconnect()
283
274
                return self.get(relpath, decode, retries+1)
284
275
 
285
 
    def put(self, relpath, fp, mode=None, retries=0):
 
276
    def put_file(self, relpath, fp, mode=None, retries=0):
286
277
        """Copy the file-like or string object into the location.
287
278
 
288
279
        :param relpath: Location to put the contents, relative to base.
290
281
        :param retries: Number of retries after temporary failures so far
291
282
                        for this operation.
292
283
 
293
 
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
 
284
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
 
285
        ftplib does not
294
286
        """
295
 
        abspath = self._abspath(relpath)
 
287
        abspath = self._remote_path(relpath)
296
288
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
297
289
                        os.getpid(), random.randint(0,0x7FFFFFFF))
298
 
        if not hasattr(fp, 'read'):
299
 
            fp = StringIO(fp)
 
290
        bytes = None
 
291
        if getattr(fp, 'read', None) is None:
 
292
            # hand in a string IO
 
293
            bytes = fp
 
294
            fp = StringIO(bytes)
 
295
        else:
 
296
            # capture the byte count; .read() may be read only so
 
297
            # decorate it.
 
298
            class byte_counter(object):
 
299
                def __init__(self, fp):
 
300
                    self.fp = fp
 
301
                    self.counted_bytes = 0
 
302
                def read(self, count):
 
303
                    result = self.fp.read(count)
 
304
                    self.counted_bytes += len(result)
 
305
                    return result
 
306
            fp = byte_counter(fp)
300
307
        try:
301
308
            mutter("FTP put: %s", abspath)
302
309
            f = self._get_FTP()
303
310
            try:
304
311
                f.storbinary('STOR '+tmp_abspath, fp)
305
 
                f.rename(tmp_abspath, abspath)
 
312
                self._rename_and_overwrite(tmp_abspath, abspath, f)
 
313
                self._setmode(relpath, mode)
 
314
                if bytes is not None:
 
315
                    return len(bytes)
 
316
                else:
 
317
                    return fp.counted_bytes
306
318
            except (ftplib.error_temp,EOFError), e:
307
319
                warning("Failure during ftp PUT. Deleting temporary file.")
308
320
                try:
313
325
                    raise e
314
326
                raise
315
327
        except ftplib.error_perm, e:
316
 
            self._translate_perm_error(e, abspath, extra='could not store')
 
328
            self._translate_perm_error(e, abspath, extra='could not store',
 
329
                                       unknown_exc=errors.NoSuchFile)
317
330
        except ftplib.error_temp, e:
318
331
            if retries > _number_of_retries:
319
332
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
320
333
                                     % self.abspath(relpath), orig_error=e)
321
334
            else:
322
335
                warning("FTP temporary error: %s. Retrying.", str(e))
323
 
                self._FTP_instance = None
324
 
                self.put(relpath, fp, mode, retries+1)
 
336
                self._reconnect()
 
337
                self.put_file(relpath, fp, mode, retries+1)
325
338
        except EOFError:
326
339
            if retries > _number_of_retries:
327
340
                raise errors.TransportError("FTP control connection closed during PUT %s."
329
342
            else:
330
343
                warning("FTP control connection closed. Trying to reopen.")
331
344
                time.sleep(_sleep_between_retries)
332
 
                self._FTP_instance = None
333
 
                self.put(relpath, fp, mode, retries+1)
 
345
                self._reconnect()
 
346
                self.put_file(relpath, fp, mode, retries+1)
334
347
 
335
348
    def mkdir(self, relpath, mode=None):
336
349
        """Create a directory at the given path."""
337
 
        abspath = self._abspath(relpath)
 
350
        abspath = self._remote_path(relpath)
338
351
        try:
339
352
            mutter("FTP mkd: %s", abspath)
340
353
            f = self._get_FTP()
341
354
            f.mkd(abspath)
 
355
            self._setmode(relpath, mode)
342
356
        except ftplib.error_perm, e:
343
357
            self._translate_perm_error(e, abspath,
344
358
                unknown_exc=errors.FileExists)
345
359
 
 
360
    def open_write_stream(self, relpath, mode=None):
 
361
        """See Transport.open_write_stream."""
 
362
        self.put_bytes(relpath, "", mode)
 
363
        result = AppendBasedFileStream(self, relpath)
 
364
        _file_streams[self.abspath(relpath)] = result
 
365
        return result
 
366
 
 
367
    def recommended_page_size(self):
 
368
        """See Transport.recommended_page_size().
 
369
 
 
370
        For FTP we suggest a large page size to reduce the overhead
 
371
        introduced by latency.
 
372
        """
 
373
        return 64 * 1024
 
374
 
346
375
    def rmdir(self, rel_path):
347
376
        """Delete the directory at rel_path"""
348
 
        abspath = self._abspath(rel_path)
 
377
        abspath = self._remote_path(rel_path)
349
378
        try:
350
379
            mutter("FTP rmd: %s", abspath)
351
380
            f = self._get_FTP()
353
382
        except ftplib.error_perm, e:
354
383
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
355
384
 
356
 
    def append(self, relpath, f, mode=None):
 
385
    def append_file(self, relpath, f, mode=None):
357
386
        """Append the text in the file-like object into the final
358
387
        location.
359
388
        """
360
 
        abspath = self._abspath(relpath)
 
389
        abspath = self._remote_path(relpath)
361
390
        if self.has(relpath):
362
391
            ftp = self._get_FTP()
363
392
            result = ftp.size(abspath)
376
405
        number of retries is exceeded.
377
406
        """
378
407
        try:
379
 
            abspath = self._abspath(relpath)
 
408
            abspath = self._remote_path(relpath)
380
409
            mutter("FTP appe (try %d) to %s", retries, abspath)
381
410
            ftp = self._get_FTP()
382
411
            ftp.voidcmd("TYPE I")
384
413
            conn = ftp.transfercmd(cmd)
385
414
            conn.sendall(text)
386
415
            conn.close()
387
 
            if mode:
388
 
                self._setmode(relpath, mode)
 
416
            self._setmode(relpath, mode)
389
417
            ftp.getresp()
390
418
        except ftplib.error_perm, e:
391
419
            self._translate_perm_error(e, abspath, extra='error appending',
396
424
                        "Aborting." % abspath, orig_error=e)
397
425
            else:
398
426
                warning("FTP temporary error: %s. Retrying.", str(e))
399
 
                self._FTP_instance = None
 
427
                self._reconnect()
400
428
                self._try_append(relpath, text, mode, retries+1)
401
429
 
402
430
    def _setmode(self, relpath, mode):
405
433
        Only set permissions if the FTP server supports the 'SITE CHMOD'
406
434
        extension.
407
435
        """
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))
 
436
        if mode:
 
437
            try:
 
438
                mutter("FTP site chmod: setting permissions to %s on %s",
 
439
                    str(mode), self._remote_path(relpath))
 
440
                ftp = self._get_FTP()
 
441
                cmd = "SITE CHMOD %s %s" % (oct(mode),
 
442
                                            self._remote_path(relpath))
 
443
                ftp.sendcmd(cmd)
 
444
            except ftplib.error_perm, e:
 
445
                # Command probably not available on this server
 
446
                warning("FTP Could not set permissions to %s on %s. %s",
 
447
                        str(mode), self._remote_path(relpath), str(e))
418
448
 
419
449
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
420
450
    #       to copy something to another machine. And you may be able
421
451
    #       to give it its own address as the 'to' location.
422
452
    #       So implement a fancier 'copy()'
423
453
 
 
454
    def rename(self, rel_from, rel_to):
 
455
        abs_from = self._remote_path(rel_from)
 
456
        abs_to = self._remote_path(rel_to)
 
457
        mutter("FTP rename: %s => %s", abs_from, abs_to)
 
458
        f = self._get_FTP()
 
459
        return self._rename(abs_from, abs_to, f)
 
460
 
 
461
    def _rename(self, abs_from, abs_to, f):
 
462
        try:
 
463
            f.rename(abs_from, abs_to)
 
464
        except ftplib.error_perm, e:
 
465
            self._translate_perm_error(e, abs_from,
 
466
                ': unable to rename to %r' % (abs_to))
 
467
 
424
468
    def move(self, rel_from, rel_to):
425
469
        """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)
 
470
        abs_from = self._remote_path(rel_from)
 
471
        abs_to = self._remote_path(rel_to)
428
472
        try:
429
473
            mutter("FTP mv: %s => %s", abs_from, abs_to)
430
474
            f = self._get_FTP()
431
 
            f.rename(abs_from, abs_to)
 
475
            self._rename_and_overwrite(abs_from, abs_to, f)
432
476
        except ftplib.error_perm, e:
433
477
            self._translate_perm_error(e, abs_from,
434
478
                extra='unable to rename to %r' % (rel_to,), 
435
479
                unknown_exc=errors.PathError)
436
480
 
437
 
    rename = move
 
481
    def _rename_and_overwrite(self, abs_from, abs_to, f):
 
482
        """Do a fancy rename on the remote server.
 
483
 
 
484
        Using the implementation provided by osutils.
 
485
        """
 
486
        osutils.fancy_rename(abs_from, abs_to,
 
487
            rename_func=lambda p1, p2: self._rename(p1, p2, f),
 
488
            unlink_func=lambda p: self._delete(p, f))
438
489
 
439
490
    def delete(self, relpath):
440
491
        """Delete the item at relpath"""
441
 
        abspath = self._abspath(relpath)
 
492
        abspath = self._remote_path(relpath)
 
493
        f = self._get_FTP()
 
494
        self._delete(abspath, f)
 
495
 
 
496
    def _delete(self, abspath, f):
442
497
        try:
443
498
            mutter("FTP rm: %s", abspath)
444
 
            f = self._get_FTP()
445
499
            f.delete(abspath)
446
500
        except ftplib.error_perm, e:
447
501
            self._translate_perm_error(e, abspath, 'error deleting',
448
502
                unknown_exc=errors.NoSuchFile)
449
503
 
 
504
    def external_url(self):
 
505
        """See bzrlib.transport.Transport.external_url."""
 
506
        # FTP URL's are externally usable.
 
507
        return self.base
 
508
 
450
509
    def listable(self):
451
510
        """See Transport.listable."""
452
511
        return True
453
512
 
454
513
    def list_dir(self, relpath):
455
514
        """See Transport.list_dir."""
 
515
        basepath = self._remote_path(relpath)
 
516
        mutter("FTP nlst: %s", basepath)
 
517
        f = self._get_FTP()
456
518
        try:
457
 
            mutter("FTP nlst: %s", self._abspath(relpath))
458
 
            f = self._get_FTP()
459
 
            basepath = self._abspath(relpath)
460
519
            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
520
        except ftplib.error_perm, e:
467
521
            self._translate_perm_error(e, relpath, extra='error with list_dir')
 
522
        except ftplib.error_temp, e:
 
523
            # xs4all's ftp server raises a 450 temp error when listing an empty
 
524
            # directory. Check for that and just return an empty list in that
 
525
            # case. See bug #215522
 
526
            if str(e).lower().startswith('450 no files found'):
 
527
                mutter('FTP Server returned "%s" for nlst.'
 
528
                       ' Assuming it means empty directory',
 
529
                       str(e))
 
530
                return []
 
531
            raise
 
532
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
 
533
        if paths and paths[0].startswith(basepath):
 
534
            entries = [path[len(basepath)+1:] for path in paths]
 
535
        else:
 
536
            entries = paths
 
537
        # Remove . and .. if present
 
538
        return [urlutils.escape(entry) for entry in entries
 
539
                if entry not in ('.', '..')]
468
540
 
469
541
    def iter_files_recursive(self):
470
542
        """See Transport.iter_files_recursive.
473
545
        mutter("FTP iter_files_recursive")
474
546
        queue = list(self.list_dir("."))
475
547
        while queue:
476
 
            relpath = urllib.quote(queue.pop(0))
 
548
            relpath = queue.pop(0)
477
549
            st = self.stat(relpath)
478
550
            if stat.S_ISDIR(st.st_mode):
479
551
                for i, basename in enumerate(self.list_dir(relpath)):
483
555
 
484
556
    def stat(self, relpath):
485
557
        """Return the stat information for a file."""
486
 
        abspath = self._abspath(relpath)
 
558
        abspath = self._remote_path(relpath)
487
559
        try:
488
560
            mutter("FTP stat: %s", abspath)
489
561
            f = self._get_FTP()
513
585
        return self.lock_read(relpath)
514
586
 
515
587
 
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(''.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
588
def get_test_permutations():
714
589
    """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 []
 
590
    from bzrlib import tests
 
591
    if tests.FTPServerFeature.available():
 
592
        from bzrlib.tests import ftp_server
 
593
        return [(FtpTransport, ftp_server.FTPServer)]
718
594
    else:
719
 
        return [(FtpTransport, FtpServer)]
 
595
        # Dummy server to have the test suite report the number of tests
 
596
        # needing that feature. We raise UnavailableFeature from methods before
 
597
        # the test server is being used. Doing so in the setUp method has bad
 
598
        # side-effects (tearDown is never called).
 
599
        class UnavailableFTPServer(object):
 
600
 
 
601
            def setUp(self):
 
602
                pass
 
603
 
 
604
            def tearDown(self):
 
605
                pass
 
606
 
 
607
            def get_url(self):
 
608
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
609
 
 
610
            def get_bogus_url(self):
 
611
                raise tests.UnavailableFeature(tests.FTPServerFeature)
 
612
 
 
613
        return [(FtpTransport, UnavailableFTPServer)]