~bzr-pqm/bzr/bzr.dev

1185.36.4 by Daniel Silverstone
Add FTP transport
1
# Copyright (C) 2005 Canonical Ltd
2
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
"""Implementation of Transport over ftp.
17
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
19
cargo-culting from the sftp transport and the http transport.
20
21
It provides the ftp:// and aftp:// protocols where ftp:// is passive ftp
22
and aftp:// is active ftp. Most people will want passive ftp for traversing
23
NAT and other firewalls, so it's best to use it unless you explicitly want
24
active, in which case aftp:// will be your friend.
25
"""
26
27
from cStringIO import StringIO
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
28
import asyncore
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
29
import errno
1185.36.4 by Daniel Silverstone
Add FTP transport
30
import ftplib
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
31
import os
32
import urllib
1185.36.4 by Daniel Silverstone
Add FTP transport
33
import urlparse
34
import stat
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
35
import threading
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
36
import time
1185.72.13 by Matthieu Moy
Make ftp put atomic
37
import random
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
38
from warnings import warn
39
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
40
from bzrlib.transport import (
41
    Transport,
42
    Server,
43
    split_url,
44
    )
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
45
import bzrlib.errors as errors
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
46
from bzrlib.trace import mutter, warning
1707.3.24 by John Arbash Meinel
Add prompt for password for ftp.
47
import bzrlib.ui
1551.3.11 by Aaron Bentley
Merge from Robert
48
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
49
_have_medusa = False
50
1551.3.11 by Aaron Bentley
Merge from Robert
51
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
52
class FtpPathError(errors.PathError):
53
    """FTP failed for path: %(path)s%(extra)s"""
54
55
1551.3.11 by Aaron Bentley
Merge from Robert
56
_FTP_cache = {}
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
57
def _find_FTP(hostname, port, username, password, is_active):
1551.3.11 by Aaron Bentley
Merge from Robert
58
    """Find an ftplib.FTP instance attached to this triplet."""
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
59
    key = (hostname, port, username, password, is_active)
60
    alt_key = (hostname, port, username, '********', is_active)
1551.3.11 by Aaron Bentley
Merge from Robert
61
    if key not in _FTP_cache:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
62
        mutter("Constructing FTP instance against %r" % (alt_key,))
1707.3.5 by John Arbash Meinel
Adding port to ftp connections, and not logging the ftp password
63
        conn = ftplib.FTP()
64
65
        conn.connect(host=hostname, port=port)
1707.3.24 by John Arbash Meinel
Add prompt for password for ftp.
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)
1707.3.5 by John Arbash Meinel
Adding port to ftp connections, and not logging the ftp password
70
        conn.login(user=username, passwd=password)
71
        conn.set_pasv(not is_active)
72
73
        _FTP_cache[key] = conn
74
1551.3.11 by Aaron Bentley
Merge from Robert
75
    return _FTP_cache[key]    
76
77
1185.36.4 by Daniel Silverstone
Add FTP transport
78
class FtpStatResult(object):
79
    def __init__(self, f, relpath):
80
        try:
81
            self.st_size = f.size(relpath)
82
            self.st_mode = stat.S_IFREG
83
        except ftplib.error_perm:
84
            pwd = f.pwd()
85
            try:
86
                f.cwd(relpath)
87
                self.st_mode = stat.S_IFDIR
88
            finally:
89
                f.cwd(pwd)
90
91
1185.72.15 by Matthieu Moy
better error messages on ftp failures
92
_number_of_retries = 2
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
93
_sleep_between_retries = 5
1185.72.12 by Matthieu Moy
made __number_of_retries global
94
1185.36.4 by Daniel Silverstone
Add FTP transport
95
class FtpTransport(Transport):
96
    """This is the transport agent for ftp:// access."""
97
98
    def __init__(self, base, _provided_instance=None):
99
        """Set the base path where files will be stored."""
100
        assert base.startswith('ftp://') or base.startswith('aftp://')
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
101
1185.36.4 by Daniel Silverstone
Add FTP transport
102
        self.is_active = base.startswith('aftp://')
