/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
2338.1.1 by John Arbash Meinel
(Arnaud Fontaine) Compatibility with vsftpd error messages.
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
2
#
1185.36.4 by Daniel Silverstone
Add FTP transport
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.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
7
#
1185.36.4 by Daniel Silverstone
Add FTP transport
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.
1887.1.1 by Adeodato Simó
Do not separate paragraphs in the copyright statement with blank lines,
12
#
1185.36.4 by Daniel Silverstone
Add FTP transport
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
1948.1.7 by John Arbash Meinel
Fix FtpTransport so that it raises UnicodeError and skips the unicode path tests
40
from bzrlib import (
41
    errors,
42
    urlutils,
43
    )
44
from bzrlib.trace import mutter, warning
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
45
from bzrlib.transport import (
46
    Server,
47
    split_url,
1948.1.7 by John Arbash Meinel
Fix FtpTransport so that it raises UnicodeError and skips the unicode path tests
48
    Transport,
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
49
    )
1707.3.24 by John Arbash Meinel
Add prompt for password for ftp.
50
import bzrlib.ui
1551.3.11 by Aaron Bentley
Merge from Robert
51
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
52
_have_medusa = False
53
1551.3.11 by Aaron Bentley
Merge from Robert
54
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
55
class FtpPathError(errors.PathError):
56
    """FTP failed for path: %(path)s%(extra)s"""
57
58
1551.3.11 by Aaron Bentley
Merge from Robert
59
_FTP_cache = {}
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
60
def _find_FTP(hostname, port, username, password, is_active):
1551.3.11 by Aaron Bentley
Merge from Robert
61
    """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.
62
    key = (hostname, port, username, password, is_active)
63
    alt_key = (hostname, port, username, '********', is_active)
1551.3.11 by Aaron Bentley
Merge from Robert
64
    if key not in _FTP_cache:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
65
        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
66
        conn = ftplib.FTP()
67
68
        conn.connect(host=hostname, port=port)
1707.3.24 by John Arbash Meinel
Add prompt for password for ftp.
69
        if username and username != 'anonymous' and not password:
70
            password = bzrlib.ui.ui_factory.get_password(
71
                prompt='FTP %(user)s@%(host)s password',
72
                user=username, host=hostname)
1707.3.5 by John Arbash Meinel
Adding port to ftp connections, and not logging the ftp password
73
        conn.login(user=username, passwd=password)
74
        conn.set_pasv(not is_active)
75
76
        _FTP_cache[key] = conn
77
1551.3.11 by Aaron Bentley
Merge from Robert
78
    return _FTP_cache[key]    
79
80
1185.36.4 by Daniel Silverstone
Add FTP transport
81
class FtpStatResult(object):
82
    def __init__(self, f, relpath):
83
        try:
84
            self.st_size = f.size(relpath)
85
            self.st_mode = stat.S_IFREG
86
        except ftplib.error_perm:
87
            pwd = f.pwd()
88
            try:
89
                f.cwd(relpath)
90
                self.st_mode = stat.S_IFDIR
91
            finally:
92
                f.cwd(pwd)
93
94
1185.72.15 by Matthieu Moy
better error messages on ftp failures
95
_number_of_retries = 2
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
96
_sleep_between_retries = 5
1185.72.12 by Matthieu Moy
made __number_of_retries global
97
1185.36.4 by Daniel Silverstone
Add FTP transport
98
class FtpTransport(Transport):
99
    """This is the transport agent for ftp:// access."""
100
101
    def __init__(self, base, _provided_instance=None):
102
        """Set the base path where files will be stored."""
103
        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.
104
1185.36.4 by Daniel Silverstone
Add FTP transport
105
        self.is_active = base.startswith('aftp://')
106
        if self.is_active:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
107
            # urlparse won't handle aftp://
1185.36.4 by Daniel Silverstone
Add FTP transport
108
            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.
109
        if not base.endswith('/'):
110
            base += '/'
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
111
        (self._proto, self._username,
112
            self._password, self._host,
113
            self._port, self._path) = split_url(base)
