/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
1185.36.4 by Daniel Silverstone
Add FTP transport
1
# Copyright (C) 2005 Canonical Ltd
2
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
"""Implementation of Transport over ftp.
17
18
Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
19
cargo-culting from the sftp transport and the http transport.
20
21
It provides the ftp:// and aftp:// protocols where ftp:// is passive ftp
22
and aftp:// is active ftp. Most people will want passive ftp for traversing
23
NAT and other firewalls, so it's best to use it unless you explicitly want
24
active, in which case aftp:// will be your friend.
25
"""
26
27
from cStringIO import StringIO
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.
28
import errno
1185.36.4 by Daniel Silverstone
Add FTP transport
29
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.
30
import os
31
import urllib
1185.36.4 by Daniel Silverstone
Add FTP transport
32
import urlparse
33
import stat
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
34
import time
1185.72.13 by Matthieu Moy
Make ftp put atomic
35
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.
36
from warnings import warn
37
38
39
from bzrlib.transport import Transport
40
from bzrlib.errors import (TransportNotPossible, TransportError,
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
41
                           NoSuchFile, FileExists, DirectoryNotEmpty)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
42
from bzrlib.trace import mutter, warning
1551.3.11 by Aaron Bentley
Merge from Robert
43
44
45
_FTP_cache = {}
46
def _find_FTP(hostname, username, password, is_active):
47
    """Find an ftplib.FTP instance attached to this triplet."""
48
    key = "%s|%s|%s|%s" % (hostname, username, password, is_active)
49
    if key not in _FTP_cache:
50
        mutter("Constructing FTP instance against %r" % key)
51
        _FTP_cache[key] = ftplib.FTP(hostname, username, password)
52
        _FTP_cache[key].set_pasv(not is_active)
53
    return _FTP_cache[key]    
54
55
56
class FtpTransportError(TransportError):
57
    pass
1185.36.4 by Daniel Silverstone
Add FTP transport
58
59
60
class FtpStatResult(object):
61
    def __init__(self, f, relpath):
62
        try:
63
            self.st_size = f.size(relpath)
64
            self.st_mode = stat.S_IFREG
65
        except ftplib.error_perm:
66
            pwd = f.pwd()
67
            try:
68
                f.cwd(relpath)
69
                self.st_mode = stat.S_IFDIR
70
            finally:
71
                f.cwd(pwd)
72
73
1185.72.15 by Matthieu Moy
better error messages on ftp failures
74
_number_of_retries = 2
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
75
_sleep_between_retries = 5
1185.72.12 by Matthieu Moy
made __number_of_retries global
76
1185.36.4 by Daniel Silverstone
Add FTP transport
77
class FtpTransport(Transport):
78
    """This is the transport agent for ftp:// access."""
79
80
    def __init__(self, base, _provided_instance=None):
81
        """Set the base path where files will be stored."""
82
        assert base.startswith('ftp://') or base.startswith('aftp://')
83
        super(FtpTransport, self).__init__(base)
84
        self.is_active = base.startswith('aftp://')
85
        if self.is_active:
86
            base = base[1:]
87
        (self._proto, self._host,
88
            self._path, self._parameters,
89
            self._query, self._fragment) = urlparse.urlparse(self.base)
90
        self._FTP_instance = _provided_instance
91
92
    def _get_FTP(self):
93
        """Return the ftplib.FTP instance for this object."""
94
        if self._FTP_instance is not None:
95
            return self._FTP_instance
96
        
97
        try:
98
            username = ''
99
            password = ''
100
            hostname = self._host
101
            if '@' in hostname:
102
                username, hostname = hostname.split("@", 1)
103
            if ':' in username:
104
                username, password = username.split(":", 1)
105
1551.3.11 by Aaron Bentley
Merge from Robert
106
            self._FTP_instance = _find_FTP(hostname, username, password,
107
                                           self.is_active)
1185.36.4 by Daniel Silverstone
Add FTP transport
108
            return self._FTP_instance
109
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
110
            raise TransportError(msg="Error setting up connection: %s"
1185.36.4 by Daniel Silverstone
Add FTP transport
111
                                    % str(e), orig_error=e)
112
113
    def should_cache(self):
114
        """Return True if the data pulled across should be cached locally.
115
        """
116
        return True
117
118
    def clone(self, offset=None):
119
        """Return a new FtpTransport with root at self.base + offset.
120
        """
121
        mutter("FTP clone")
122
        if offset is None:
123
            return FtpTransport(self.base, self._FTP_instance)