103
        if self.is_active:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
104
            # urlparse won't handle aftp://
1185.36.4 by Daniel Silverstone
Add FTP transport
105
            base = base[1:]
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
106
        if not base.endswith('/'):
107
            base += '/'
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
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)
1185.36.4 by Daniel Silverstone
Add FTP transport
114
        self._FTP_instance = _provided_instance
115
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
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
1185.36.4 by Daniel Silverstone
Add FTP transport
127
    def _get_FTP(self):
128
        """Return the ftplib.FTP instance for this object."""
129
        if self._FTP_instance is not None:
130
            return self._FTP_instance
131
        
132
        try:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
133
            self._FTP_instance = _find_FTP(self._host, self._port,
134
                                           self._username, self._password,
1551.3.11 by Aaron Bentley
Merge from Robert
135
                                           self.is_active)
1185.36.4 by Daniel Silverstone
Add FTP transport
136
            return self._FTP_instance
137
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
138
            raise errors.TransportError(msg="Error setting up connection: %s"
1185.36.4 by Daniel Silverstone
Add FTP transport
139
                                    % str(e), orig_error=e)
140
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
141
    def _translate_perm_error(self, err, path, extra=None, unknown_exc=FtpPathError):
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
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
        """
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
150
        s = str(err).lower()
151
        if not extra:
152
            extra = str(err)
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
153
        else:
154
            extra += ': ' + str(err)
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
155
        if ('no such file' in s
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
156
            or 'could not open' in s
157
            or 'no such dir' in s
158
            ):
159
            raise errors.NoSuchFile(path, extra=extra)
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
160
        if ('file exists' in s):
1707.3.15 by John Arbash Meinel
Handle trying to list a file instead of a dir
161
            raise errors.FileExists(path, extra=extra)
162
        if ('not a directory' in s):
163
            raise errors.PathError(path, extra=extra)
164
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
165
        mutter('unable to understand error for path: %s: %s', path, err)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
166
167
        if unknown_exc:
168
            raise unknown_exc(path, extra=extra)
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
169
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
170
        #       something like TransportError, but this loses the traceback
1707.3.15 by John Arbash Meinel
Handle trying to list a file instead of a dir
171
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
172
        #       to handle. Consider doing something like that here.
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
173
        #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
1707.3.15 by John Arbash Meinel
Handle trying to list a file instead of a dir
174
        raise
1707.3.10 by John Arbash Meinel
Getting the append() tests to pass, and adding better error translation. These are probably dependent on the ftp test server, but for now it works.
175
1185.36.4 by Daniel Silverstone
Add FTP transport
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)
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
193
        relpath_parts = relpath.split('/')
1185.36.4 by Daniel Silverstone
Add FTP transport
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.
1707.3.3 by John Arbash Meinel
FtpTransport.abspath() returns '/' for an empty path
215
        return '/'.join(basepath) or '/'
1185.36.4 by Daniel Silverstone
Add FTP transport
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)
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
222
        return self._unparse_url(path)
1185.36.4 by Daniel Silverstone
Add FTP transport
223
224
    def has(self, relpath):
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
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)
1185.36.4 by Daniel Silverstone
Add FTP transport
232
        try:
233
            f = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
234
            mutter('FTP has check: %s => %s', relpath, abspath)
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
235
            s = f.size(abspath)
236
            mutter("FTP has: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
237
            return True
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
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)
1185.36.4 by Daniel Silverstone
Add FTP transport
243
            return False
244
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
245
    def get(self, relpath, decode=False, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
246
        """Get the file at the given relative path.
247
248
        :param relpath: The relative path to the file
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
249
        :param retries: Number of retries after temporary failures so far
250
                        for this operation.
1185.36.4 by Daniel Silverstone
Add FTP transport
251
252
        We're meant to return a file-like object which bzr will
253
        then read from. For now we do this via the magic of StringIO
