~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

Merge in bzrdir work to enable checkout improvements.

Show diffs side-by-side

added added

removed removed

Lines of Context:
31
31
import urllib
32
32
import urlparse
33
33
import stat
 
34
import time
 
35
import random
34
36
from warnings import warn
35
37
 
36
38
 
37
39
from bzrlib.transport import Transport
38
40
from bzrlib.errors import (TransportNotPossible, TransportError,
39
41
                           NoSuchFile, FileExists)
40
 
from bzrlib.trace import mutter
 
42
from bzrlib.trace import mutter, warning
 
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
41
58
 
42
59
 
43
60
class FtpStatResult(object):
54
71
                f.cwd(pwd)
55
72
 
56
73
 
 
74
_number_of_retries = 2
 
75
_sleep_between_retries = 5
 
76
 
57
77
class FtpTransport(Transport):
58
78
    """This is the transport agent for ftp:// access."""
59
79
 
69
89
            self._query, self._fragment) = urlparse.urlparse(self.base)
70
90
        self._FTP_instance = _provided_instance
71
91
 
72
 
 
73
92
    def _get_FTP(self):
74
93
        """Return the ftplib.FTP instance for this object."""
75
94
        if self._FTP_instance is not None:
84
103
            if ':' in username:
85
104
                username, password = username.split(":", 1)
86
105
 
87
 
            mutter("Constructing FTP instance")
88
 
            self._FTP_instance = ftplib.FTP(hostname, username, password)
89
 
            self._FTP_instance.set_pasv(not self.is_active)
 
106
            self._FTP_instance = _find_FTP(hostname, username, password,
 
107
                                           self.is_active)
90
108
            return self._FTP_instance
91
109
        except ftplib.error_perm, e:
92
110
            raise TransportError(msg="Error setting up connection: %s"
161
179
            mutter("FTP has not: %s" % self._abspath(relpath))
162
180
            return False
163
181
 
164
 
    def get(self, relpath, decode=False):
 
182
    def get(self, relpath, decode=False, retries=0):
165
183
        """Get the file at the given relative path.
166
184
 
167
185
        :param relpath: The relative path to the file
 
186
        :param retries: Number of retries after temporary failures so far
 
187
                        for this operation.
168
188
 
169
189
        We're meant to return a file-like object which bzr will
170
190
        then read from. For now we do this via the magic of StringIO
177
197
            ret.seek(0)
178
198
            return ret
179
199
        except ftplib.error_perm, e:
180
 
            raise NoSuchFile(self.abspath(relpath), extra=extra)
 
200
            raise NoSuchFile(self.abspath(relpath), extra=str(e))
 
201
        except ftplib.error_temp, e:
 
202
            if retries > _number_of_retries:
 
203
                raise TransportError(msg="FTP temporary error during GET %s. Aborting."
 
204
                                     % self.abspath(relpath),
 
205
                                     orig_error=e)
 
206
            else:
 
207
                warning("FTP temporary error: %s. Retrying." % str(e))
 
208
                self._FTP_instance = None
 
209
                return self.get(relpath, decode, retries+1)
 
210
        except EOFError, e:
 
211
            if retries > _number_of_retries:
 
212
                raise TransportError("FTP control connection closed during GET %s."
 
213
                                     % self.abspath(relpath),
 
214
                                     orig_error=e)
 
215
            else:
 
216
                warning("FTP control connection closed. Trying to reopen.")
 
217
                time.sleep(_sleep_between_retries)
 
218
                self._FTP_instance = None
 
219
                return self.get(relpath, decode, retries+1)
181
220
 
182
 
    def put(self, relpath, fp, mode=None):
 
221
    def put(self, relpath, fp, mode=None, retries=0):
183
222
        """Copy the file-like or string object into the location.
184
223
 
185
224
        :param relpath: Location to put the contents, relative to base.
186
 
        :param f:       File-like or string object.
187
 
        TODO: jam 20051215 This should be an atomic put, not overwritting files in place
 
225
        :param fp:       File-like or string object.
 
226
        :param retries: Number of retries after temporary failures so far
 
227
                        for this operation.
 
228
 
188
229
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
189
230
        """
 
231
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (self._abspath(relpath), time.time(),
 
232
                        os.getpid(), random.randint(0,0x7FFFFFFF))
190
233
        if not hasattr(fp, 'read'):
191
234
            fp = StringIO(fp)
192
235
        try:
193
236
            mutter("FTP put: %s" % self._abspath(relpath))
194
237
            f = self._get_FTP()
195
 
            f.storbinary('STOR '+self._abspath(relpath), fp, 8192)
 
238
            try:
 
239
                f.storbinary('STOR '+tmp_abspath, fp)
 
240
                f.rename(tmp_abspath, self._abspath(relpath))
 
241
            except (ftplib.error_temp,EOFError), e:
 
242
                warning("Failure during ftp PUT. Deleting temporary file.")
 
243
                try:
 
244
                    f.delete(tmp_abspath)
 
245
                except:
 
246
                    warning("Failed to delete temporary file on the server.\nFile: %s"
 
247
                            % tmp_abspath)
 
248
                    raise e
 
249
                raise
196
250
        except ftplib.error_perm, e:
197
 
            raise TransportError(orig_error=e)
 
251
            if "no such file" in str(e).lower():
 
252
                raise NoSuchFile("Error storing %s: %s"
 
253
                                 % (self.abspath(relpath), str(e)), extra=e)
 
254
            else:
 
255
                raise FtpTransportError(orig_error=e)
 
256
        except ftplib.error_temp, e:
 
257
            if retries > _number_of_retries:
 
258
                raise TransportError("FTP temporary error during PUT %s. Aborting."
 
259
                                     % self.abspath(relpath), orig_error=e)
 
260
            else:
 
261
                warning("FTP temporary error: %s. Retrying." % str(e))
 
262
                self._FTP_instance = None
 
263
                self.put(relpath, fp, mode, retries+1)
 
264
        except EOFError:
 
265
            if retries > _number_of_retries:
 
266
                raise TransportError("FTP control connection closed during PUT %s."
 
267
                                     % self.abspath(relpath), orig_error=e)
 
268
            else:
 
269
                warning("FTP control connection closed. Trying to reopen.")
 
270
                time.sleep(_sleep_between_retries)
 
271
                self._FTP_instance = None
 
272
                self.put(relpath, fp, mode, retries+1)
 
273
 
198
274
 
199
275
    def mkdir(self, relpath, mode=None):
200
276
        """Create a directory at the given path."""
282
358
            f = self._get_FTP()
283
359
            return FtpStatResult(f, self._abspath(relpath))
284
360
        except ftplib.error_perm, e:
285
 
            raise TransportError(orig_error=e)
 
361
            if "no such file" in str(e).lower():
 
362
                raise NoSuchFile("Error storing %s: %s"
 
363
                                 % (self.abspath(relpath), str(e)), extra=e)
 
364
            else:
 
365
                raise FtpTransportError(orig_error=e)
286
366
 
287
367
    def lock_read(self, relpath):
288
368
        """Lock the given file for shared (read) access.