114
        base = self._unparse_url()
115
116
        super(FtpTransport, self).__init__(base)
1185.36.4 by Daniel Silverstone
Add FTP transport
117
        self._FTP_instance = _provided_instance
118
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
119
    def _unparse_url(self, path=None):
120
        if path is None:
121
            path = self._path
122
        path = urllib.quote(path)
123
        netloc = urllib.quote(self._host)
124
        if self._username is not None:
125
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
126
        if self._port is not None:
127
            netloc = '%s:%d' % (netloc, self._port)
1952.1.1 by John Arbash Meinel
Ghozzy: Fix Bzr's support of Active FTP (aftp://)
128
        proto = 'ftp'
129
        if self.is_active:
130
            proto = 'aftp'
131
        return urlparse.urlunparse((proto, netloc, path, '', '', ''))
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
132
1185.36.4 by Daniel Silverstone
Add FTP transport
133
    def _get_FTP(self):
134
        """Return the ftplib.FTP instance for this object."""
135
        if self._FTP_instance is not None:
136
            return self._FTP_instance
137
        
138
        try:
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
139
            self._FTP_instance = _find_FTP(self._host, self._port,
140
                                           self._username, self._password,
1551.3.11 by Aaron Bentley
Merge from Robert
141
                                           self.is_active)
1185.36.4 by Daniel Silverstone
Add FTP transport
142
            return self._FTP_instance
143
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
144
            raise errors.TransportError(msg="Error setting up connection: %s"
1185.36.4 by Daniel Silverstone
Add FTP transport
145
                                    % str(e), orig_error=e)
146
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
147
    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()
148
        """Try to translate an ftplib.error_perm exception.
149
150
        :param err: The error to translate into a bzr error
151
        :param path: The path which had problems
152
        :param extra: Extra information which can be included
153
        :param unknown_exc: If None, we will just raise the original exception
154
                    otherwise we raise unknown_exc(path, extra=extra)
155
        """
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.
156
        s = str(err).lower()
157
        if not extra:
158
            extra = str(err)
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
159
        else:
160
            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.
161
        if ('no such file' in s
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
162
            or 'could not open' in s
163
            or 'no such dir' in s
2338.1.1 by John Arbash Meinel
(Arnaud Fontaine) Compatibility with vsftpd error messages.
164
            or 'could not create file' in s # vsftpd
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
165
            ):
166
            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.
167
        if ('file exists' in s):
1707.3.15 by John Arbash Meinel
Handle trying to list a file instead of a dir
168
            raise errors.FileExists(path, extra=extra)
169
        if ('not a directory' in s):
170
            raise errors.PathError(path, extra=extra)
171
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
172
        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()
173
174
        if unknown_exc:
175
            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.
176
        # TODO: jam 20060516 Consider re-raising the error wrapped in 
177
        #       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
178
        #       Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
179
        #       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.
180
        #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
181
        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.
182
1185.36.4 by Daniel Silverstone
Add FTP transport
183
    def should_cache(self):
184
        """Return True if the data pulled across should be cached locally.
185
        """
186
        return True
187
188
    def clone(self, offset=None):
189
        """Return a new FtpTransport with root at self.base + offset.
190
        """
191
        mutter("FTP clone")
192
        if offset is None:
193
            return FtpTransport(self.base, self._FTP_instance)
194
        else:
195
            return FtpTransport(self.abspath(offset), self._FTP_instance)
196
197
    def _abspath(self, relpath):
198
        assert isinstance(relpath, basestring)
1948.1.7 by John Arbash Meinel
Fix FtpTransport so that it raises UnicodeError and skips the unicode path tests
199
        relpath = urlutils.unescape(relpath)
1910.15.11 by Andrew Bennetts
Fix the FTP transport's handling of abspath('/')
200
        if relpath.startswith('/'):
201
            basepath = []
202
        else:
203
            basepath = self._path.split('/')