254
        """
1540.3.6 by Martin Pool
[merge] update from bzr.dev
255
        # TODO: decode should be deprecated
1185.36.4 by Daniel Silverstone
Add FTP transport
256
        try:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
257
            mutter("FTP get: %s", self._abspath(relpath))
1185.36.4 by Daniel Silverstone
Add FTP transport
258
            f = self._get_FTP()
259
            ret = StringIO()
260
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
261
            ret.seek(0)
262
            return ret
263
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
264
            raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
265
        except ftplib.error_temp, e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
266
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
267
                raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
268
                                     % self.abspath(relpath),
269
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
270
            else:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
271
                warning("FTP temporary error: %s. Retrying.", str(e))
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
272
                self._FTP_instance = None
273
                return self.get(relpath, decode, retries+1)
1185.72.15 by Matthieu Moy
better error messages on ftp failures
274
        except EOFError, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
275
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
276
                raise errors.TransportError("FTP control connection closed during GET %s."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
277
                                     % self.abspath(relpath),
278
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
279
            else:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
280
                warning("FTP control connection closed. Trying to reopen.")
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
281
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
282
                self._FTP_instance = None
283
                return self.get(relpath, decode, retries+1)
1185.36.4 by Daniel Silverstone
Add FTP transport
284
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
285
    def put(self, relpath, fp, mode=None, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
286
        """Copy the file-like or string object into the location.
287
288
        :param relpath: Location to put the contents, relative to base.
1185.72.13 by Matthieu Moy
Make ftp put atomic
289
        :param fp:       File-like or string object.
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
290
        :param retries: Number of retries after temporary failures so far
291
                        for this operation.
292
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
293
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
1185.36.4 by Daniel Silverstone
Add FTP transport
294
        """
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
295
        abspath = self._abspath(relpath)
296
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
1185.72.13 by Matthieu Moy
Make ftp put atomic
297
                        os.getpid(), random.randint(0,0x7FFFFFFF))
1185.36.4 by Daniel Silverstone
Add FTP transport
298
        if not hasattr(fp, 'read'):
299
            fp = StringIO(fp)
300
        try:
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
301
            mutter("FTP put: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
302
            f = self._get_FTP()
1185.72.13 by Matthieu Moy
Make ftp put atomic
303
            try:
304
                f.storbinary('STOR '+tmp_abspath, fp)
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
305
                f.rename(tmp_abspath, abspath)
1185.72.13 by Matthieu Moy
Make ftp put atomic
306
            except (ftplib.error_temp,EOFError), e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
307
                warning("Failure during ftp PUT. Deleting temporary file.")
1185.72.13 by Matthieu Moy
Make ftp put atomic
308
                try:
309
                    f.delete(tmp_abspath)
310
                except:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
311
                    warning("Failed to delete temporary file on the"
312
                            " server.\nFile: %s", tmp_abspath)
1185.72.13 by Matthieu Moy
Make ftp put atomic
313
                    raise e
314
                raise
1185.36.4 by Daniel Silverstone
Add FTP transport
315
        except ftplib.error_perm, e:
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
316
            self._translate_perm_error(e, abspath, extra='could not store')
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
317
        except ftplib.error_temp, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
318
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
319
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
320
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
321
            else:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
322
                warning("FTP temporary error: %s. Retrying.", str(e))
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
323
                self._FTP_instance = None
324
                self.put(relpath, fp, mode, retries+1)
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
325
        except EOFError:
1185.72.10 by Matthieu Moy
One 1 -> _number_of_retries was missing
326
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
327
                raise errors.TransportError("FTP control connection closed during PUT %s."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
328
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
329
            else:
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
330
                warning("FTP control connection closed. Trying to reopen.")
331
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
332
                self._FTP_instance = None
333
                self.put(relpath, fp, mode, retries+1)
334
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
335
    def mkdir(self, relpath, mode=None):
1185.36.4 by Daniel Silverstone
Add FTP transport
336
        """Create a directory at the given path."""
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
337
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
338
        try:
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
339
            mutter("FTP mkd: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
340
            f = self._get_FTP()
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
341
            f.mkd(abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
342
        except ftplib.error_perm, e:
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
343
            self._translate_perm_error(e, abspath,
344
                unknown_exc=errors.FileExists)
1185.36.4 by Daniel Silverstone
Add FTP transport
345
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
346
    def rmdir(self, rel_path):
347
        """Delete the directory at rel_path"""
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
348
        abspath = self._abspath(rel_path)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
349
        try:
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
350
            mutter("FTP rmd: %s", abspath)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
351
            f = self._get_FTP()
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
352
            f.rmd(abspath)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
353
        except ftplib.error_perm, e:
1707.3.21 by John Arbash Meinel
rmdir can raise PathError when it cannot delete a directory.
354
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
355
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
356
    def append(self, relpath, f, mode=None):
1185.36.4 by Daniel Silverstone
Add FTP transport
357
        """Append the text in the file-like object into the final
