~bzr-pqm/bzr/bzr.dev

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