124
        else:
125
            return FtpTransport(self.abspath(offset), self._FTP_instance)
126
127
    def _abspath(self, relpath):
128
        assert isinstance(relpath, basestring)
129
        relpath = urllib.unquote(relpath)
130
        if isinstance(relpath, basestring):
131
            relpath_parts = relpath.split('/')
132
        else:
133
            # TODO: Don't call this with an array - no magic interfaces
134
            relpath_parts = relpath[:]
135
        if len(relpath_parts) > 1:
136
            if relpath_parts[0] == '':
137
                raise ValueError("path %r within branch %r seems to be absolute"
138
                                 % (relpath, self._path))
139
        basepath = self._path.split('/')
140
        if len(basepath) > 0 and basepath[-1] == '':
141
            basepath = basepath[:-1]
142
        for p in relpath_parts:
143
            if p == '..':
144
                if len(basepath) == 0:
145
                    # In most filesystems, a request for the parent
146
                    # of root, just returns root.
147
                    continue
148
                basepath.pop()
149
            elif p == '.' or p == '':
150
                continue # No-op
151
            else:
152
                basepath.append(p)
153
        # Possibly, we could use urlparse.urljoin() here, but
154
        # I'm concerned about when it chooses to strip the last
155
        # portion of the path, and when it doesn't.
156
        return '/'.join(basepath)
157
    
158
    def abspath(self, relpath):
159
        """Return the full url to the given relative path.
160
        This can be supplied with a string or a list
161
        """
162
        path = self._abspath(relpath)
163
        return urlparse.urlunparse((self._proto,
164
                self._host, path, '', '', ''))
165
166
    def has(self, relpath):
167
        """Does the target location exist?
168
169
        XXX: I assume we're never asked has(dirname) and thus I use
170
        the FTP size command and assume that if it doesn't raise,
171
        all is good.
172
        """
173
        try:
174
            f = self._get_FTP()
175
            s = f.size(self._abspath(relpath))
176
            mutter("FTP has: %s" % self._abspath(relpath))
177
            return True
178
        except ftplib.error_perm:
179
            mutter("FTP has not: %s" % self._abspath(relpath))
180
            return False