358
        location.
359
        """
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
360
        abspath = self._abspath(relpath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
361
        if self.has(relpath):
362
            ftp = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
363
            result = ftp.size(abspath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
364
        else:
365
            result = 0
366
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
367
        mutter("FTP appe to %s", abspath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
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()
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
387
            if mode:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
388
                self._setmode(relpath, mode)
389
            ftp.getresp()
390
        except ftplib.error_perm, e:
1707.3.26 by John Arbash Meinel
Added a default exception for append
391
            self._translate_perm_error(e, abspath, extra='error appending',
392
                unknown_exc=errors.NoSuchFile)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
393
        except ftplib.error_temp, e:
394
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
395
                raise errors.TransportError("FTP temporary error during APPEND %s." \
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
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))
1185.36.4 by Daniel Silverstone
Add FTP transport
418
1707.3.11 by John Arbash Meinel
fixing more tests.
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()'
1185.36.4 by Daniel Silverstone
Add FTP transport
423
424
    def move(self, rel_from, rel_to):
425
        """Move the item at rel_from to the location at rel_to"""
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
426
        abs_from = self._abspath(rel_from)
427
        abs_to = self._abspath(rel_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
428
        try:
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
429
            mutter("FTP mv: %s => %s", abs_from, abs_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
430
            f = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
431
            f.rename(abs_from, abs_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
432
        except ftplib.error_perm, e:
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
433
            self._translate_perm_error(e, abs_from,
434
                extra='unable to rename to %r' % (rel_to,), 
435
                unknown_exc=errors.PathError)
1185.36.4 by Daniel Silverstone
Add FTP transport
436
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
437
    rename = move
438
1185.36.4 by Daniel Silverstone
Add FTP transport
439
    def delete(self, relpath):
440
        """Delete the item at relpath"""
1707.3.11 by John Arbash Meinel
fixing more tests.
441
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
442
        try:
1707.3.11 by John Arbash Meinel
fixing more tests.
443
            mutter("FTP rm: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
444
            f = self._get_FTP()
1707.3.11 by John Arbash Meinel
fixing more tests.
445
            f.delete(abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
446
        except ftplib.error_perm, e:
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
447
            self._translate_perm_error(e, abspath, 'error deleting',
448
                unknown_exc=errors.NoSuchFile)
1185.36.4 by Daniel Silverstone
Add FTP transport
449
450
    def listable(self):
451
        """See Transport.listable."""
452
        return True
453
454
    def list_dir(self, relpath):
455
        """See Transport.list_dir."""
456
        try:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
457
            mutter("FTP nlst: %s", self._abspath(relpath))
1185.36.4 by Daniel Silverstone
Add FTP transport
458
            f = self._get_FTP()
459
            basepath = self._abspath(relpath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
460
            paths = f.nlst(basepath)
461
            # If FTP.nlst returns paths prefixed by relpath, strip 'em
1707.3.12 by John Arbash Meinel
Fixing list_dir when there is nothing in the directory.
462
            if paths and paths[0].startswith(basepath):
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
463
                paths = [path[len(basepath)+1:] for path in paths]
1185.36.4 by Daniel Silverstone
Add FTP transport
464
            # Remove . and .. if present, and return
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
465
            return [path for path in paths if path not in (".", "..")]
1185.36.4 by Daniel Silverstone
Add FTP transport
466
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
467
            self._translate_perm_error(e, relpath, extra='error with list_dir')
1185.36.4 by Daniel Silverstone
Add FTP transport
468
469
    def iter_files_recursive(self):
470
        """See Transport.iter_files_recursive.