1185.36.4 by Daniel Silverstone
Add FTP transport
204
        if len(basepath) > 0 and basepath[-1] == '':
205
            basepath = basepath[:-1]
1910.15.11 by Andrew Bennetts
Fix the FTP transport's handling of abspath('/')
206
        for p in relpath.split('/'):
1185.36.4 by Daniel Silverstone
Add FTP transport
207
            if p == '..':
208
                if len(basepath) == 0:
209
                    # In most filesystems, a request for the parent
210
                    # of root, just returns root.
211
                    continue
212
                basepath.pop()
213
            elif p == '.' or p == '':
214
                continue # No-op
215
            else:
216
                basepath.append(p)
217
        # Possibly, we could use urlparse.urljoin() here, but
218
        # I'm concerned about when it chooses to strip the last
219
        # portion of the path, and when it doesn't.
1948.1.7 by John Arbash Meinel
Fix FtpTransport so that it raises UnicodeError and skips the unicode path tests
220
221
        # XXX: It seems that ftplib does not handle Unicode paths
222
        # at the same time, medusa won't handle utf8 paths
223
        # So if we .encode(utf8) here, then we get a Server failure.
224
        # while if we use str(), we get a UnicodeError, and the test suite
225
        # just skips testing UnicodePaths.
226
        return str('/'.join(basepath) or '/')
1185.36.4 by Daniel Silverstone
Add FTP transport
227
    
228
    def abspath(self, relpath):
229
        """Return the full url to the given relative path.
230
        This can be supplied with a string or a list
231
        """
232
        path = self._abspath(relpath)
1707.3.4 by John Arbash Meinel
Moved most of sftp.split_url into a Transport function.
233
        return self._unparse_url(path)
1185.36.4 by Daniel Silverstone
Add FTP transport
234
235
    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.
236
        """Does the target location exist?"""
237
        # FIXME jam 20060516 We *do* ask about directories in the test suite
238
        #       We don't seem to in the actual codebase
239
        # XXX: I assume we're never asked has(dirname) and thus I use
240
        # the FTP size command and assume that if it doesn't raise,
241
        # all is good.
242
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
243
        try:
244
            f = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
245
            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.
246
            s = f.size(abspath)