181
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
182
    def get(self, relpath, decode=False, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
183
        """Get the file at the given relative path.
184
185
        :param relpath: The relative path to the file
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
186
        :param retries: Number of retries after temporary failures so far
187
                        for this operation.
1185.36.4 by Daniel Silverstone
Add FTP transport
188
189
        We're meant to return a file-like object which bzr will
190
        then read from. For now we do this via the magic of StringIO
191
        """
1540.3.6 by Martin Pool
[merge] update from bzr.dev
192
        # TODO: decode should be deprecated
1185.36.4 by Daniel Silverstone
Add FTP transport
193
        try:
194
            mutter("FTP get: %s" % self._abspath(relpath))
195
            f = self._get_FTP()
196
            ret = StringIO()
197
            f.retrbinary('RETR '+self._abspath(relpath), ret.write, 8192)
198
            ret.seek(0)
199
            return ret
200
        except ftplib.error_perm, e:
1185.50.39 by John Arbash Meinel
[patch] Wouter Bolsterlee: Fix ftp error in error handling code
201
            raise NoSuchFile(self.abspath(relpath), extra=str(e))
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
202
        except ftplib.error_temp, e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
203
            if retries > _number_of_retries:
204
                raise TransportError(msg="FTP temporary error during GET %s. Aborting."
205
                                     % self.abspath(relpath),
206
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
207
            else:
208
                warning("FTP temporary error: %s. Retrying." % str(e))
209
                self._FTP_instance = None
210
                return self.get(relpath, decode, retries+1)
1185.72.15 by Matthieu Moy
better error messages on ftp failures
211
        except EOFError, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
212
            if retries > _number_of_retries:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
213
                raise TransportError("FTP control connection closed during GET %s."
214
                                     % self.abspath(relpath),
215
                                     orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
216
            else:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
217
                warning("FTP control connection closed. Trying to reopen.")
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
218
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
219
                self._FTP_instance = None
220
                return self.get(relpath, decode, retries+1)
1185.36.4 by Daniel Silverstone
Add FTP transport
221
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
222
    def put(self, relpath, fp, mode=None, retries=0):
1185.36.4 by Daniel Silverstone
Add FTP transport
223
        """Copy the file-like or string object into the location.
224
225
        :param relpath: Location to put the contents, relative to base.
1185.72.13 by Matthieu Moy
Make ftp put atomic
226
        :param fp:       File-like or string object.
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
227
        :param retries: Number of retries after temporary failures so far
228
                        for this operation.
229
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
230
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
1185.36.4 by Daniel Silverstone
Add FTP transport
231
        """
1185.72.13 by Matthieu Moy
Make ftp put atomic
232
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (self._abspath(relpath), time.time(),
233
                        os.getpid(), random.randint(0,0x7FFFFFFF))
1185.36.4 by Daniel Silverstone
Add FTP transport
234
        if not hasattr(fp, 'read'):
235
            fp = StringIO(fp)
236
        try:
237
            mutter("FTP put: %s" % self._abspath(relpath))
238
            f = self._get_FTP()
1185.72.13 by Matthieu Moy
Make ftp put atomic
239
            try:
240
                f.storbinary('STOR '+tmp_abspath, fp)
241
                f.rename(tmp_abspath, self._abspath(relpath))
242
            except (ftplib.error_temp,EOFError), e:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
243
                warning("Failure during ftp PUT. Deleting temporary file.")
1185.72.13 by Matthieu Moy
Make ftp put atomic
244
                try:
245
                    f.delete(tmp_abspath)
246
                except:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
247
                    warning("Failed to delete temporary file on the server.\nFile: %s"
248
                            % tmp_abspath)
1185.72.13 by Matthieu Moy
Make ftp put atomic
249
                    raise e
250
                raise
1185.36.4 by Daniel Silverstone
Add FTP transport
251
        except ftplib.error_perm, e:
1551.3.11 by Aaron Bentley
Merge from Robert
252
            if "no such file" in str(e).lower():
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
253
                raise NoSuchFile("Error storing %s: %s"
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
254
                                 % (self.abspath(relpath), str(e)), extra=e)
1551.3.11 by Aaron Bentley
Merge from Robert
255
            else:
256
                raise FtpTransportError(orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
257
        except ftplib.error_temp, e:
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
258
            if retries > _number_of_retries:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
259
                raise TransportError("FTP temporary error during PUT %s. Aborting."
260
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
261
            else:
262
                warning("FTP temporary error: %s. Retrying." % str(e))
263
                self._FTP_instance = None
264
                self.put(relpath, fp, mode, retries+1)
1185.72.9 by Matthieu Moy
suggestions by jam and robertc
265
        except EOFError:
1185.72.10 by Matthieu Moy
One 1 -> _number_of_retries was missing
266
            if retries > _number_of_retries:
1185.72.15 by Matthieu Moy
better error messages on ftp failures
267
                raise TransportError("FTP control connection closed during PUT %s."
268
                                     % self.abspath(relpath), orig_error=e)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
269
            else:
1185.72.14 by Matthieu Moy
Sleep between retries when the connection closes
270
                warning("FTP control connection closed. Trying to reopen.")
271
                time.sleep(_sleep_between_retries)
1185.72.8 by Matthieu Moy
try to solve the problem of ftp servers closing the connection
272
                self._FTP_instance = None
273
                self.put(relpath, fp, mode, retries+1)
274
1185.36.4 by Daniel Silverstone
Add FTP transport
275
1185.58.2 by John Arbash Meinel
Added mode to the appropriate transport functions, and tests to make sure they work.
276
    def mkdir(self, relpath, mode=None):
1185.36.4 by Daniel Silverstone
Add FTP transport
277
        """Create a directory at the given path."""
278
        try:
279
            mutter("FTP mkd: %s" % self._abspath(relpath))
280
            f = self._get_FTP()
281
            try:
282
                f.mkd(self._abspath(relpath))
283
            except ftplib.error_perm, e:
284
                s = str(e)
285
                if 'File exists' in s:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
286
                    raise FileExists(self.abspath(relpath), extra=s)
1185.36.4 by Daniel Silverstone
Add FTP transport
287
                else:
288
                    raise
289
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
290
            raise TransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
291
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
292
    def rmdir(self, rel_path):
293
        """Delete the directory at rel_path"""
294
        try:
295
            mutter("FTP rmd: %s" % self._abspath(rel_path))
296
297
            f = self._get_FTP()
298
            f.rmd(self._abspath(rel_path))
299
        except ftplib.error_perm, e:
300
            if str(e).endswith("Directory not empty"):
301
                raise DirectoryNotEmpty(self._abspath(rel_path), extra=str(e))
302
            else:
303
                raise TransportError(msg="Cannot remove directory at %s" % \
304
                        self._abspath(rel_path), extra=str(e))
305
1185.36.4 by Daniel Silverstone
Add FTP transport
306
    def append(self, relpath, f):
307
        """Append the text in the file-like object into the final
308
        location.
309
        """
310
        raise TransportNotPossible('ftp does not support append()')
311
312
    def copy(self, rel_from, rel_to):
313
        """Copy the item at rel_from to the location at rel_to"""
314
        raise TransportNotPossible('ftp does not (yet) support copy()')
315
316
    def move(self, rel_from, rel_to):
317
        """Move the item at rel_from to the location at rel_to"""
318
        try:
319
            mutter("FTP mv: %s => %s" % (self._abspath(rel_from),
320
                                         self._abspath(rel_to)))
321
            f = self._get_FTP()
322
            f.rename(self._abspath(rel_from), self._abspath(rel_to))
323
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
324
            raise TransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
325
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
326
    rename = move
327
1185.36.4 by Daniel Silverstone
Add FTP transport
328
    def delete(self, relpath):
329
        """Delete the item at relpath"""
330
        try:
331
            mutter("FTP rm: %s" % self._abspath(relpath))
332
            f = self._get_FTP()
333
            f.delete(self._abspath(relpath))
334
        except ftplib.error_perm, e:
1658.1.1 by Martin Pool
Fix FTP push with metadir format (Alexandre Saint)
335
            if str(e).endswith("No such file or directory"):
336
                raise NoSuchFile(self._abspath(relpath), extra=str(e))
337
            else:
338
                raise TransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
339
340
    def listable(self):
341
        """See Transport.listable."""
342
        return True
343
344
    def list_dir(self, relpath):
345
        """See Transport.list_dir."""
346
        try:
347
            mutter("FTP nlst: %s" % self._abspath(relpath))
348
            f = self._get_FTP()
349
            basepath = self._abspath(relpath)
350
            # FTP.nlst returns paths prefixed by relpath, strip 'em
351
            the_list = f.nlst(basepath)
352
            stripped = [path[len(basepath)+1:] for path in the_list]
353
            # Remove . and .. if present, and return
354
            return [path for path in stripped if path not in (".", "..")]
355
        except ftplib.error_perm, e:
1185.31.44 by John Arbash Meinel
Cleaned up Exceptions for all transports.
356
            raise TransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
357
358
    def iter_files_recursive(self):
359
        """See Transport.iter_files_recursive.
360
361
        This is cargo-culted from the SFTP transport"""
362
        mutter("FTP iter_files_recursive")
363
        queue = list(self.list_dir("."))
364
        while queue:
365
            relpath = urllib.quote(queue.pop(0))
366
            st = self.stat(relpath)
367
            if stat.S_ISDIR(st.st_mode):
368
                for i, basename in enumerate(self.list_dir(relpath)):
369
                    queue.insert(i, relpath+"/"+basename)
370
            else:
371
                yield relpath
372
373
    def stat(self, relpath):
374
        """Return the stat information for a file.
375
        """
376
        try:
377
            mutter("FTP stat: %s" % self._abspath(relpath))
378
            f = self._get_FTP()
379
            return FtpStatResult(f, self._abspath(relpath))
380
        except ftplib.error_perm, e:
1185.72.16 by Matthieu Moy
Raise NoSuchFile in stat when this is the case
381
            if "no such file" in str(e).lower():
382
                raise NoSuchFile("Error storing %s: %s"
383
                                 % (self.abspath(relpath), str(e)), extra=e)
384
            else:
385
                raise FtpTransportError(orig_error=e)
1185.36.4 by Daniel Silverstone
Add FTP transport
386
387
    def lock_read(self, relpath):
388
        """Lock the given file for shared (read) access.
389
        :return: A lock object, which should be passed to Transport.unlock()
390
        """
391
        # The old RemoteBranch ignore lock for reading, so we will
392
        # continue that tradition and return a bogus lock object.
393
        class BogusLock(object):
394
            def __init__(self, path):
395
                self.path = path
396
            def unlock(self):
397
                pass
398
        return BogusLock(relpath)
399
400
    def lock_write(self, relpath):
401
        """Lock the given file for exclusive (write) access.
402
        WARNING: many transports do not support this, so trying avoid using it
403
404
        :return: A lock object, which should be passed to Transport.unlock()
405
        """
406
        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.
407
408
409
def get_test_permutations():
410
    """Return the permutations to be used in testing."""
411
    warn("There are no FTP transport provider tests yet.")
412
    return []