471
472
        This is cargo-culted from the SFTP transport"""
473
        mutter("FTP iter_files_recursive")
474
        queue = list(self.list_dir("."))
475
        while queue:
476
            relpath = urllib.quote(queue.pop(0))
477
            st = self.stat(relpath)
478
            if stat.S_ISDIR(st.st_mode):
479
                for i, basename in enumerate(self.list_dir(relpath)):
480
                    queue.insert(i, relpath+"/"+basename)
481
            else:
482
                yield relpath
483
484
    def stat(self, relpath):
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
485
        """Return the stat information for a file."""
486
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
487
        try:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
488
            mutter("FTP stat: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
489
            f = self._get_FTP()
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
490
            return FtpStatResult(f, abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
491
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
492
            self._translate_perm_error(e, abspath, extra='error w/ stat')
1185.36.4 by Daniel Silverstone
Add FTP transport
493
494
    def lock_read(self, relpath):
495
        """Lock the given file for shared (read) access.
496
        :return: A lock object, which should be passed to Transport.unlock()
497
        """
498
        # The old RemoteBranch ignore lock for reading, so we will
499
        # continue that tradition and return a bogus lock object.
500
        class BogusLock(object):
501
            def __init__(self, path):
502
                self.path = path
503
            def unlock(self):
504
                pass
505
        return BogusLock(relpath)
506
507
    def lock_write(self, relpath):
508
        """Lock the given file for exclusive (write) access.
509
        WARNING: many transports do not support this, so trying avoid using it
510
511
        :return: A lock object, which should be passed to Transport.unlock()
512
        """
513
        return self.lock_read(relpath)
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
514
515
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
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
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
541
        if not _have_medusa:
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
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):
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
609
        """Customized ftp channel"""
610
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
611
        def log(self, message):
612
            """Redirect logging requests."""
613
            mutter('_ftp_channel: %s', message)
614
            
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
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)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
635
            self._renaming = None
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
636
            pto = self.filesystem.translate(line[1])
637
            try:
638
                os.rename(pfrom, pto)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
639
            except (IOError, OSError), e:
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
640
                # TODO: jam 20060516 return custom responses based on
641
                #       why the command failed
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
642
                self.respond('550 RNTO failed: %s' % (e,))
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
643
            except:
644
                self.respond('550 RNTO failed')
645
                # For a test server, we will go ahead and just die
646
                raise
1707.3.20 by John Arbash Meinel
Found the problem, was sending 2 responses
647
            else:
648
                self.respond('250 Rename successful.')
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
649
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
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' 
1707.3.17 by John Arbash Meinel
Fix errors in cmd_size
664
                    % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
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
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
684
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
685
    class ftp_server(medusa.ftp_server.ftp_server):
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
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
        """
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
691
        _renaming = None
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
692
        ftp_channel_class = ftp_channel
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
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)
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
697
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
698
        def log(self, message):
699
            """Redirect logging requests."""
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
700
            mutter('_ftp_server: %s', message)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
701
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
702
        def log_info(self, message, type='info'):
703
            """Override the asyncore.log_info so we don't stipple the screen."""
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
704
            mutter('_ftp_server %s: %s', type, message)
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
705
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
706
    _test_authorizer = test_authorizer
707
    _ftp_channel = ftp_channel
708
    _ftp_server = ftp_server
709
710
    return True
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
711
712
1530.1.11 by Robert Collins
Push the transport permutations list into each transport module allowing for automatic testing of new modules that are registered as transports.
713
def get_test_permutations():
714
    """Return the permutations to be used in testing."""
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
715
    if not _setup_medusa():
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
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)]