247
            mutter("FTP has: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
248
            return True
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
249
        except ftplib.error_perm, e:
250
            if ('is a directory' in str(e).lower()):
251
                mutter("FTP has dir: %s: %s", abspath, e)
252
                return True
253
            mutter("FTP has not: %s: %s", abspath, e)
1185.36.4 by Daniel Silverstone
Add FTP transport
254
            return False
255
2164.2.15 by Vincent Ladeuil
Http redirections are not followed by default. Do not use hints
256
    def get(self, relpath, decode=False, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
257
        """Get the file at the given relative path.
258
259
        :param relpath: The relative path to the file
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
260
        :param retries: Number of retries after temporary failures so far
261
                        for this operation.
1185.36.4 by Daniel Silverstone
Add FTP transport
262
263
        We're meant to return a file-like object which bzr will
264
        then read from. For now we do this via the magic of StringIO
265
        """
1540.3.6 by Martin Pool
[merge] update from bzr.dev
266
        # TODO: decode should be deprecated
1185.36.4 by Daniel Silverstone
Add FTP transport
267
        try:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
268
            mutter("FTP get: %s", self._abspath(relpath))
1185.36.4 by Daniel Silverstone
Add FTP transport
269
            f = self._get_FTP()
270
            ret = StringIO()
271
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
272
            ret.seek(0)
273
            return ret
274
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
275
            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
276
        except ftplib.error_temp, e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
277
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
278
                raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
279
                                     % self.abspath(relpath),
280
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
281
            else:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
282
                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
283
                self._FTP_instance = None
284
                return self.get(relpath, decode, retries+1)
1185.72.15 by Matthieu Moy
better error messages on ftp failures
285
        except EOFError, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
286
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
287
                raise errors.TransportError("FTP control connection closed during GET %s."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
288
                                     % self.abspath(relpath),
289
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
290
            else:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
291
                warning("FTP control connection closed. Trying to reopen.")
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
292
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
293
                self._FTP_instance = None
294
                return self.get(relpath, decode, retries+1)
1185.36.4 by Daniel Silverstone
Add FTP transport
295
1955.3.6 by John Arbash Meinel
Lots of deprecation warnings, but no errors
296
    def put_file(self, relpath, fp, mode=None, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
297
        """Copy the file-like or string object into the location.
298
299
        :param relpath: Location to put the contents, relative to base.
1185.72.13 by Matthieu Moy
Make ftp put atomic
300
        :param fp:       File-like or string object.
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
301
        :param retries: Number of retries after temporary failures so far
302
                        for this operation.
303
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
304
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
1185.36.4 by Daniel Silverstone
Add FTP transport
305
        """
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
306
        abspath = self._abspath(relpath)
307
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
1185.72.13 by Matthieu Moy
Make ftp put atomic
308
                        os.getpid(), random.randint(0,0x7FFFFFFF))
1963.2.6 by Robey Pointer
pychecker is on crack; go back to using 'is None'.
309
        if getattr(fp, 'read', None) is None:
1185.36.4 by Daniel Silverstone
Add FTP transport
310
            fp = StringIO(fp)
311
        try:
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
312
            mutter("FTP put: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
313
            f = self._get_FTP()
1185.72.13 by Matthieu Moy
Make ftp put atomic
314
            try:
315
                f.storbinary('STOR '+tmp_abspath, fp)
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
316
                f.rename(tmp_abspath, abspath)
1185.72.13 by Matthieu Moy
Make ftp put atomic
317
            except (ftplib.error_temp,EOFError), e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
318
                warning("Failure during ftp PUT. Deleting temporary file.")
1185.72.13 by Matthieu Moy
Make ftp put atomic
319
                try:
320
                    f.delete(tmp_abspath)
321
                except:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
322
                    warning("Failed to delete temporary file on the"
323
                            " server.\nFile: %s", tmp_abspath)
1185.72.13 by Matthieu Moy
Make ftp put atomic
324
                    raise e
325
                raise
1185.36.4 by Daniel Silverstone
Add FTP transport
326
        except ftplib.error_perm, e:
1707.3.18 by John Arbash Meinel
Fix error handling for ftp.put()
327
            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
328
        except ftplib.error_temp, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
329
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
330
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
331
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
332
            else:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
333
                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
334
                self._FTP_instance = None
1955.3.6 by John Arbash Meinel
Lots of deprecation warnings, but no errors
335
                self.put_file(relpath, fp, mode, retries+1)
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
336
        except EOFError:
1185.72.10 by Matthieu Moy
One 1 -> _number_of_retries was missing
337
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
338
                raise errors.TransportError("FTP control connection closed during PUT %s."
1185.72.15 by Matthieu Moy
better error messages on ftp failures
339
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
340
            else:
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
341
                warning("FTP control connection closed. Trying to reopen.")
342
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
343
                self._FTP_instance = None
1955.3.6 by John Arbash Meinel
Lots of deprecation warnings, but no errors
344
                self.put_file(relpath, fp, mode, retries+1)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
345
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
346
    def mkdir(self, relpath, mode=None):
1185.36.4 by Daniel Silverstone
Add FTP transport
347
        """Create a directory at the given path."""
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
348
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
349
        try:
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
350
            mutter("FTP mkd: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
351
            f = self._get_FTP()
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
352
            f.mkd(abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
353
        except ftplib.error_perm, e:
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
354
            self._translate_perm_error(e, abspath,
355
                unknown_exc=errors.FileExists)
1185.36.4 by Daniel Silverstone
Add FTP transport
356
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
357
    def rmdir(self, rel_path):
358
        """Delete the directory at rel_path"""
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
359
        abspath = self._abspath(rel_path)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
360
        try:
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
361
            mutter("FTP rmd: %s", abspath)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
362
            f = self._get_FTP()
1707.3.14 by John Arbash Meinel
Cleanup mkdir and rmdir error handling.
363
            f.rmd(abspath)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
364
        except ftplib.error_perm, e:
1707.3.21 by John Arbash Meinel
rmdir can raise PathError when it cannot delete a directory.
365
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
366
1955.3.15 by John Arbash Meinel
Deprecate 'Transport.append' in favor of Transport.append_file or Transport.append_bytes
367
    def append_file(self, relpath, f, mode=None):
1185.36.4 by Daniel Silverstone
Add FTP transport
368
        """Append the text in the file-like object into the final
369
        location.
370
        """
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
371
        abspath = self._abspath(relpath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
372
        if self.has(relpath):
373
            ftp = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
374
            result = ftp.size(abspath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
375
        else:
376
            result = 0
377
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
378
        mutter("FTP appe to %s", abspath)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
379
        self._try_append(relpath, f.read(), mode)
380
381
        return result
382
383
    def _try_append(self, relpath, text, mode=None, retries=0):
384
        """Try repeatedly to append the given text to the file at relpath.
385
        
386
        This is a recursive function. On errors, it will be called until the
387
        number of retries is exceeded.
388
        """
389
        try:
390
            abspath = self._abspath(relpath)
391
            mutter("FTP appe (try %d) to %s", retries, abspath)
392
            ftp = self._get_FTP()
393
            ftp.voidcmd("TYPE I")
394
            cmd = "APPE %s" % abspath
395
            conn = ftp.transfercmd(cmd)
396
            conn.sendall(text)
397
            conn.close()
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
398
            if mode:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
399
                self._setmode(relpath, mode)
400
            ftp.getresp()
401
        except ftplib.error_perm, e:
1707.3.26 by John Arbash Meinel
Added a default exception for append
402
            self._translate_perm_error(e, abspath, extra='error appending',
403
                unknown_exc=errors.NoSuchFile)
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
404
        except ftplib.error_temp, e:
405
            if retries > _number_of_retries:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
406
                raise errors.TransportError("FTP temporary error during APPEND %s." \
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
407
                        "Aborting." % abspath, orig_error=e)
408
            else:
409
                warning("FTP temporary error: %s. Retrying.", str(e))
410
                self._FTP_instance = None
411
                self._try_append(relpath, text, mode, retries+1)
412
413
    def _setmode(self, relpath, mode):
414
        """Set permissions on a path.
415
416
        Only set permissions if the FTP server supports the 'SITE CHMOD'
417
        extension.
418
        """
419
        try:
420
            mutter("FTP site chmod: setting permissions to %s on %s",
421
                str(mode), self._abspath(relpath))
422
            ftp = self._get_FTP()
423
            cmd = "SITE CHMOD %s %s" % (self._abspath(relpath), str(mode))
424
            ftp.sendcmd(cmd)
425
        except ftplib.error_perm, e:
426
            # Command probably not available on this server
427
            warning("FTP Could not set permissions to %s on %s. %s",
428
                    str(mode), self._abspath(relpath), str(e))
1185.36.4 by Daniel Silverstone
Add FTP transport
429
1707.3.11 by John Arbash Meinel
fixing more tests.
430
    # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
431
    #       to copy something to another machine. And you may be able
432
    #       to give it its own address as the 'to' location.
433
    #       So implement a fancier 'copy()'
1185.36.4 by Daniel Silverstone
Add FTP transport
434
435
    def move(self, rel_from, rel_to):
436
        """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()
437
        abs_from = self._abspath(rel_from)
438
        abs_to = self._abspath(rel_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
439
        try:
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
440
            mutter("FTP mv: %s => %s", abs_from, abs_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
441
            f = self._get_FTP()
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
442
            f.rename(abs_from, abs_to)
1185.36.4 by Daniel Silverstone
Add FTP transport
443
        except ftplib.error_perm, e:
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
444
            self._translate_perm_error(e, abs_from,
445
                extra='unable to rename to %r' % (rel_to,), 
446
                unknown_exc=errors.PathError)
1185.36.4 by Daniel Silverstone
Add FTP transport
447
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
448
    rename = move
449
1185.36.4 by Daniel Silverstone
Add FTP transport
450
    def delete(self, relpath):
451
        """Delete the item at relpath"""
1707.3.11 by John Arbash Meinel
fixing more tests.
452
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
453
        try:
1707.3.11 by John Arbash Meinel
fixing more tests.
454
            mutter("FTP rm: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
455
            f = self._get_FTP()
1707.3.11 by John Arbash Meinel
fixing more tests.
456
            f.delete(abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
457
        except ftplib.error_perm, e:
1707.3.25 by John Arbash Meinel
Setting some default exception classes, which can help a lot.
458
            self._translate_perm_error(e, abspath, 'error deleting',
459
                unknown_exc=errors.NoSuchFile)
1185.36.4 by Daniel Silverstone
Add FTP transport
460
461
    def listable(self):
462
        """See Transport.listable."""
463
        return True
464
465
    def list_dir(self, relpath):
466
        """See Transport.list_dir."""
1959.2.5 by John Arbash Meinel
Small fixes in ftp.list_dir()
467
        basepath = self._abspath(relpath)
468
        mutter("FTP nlst: %s", basepath)
1959.2.1 by John Arbash Meinel
David Allouche: Make transports return escaped paths
469
        f = self._get_FTP()
1185.36.4 by Daniel Silverstone
Add FTP transport
470
        try:
1707.3.8 by John Arbash Meinel
[patch] Alexandre Saint: enable append support for ftp
471
            paths = f.nlst(basepath)
1185.36.4 by Daniel Silverstone
Add FTP transport
472
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
473
            self._translate_perm_error(e, relpath, extra='error with list_dir')
1959.2.1 by John Arbash Meinel
David Allouche: Make transports return escaped paths
474
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
475
        if paths and paths[0].startswith(basepath):
476
            entries = [path[len(basepath)+1:] for path in paths]
477
        else:
478
            entries = paths
479
        # Remove . and .. if present
1959.2.5 by John Arbash Meinel
Small fixes in ftp.list_dir()
480
        return [urlutils.escape(entry) for entry in entries
481
                if entry not in ('.', '..')]
1185.36.4 by Daniel Silverstone
Add FTP transport
482
483
    def iter_files_recursive(self):
484
        """See Transport.iter_files_recursive.
485
486
        This is cargo-culted from the SFTP transport"""
487
        mutter("FTP iter_files_recursive")
488
        queue = list(self.list_dir("."))
489
        while queue:
1959.2.1 by John Arbash Meinel
David Allouche: Make transports return escaped paths
490
            relpath = queue.pop(0)
1185.36.4 by Daniel Silverstone
Add FTP transport
491
            st = self.stat(relpath)
492
            if stat.S_ISDIR(st.st_mode):
493
                for i, basename in enumerate(self.list_dir(relpath)):
494
                    queue.insert(i, relpath+"/"+basename)
495
            else:
496
                yield relpath
497
498
    def stat(self, relpath):
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
499
        """Return the stat information for a file."""
500
        abspath = self._abspath(relpath)
1185.36.4 by Daniel Silverstone
Add FTP transport
501
        try:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
502
            mutter("FTP stat: %s", abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
503
            f = self._get_FTP()
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
504
            return FtpStatResult(f, abspath)
1185.36.4 by Daniel Silverstone
Add FTP transport
505
        except ftplib.error_perm, e:
1707.3.13 by John Arbash Meinel
Changing raise Error to raise errors.Error
506
            self._translate_perm_error(e, abspath, extra='error w/ stat')
1185.36.4 by Daniel Silverstone
Add FTP transport
507
508
    def lock_read(self, relpath):
509
        """Lock the given file for shared (read) access.
510
        :return: A lock object, which should be passed to Transport.unlock()
511
        """
512
        # The old RemoteBranch ignore lock for reading, so we will
513
        # continue that tradition and return a bogus lock object.
514
        class BogusLock(object):
515
            def __init__(self, path):
516
                self.path = path
517
            def unlock(self):
518
                pass
519
        return BogusLock(relpath)
520
521
    def lock_write(self, relpath):
522
        """Lock the given file for exclusive (write) access.
523
        WARNING: many transports do not support this, so trying avoid using it
524
525
        :return: A lock object, which should be passed to Transport.unlock()
526
        """
527
        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.
528
529
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
530
class FtpServer(Server):
531
    """Common code for SFTP server facilities."""
532
533
    def __init__(self):
534
        self._root = None
535
        self._ftp_server = None
536
        self._port = None
537
        self._async_thread = None
538
        # ftp server logs
539
        self.logs = []
540
541
    def get_url(self):
542
        """Calculate an ftp url to this server."""
543
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
544
545
#    def get_bogus_url(self):
546
#        """Return a URL which cannot be connected to."""
547
#        return 'ftp://127.0.0.1:1'
548
549
    def log(self, message):
550
        """This is used by medusa.ftp_server to log connections, etc."""
551
        self.logs.append(message)
552
553
    def setUp(self):
554
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
555
        if not _have_medusa:
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
556
            raise RuntimeError('Must have medusa to run the FtpServer')
557
558
        self._root = os.getcwdu()
559
        self._ftp_server = _ftp_server(
560
            authorizer=_test_authorizer(root=self._root),
561
            ip='localhost',
562
            port=0, # bind to a random port
563
            resolver=None,
564
            logger_object=self # Use FtpServer.log() for messages
565
            )
566
        self._port = self._ftp_server.getsockname()[1]
567
        # Don't let it loop forever, or handle an infinite number of requests.
568
        # In this case it will run for 100s, or 1000 requests
569
        self._async_thread = threading.Thread(target=asyncore.loop,
570
                kwargs={'timeout':0.1, 'count':1000})
571
        self._async_thread.setDaemon(True)
572
        self._async_thread.start()
573
574
    def tearDown(self):
575
        """See bzrlib.transport.Server.tearDown."""
576
        # have asyncore release the channel
577
        self._ftp_server.del_channel()
578
        asyncore.close_all()
579
        self._async_thread.join()
580
581
582
_ftp_channel = None
583
_ftp_server = None
584
_test_authorizer = None
585
586
587
def _setup_medusa():
588
    global _have_medusa, _ftp_channel, _ftp_server, _test_authorizer
589
    try:
590
        import medusa
591
        import medusa.filesys
592
        import medusa.ftp_server
593
    except ImportError:
594
        return False
595
596
    _have_medusa = True
597
598
    class test_authorizer(object):
599
        """A custom Authorizer object for running the test suite.
600
601
        The reason we cannot use dummy_authorizer, is because it sets the
602
        channel to readonly, which we don't always want to do.
603
        """
604
605
        def __init__(self, root):
606
            self.root = root
607
608
        def authorize(self, channel, username, password):
609
            """Return (success, reply_string, filesystem)"""
610
            if not _have_medusa:
611
                return 0, 'No Medusa.', None
612
613
            channel.persona = -1, -1
614
            if username == 'anonymous':
615
                channel.read_only = 1
616
            else:
617
                channel.read_only = 0
618
619
            return 1, 'OK.', medusa.filesys.os_filesystem(self.root)
620
621
622
    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.
623
        """Customized ftp channel"""
624
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
625
        def log(self, message):
626
            """Redirect logging requests."""
627
            mutter('_ftp_channel: %s', message)
628
            
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
629
        def log_info(self, message, type='info'):
630
            """Redirect logging requests."""
631
            mutter('_ftp_channel %s: %s', type, message)
632
            
633
        def cmd_rnfr(self, line):
634
            """Prepare for renaming a file."""
635
            self._renaming = line[1]
636
            self.respond('350 Ready for RNTO')
637
            # TODO: jam 20060516 in testing, the ftp server seems to
638
            #       check that the file already exists, or it sends
639
            #       550 RNFR command failed
640
641
        def cmd_rnto(self, line):
642
            """Rename a file based on the target given.
643
644
            rnto must be called after calling rnfr.
645
            """
646
            if not self._renaming:
647
                self.respond('503 RNFR required first.')
648
            pfrom = self.filesystem.translate(self._renaming)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
649
            self._renaming = None
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
650
            pto = self.filesystem.translate(line[1])
651
            try:
652
                os.rename(pfrom, pto)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
653
            except (IOError, OSError), e:
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
654
                # TODO: jam 20060516 return custom responses based on
655
                #       why the command failed
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
656
                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.
657
            except:
658
                self.respond('550 RNTO failed')
659
                # For a test server, we will go ahead and just die
660
                raise
1707.3.20 by John Arbash Meinel
Found the problem, was sending 2 responses
661
            else:
662
                self.respond('250 Rename successful.')
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
663
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
664
        def cmd_size(self, line):
665
            """Return the size of a file
666
667
            This is overloaded to help the test suite determine if the 
668
            target is a directory.
669
            """
670
            filename = line[1]
671
            if not self.filesystem.isfile(filename):
672
                if self.filesystem.isdir(filename):
673
                    self.respond('550 "%s" is a directory' % (filename,))
674
                else:
675
                    self.respond('550 "%s" is not a file' % (filename,))
676
            else:
677
                self.respond('213 %d' 
1707.3.17 by John Arbash Meinel
Fix errors in cmd_size
678
                    % (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.
679
680
        def cmd_mkd(self, line):
681
            """Create a directory.
682
683
            Overloaded because default implementation does not distinguish
684
            *why* it cannot make a directory.
685
            """
686
            if len (line) != 2:
1773.4.1 by Martin Pool
Add pyflakes makefile target; fix many warnings
687
                self.command_not_understood(''.join(line))
1707.3.16 by John Arbash Meinel
Overloading cmd_size and cmd_mkd so that the test suite can handle directories.
688
            else:
689
                path = line[1]
690
                try:
691
                    self.filesystem.mkdir (path)
692
                    self.respond ('257 MKD command successful.')
693
                except (IOError, OSError), e:
694
                    self.respond ('550 error creating directory: %s' % (e,))
695
                except:
696
                    self.respond ('550 error creating directory.')
697
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
698
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
699
    class ftp_server(medusa.ftp_server.ftp_server):
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
700
        """Customize the behavior of the Medusa ftp_server.
701
702
        There are a few warts on the ftp_server, based on how it expects
703
        to be used.
704
        """
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
705
        _renaming = None
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
706
        ftp_channel_class = ftp_channel
1707.3.7 by John Arbash Meinel
Creating child of ftp_channel to support renaming, need to add appending.
707
708
        def __init__(self, *args, **kwargs):
709
            mutter('Initializing _ftp_server: %r, %r', args, kwargs)
710
            medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
711
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
712
        def log(self, message):
713
            """Redirect logging requests."""
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
714
            mutter('_ftp_server: %s', message)
1707.3.19 by John Arbash Meinel
For some reason after RNTO we get a failure for has()
715
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
716
        def log_info(self, message, type='info'):
717
            """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.
718
            mutter('_ftp_server %s: %s', type, message)
1707.3.2 by John Arbash Meinel
Removing stipple from ftp tests.
719
1707.3.30 by John Arbash Meinel
Only load medusa if you are running the test suite.
720
    _test_authorizer = test_authorizer
721
    _ftp_channel = ftp_channel
722
    _ftp_server = ftp_server
723
724
    return True
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
725
726
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.
727
def get_test_permutations():
728
    """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.
729
    if not _setup_medusa():
1707.3.1 by John Arbash Meinel
Have a FtpServer, but the tests fail
730
        warn("You must install medusa (http://www.amk.ca/python/code/medusa.html) for FTP tests")
731
        return []
732
    else:
733
        return [(FtpTransport, FtpServer)]