~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Wouter van Heyst
  • Date: 2006-06-07 16:05:27 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: larstiq@larstiq.dyndns.org-20060607160527-2b3649154d0e2e84
more code cleanup

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29
29
import errno
30
30
import ftplib
31
31
import os
32
 
import os.path
33
32
import urllib
34
33
import urlparse
35
 
import select
36
34
import stat
37
35
import threading
38
36
import time
39
37
import random
40
38
from warnings import warn
41
39
 
42
 
from bzrlib import (
43
 
    errors,
44
 
    osutils,
45
 
    urlutils,
46
 
    )
47
 
from bzrlib.trace import mutter, warning
48
40
from bzrlib.transport import (
 
41
    Transport,
49
42
    Server,
50
43
    split_url,
51
 
    Transport,
52
44
    )
53
 
from bzrlib.transport.local import LocalURLServer
 
45
import bzrlib.errors as errors
 
46
from bzrlib.trace import mutter, warning
54
47
import bzrlib.ui
55
48
 
56
49
_have_medusa = False
129
122
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
130
123
        if self._port is not None:
131
124
            netloc = '%s:%d' % (netloc, self._port)
132
 
        proto = 'ftp'
133
 
        if self.is_active:
134
 
            proto = 'aftp'
135
 
        return urlparse.urlunparse((proto, netloc, path, '', '', ''))
 
125
        return urlparse.urlunparse(('ftp', netloc, path, '', '', ''))
136
126
 
137
127
    def _get_FTP(self):
138
128
        """Return the ftplib.FTP instance for this object."""
165
155
        if ('no such file' in s
166
156
            or 'could not open' in s
167
157
            or 'no such dir' in s
168
 
            or 'could not create file' in s # vsftpd
169
 
            or 'file doesn\'t exist' in s
170
158
            ):
171
159
            raise errors.NoSuchFile(path, extra=extra)
172
160
        if ('file exists' in s):
201
189
 
202
190
    def _abspath(self, relpath):
203
191
        assert isinstance(relpath, basestring)
204
 
        relpath = urlutils.unescape(relpath)
205
 
        if relpath.startswith('/'):
206
 
            basepath = []
207
 
        else:
208
 
            basepath = self._path.split('/')
 
192
        relpath = urllib.unquote(relpath)
 
193
        relpath_parts = relpath.split('/')
 
194
        if len(relpath_parts) > 1:
 
195
            if relpath_parts[0] == '':
 
196
                raise ValueError("path %r within branch %r seems to be absolute"
 
197
                                 % (relpath, self._path))
 
198
        basepath = self._path.split('/')
209
199
        if len(basepath) > 0 and basepath[-1] == '':
210
200
            basepath = basepath[:-1]
211
 
        for p in relpath.split('/'):
 
201
        for p in relpath_parts:
212
202
            if p == '..':
213
203
                if len(basepath) == 0:
214
204
                    # In most filesystems, a request for the parent
222
212
        # Possibly, we could use urlparse.urljoin() here, but
223
213
        # I'm concerned about when it chooses to strip the last
224
214
        # portion of the path, and when it doesn't.
225
 
 
226
 
        # XXX: It seems that ftplib does not handle Unicode paths
227
 
        # at the same time, medusa won't handle utf8 paths
228
 
        # So if we .encode(utf8) here, then we get a Server failure.
229
 
        # while if we use str(), we get a UnicodeError, and the test suite
230
 
        # just skips testing UnicodePaths.
231
 
        return str('/'.join(basepath) or '/')
 
215
        return '/'.join(basepath) or '/'
232
216
    
233
217
    def abspath(self, relpath):
234
218
        """Return the full url to the given relative path.
298
282
                self._FTP_instance = None
299
283
                return self.get(relpath, decode, retries+1)
300
284
 
301
 
    def put_file(self, relpath, fp, mode=None, retries=0):
 
285
    def put(self, relpath, fp, mode=None, retries=0):
302
286
        """Copy the file-like or string object into the location.
303
287
 
304
288
        :param relpath: Location to put the contents, relative to base.
306
290
        :param retries: Number of retries after temporary failures so far
307
291
                        for this operation.
308
292
 
309
 
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but
310
 
        ftplib does not
 
293
        TODO: jam 20051215 ftp as a protocol seems to support chmod, but ftplib does not
311
294
        """
312
295
        abspath = self._abspath(relpath)
313
296
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
314
297
                        os.getpid(), random.randint(0,0x7FFFFFFF))
315
 
        if getattr(fp, 'read', None) is None:
 
298
        if not hasattr(fp, 'read'):
316
299
            fp = StringIO(fp)
317
300
        try:
318
301
            mutter("FTP put: %s", abspath)
319
302
            f = self._get_FTP()
320
303
            try:
321
304
                f.storbinary('STOR '+tmp_abspath, fp)
322
 
                self._rename_and_overwrite(tmp_abspath, abspath, f)
 
305
                f.rename(tmp_abspath, abspath)
323
306
            except (ftplib.error_temp,EOFError), e:
324
307
                warning("Failure during ftp PUT. Deleting temporary file.")
325
308
                try:
330
313
                    raise e
331
314
                raise
332
315
        except ftplib.error_perm, e:
333
 
            self._translate_perm_error(e, abspath, extra='could not store',
334
 
                                       unknown_exc=errors.NoSuchFile)
 
316
            self._translate_perm_error(e, abspath, extra='could not store')
335
317
        except ftplib.error_temp, e:
336
318
            if retries > _number_of_retries:
337
319
                raise errors.TransportError("FTP temporary error during PUT %s. Aborting."
339
321
            else:
340
322
                warning("FTP temporary error: %s. Retrying.", str(e))
341
323
                self._FTP_instance = None
342
 
                self.put_file(relpath, fp, mode, retries+1)
 
324
                self.put(relpath, fp, mode, retries+1)
343
325
        except EOFError:
344
326
            if retries > _number_of_retries:
345
327
                raise errors.TransportError("FTP control connection closed during PUT %s."
348
330
                warning("FTP control connection closed. Trying to reopen.")
349
331
                time.sleep(_sleep_between_retries)
350
332
                self._FTP_instance = None
351
 
                self.put_file(relpath, fp, mode, retries+1)
 
333
                self.put(relpath, fp, mode, retries+1)
352
334
 
353
335
    def mkdir(self, relpath, mode=None):
354
336
        """Create a directory at the given path."""
371
353
        except ftplib.error_perm, e:
372
354
            self._translate_perm_error(e, abspath, unknown_exc=errors.PathError)
373
355
 
374
 
    def append_file(self, relpath, f, mode=None):
 
356
    def append(self, relpath, f, mode=None):
375
357
        """Append the text in the file-like object into the final
376
358
        location.
377
359
        """
439
421
    #       to give it its own address as the 'to' location.
440
422
    #       So implement a fancier 'copy()'
441
423
 
442
 
    def rename(self, rel_from, rel_to):
443
 
        abs_from = self._abspath(rel_from)
444
 
        abs_to = self._abspath(rel_to)
445
 
        mutter("FTP rename: %s => %s", abs_from, abs_to)
446
 
        f = self._get_FTP()
447
 
        return self._rename(abs_from, abs_to, f)
448
 
 
449
 
    def _rename(self, abs_from, abs_to, f):
450
 
        try:
451
 
            f.rename(abs_from, abs_to)
452
 
        except ftplib.error_perm, e:
453
 
            self._translate_perm_error(e, abs_from,
454
 
                ': unable to rename to %r' % (abs_to))
455
 
 
456
424
    def move(self, rel_from, rel_to):
457
425
        """Move the item at rel_from to the location at rel_to"""
458
426
        abs_from = self._abspath(rel_from)
460
428
        try:
461
429
            mutter("FTP mv: %s => %s", abs_from, abs_to)
462
430
            f = self._get_FTP()
463
 
            self._rename_and_overwrite(abs_from, abs_to, f)
 
431
            f.rename(abs_from, abs_to)
464
432
        except ftplib.error_perm, e:
465
433
            self._translate_perm_error(e, abs_from,
466
434
                extra='unable to rename to %r' % (rel_to,), 
467
435
                unknown_exc=errors.PathError)
468
436
 
469
 
    def _rename_and_overwrite(self, abs_from, abs_to, f):
470
 
        """Do a fancy rename on the remote server.
471
 
 
472
 
        Using the implementation provided by osutils.
473
 
        """
474
 
        osutils.fancy_rename(abs_from, abs_to,
475
 
            rename_func=lambda p1, p2: self._rename(p1, p2, f),
476
 
            unlink_func=lambda p: self._delete(p, f))
 
437
    rename = move
477
438
 
478
439
    def delete(self, relpath):
479
440
        """Delete the item at relpath"""
480
441
        abspath = self._abspath(relpath)
481
 
        f = self._get_FTP()
482
 
        self._delete(abspath, f)
483
 
 
484
 
    def _delete(self, abspath, f):
485
442
        try:
486
443
            mutter("FTP rm: %s", abspath)
 
444
            f = self._get_FTP()
487
445
            f.delete(abspath)
488
446
        except ftplib.error_perm, e:
489
447
            self._translate_perm_error(e, abspath, 'error deleting',
495
453
 
496
454
    def list_dir(self, relpath):
497
455
        """See Transport.list_dir."""
498
 
        basepath = self._abspath(relpath)
499
 
        mutter("FTP nlst: %s", basepath)
500
 
        f = self._get_FTP()
501
456
        try:
 
457
            mutter("FTP nlst: %s", self._abspath(relpath))
 
458
            f = self._get_FTP()
 
459
            basepath = self._abspath(relpath)
502
460
            paths = f.nlst(basepath)
 
461
            # If FTP.nlst returns paths prefixed by relpath, strip 'em
 
462
            if paths and paths[0].startswith(basepath):
 
463
                paths = [path[len(basepath)+1:] for path in paths]
 
464
            # Remove . and .. if present, and return
 
465
            return [path for path in paths if path not in (".", "..")]
503
466
        except ftplib.error_perm, e:
504
467
            self._translate_perm_error(e, relpath, extra='error with list_dir')
505
 
        # If FTP.nlst returns paths prefixed by relpath, strip 'em
506
 
        if paths and paths[0].startswith(basepath):
507
 
            entries = [path[len(basepath)+1:] for path in paths]
508
 
        else:
509
 
            entries = paths
510
 
        # Remove . and .. if present
511
 
        return [urlutils.escape(entry) for entry in entries
512
 
                if entry not in ('.', '..')]
513
468
 
514
469
    def iter_files_recursive(self):
515
470
        """See Transport.iter_files_recursive.
518
473
        mutter("FTP iter_files_recursive")
519
474
        queue = list(self.list_dir("."))
520
475
        while queue:
521
 
            relpath = queue.pop(0)
 
476
            relpath = urllib.quote(queue.pop(0))
522
477
            st = self.stat(relpath)
523
478
            if stat.S_ISDIR(st.st_mode):
524
479
                for i, basename in enumerate(self.list_dir(relpath)):
581
536
        """This is used by medusa.ftp_server to log connections, etc."""
582
537
        self.logs.append(message)
583
538
 
584
 
    def setUp(self, vfs_server=None):
 
539
    def setUp(self):
 
540
 
585
541
        if not _have_medusa:
586
542
            raise RuntimeError('Must have medusa to run the FtpServer')
587
543
 
588
 
        assert vfs_server is None or isinstance(vfs_server, LocalURLServer), \
589
 
            "FtpServer currently assumes local transport, got %s" % vfs_server
590
 
 
591
544
        self._root = os.getcwdu()
592
545
        self._ftp_server = _ftp_server(
593
546
            authorizer=_test_authorizer(root=self._root),
599
552
        self._port = self._ftp_server.getsockname()[1]
600
553
        # Don't let it loop forever, or handle an infinite number of requests.
601
554
        # In this case it will run for 100s, or 1000 requests
602
 
        self._async_thread = threading.Thread(
603
 
                target=FtpServer._asyncore_loop_ignore_EBADF,
 
555
        self._async_thread = threading.Thread(target=asyncore.loop,
604
556
                kwargs={'timeout':0.1, 'count':1000})
605
557
        self._async_thread.setDaemon(True)
606
558
        self._async_thread.start()
612
564
        asyncore.close_all()
613
565
        self._async_thread.join()
614
566
 
615
 
    @staticmethod
616
 
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
617
 
        """Ignore EBADF during server shutdown.
618
 
 
619
 
        We close the socket to get the server to shutdown, but this causes
620
 
        select.select() to raise EBADF.
621
 
        """
622
 
        try:
623
 
            asyncore.loop(*args, **kwargs)
624
 
        except select.error, e:
625
 
            if e.args[0] != errno.EBADF:
626
 
                raise
627
 
 
628
567
 
629
568
_ftp_channel = None
630
569
_ftp_server = None
695
634
            pfrom = self.filesystem.translate(self._renaming)
696
635
            self._renaming = None
697
636
            pto = self.filesystem.translate(line[1])
698
 
            if os.path.exists(pto):
699
 
                self.respond('550 RNTO failed: file exists')
700
 
                return
701
637
            try:
702
638
                os.rename(pfrom, pto)
703
639
            except (IOError, OSError), e:
704
640
                # TODO: jam 20060516 return custom responses based on
705
641
                #       why the command failed
706
 
                # (bialix 20070418) str(e) on Python 2.5 @ Windows
707
 
                # sometimes don't provide expected error message;
708
 
                # so we obtain such message via os.strerror()
709
 
                self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
 
642
                self.respond('550 RNTO failed: %s' % (e,))
710
643
            except:
711
644
                self.respond('550 RNTO failed')
712
645
                # For a test server, we will go ahead and just die
737
670
            *why* it cannot make a directory.
738
671
            """
739
672
            if len (line) != 2:
740
 
                self.command_not_understood(''.join(line))
 
673
                self.command_not_understood (string.join (line))
741
674
            else:
742
675
                path = line[1]
743
676
                try:
744
677
                    self.filesystem.mkdir (path)
745
678
                    self.respond ('257 MKD command successful.')
746
679
                except (IOError, OSError), e:
747
 
                    # (bialix 20070418) str(e) on Python 2.5 @ Windows
748
 
                    # sometimes don't provide expected error message;
749
 
                    # so we obtain such message via os.strerror()
750
 
                    self.respond ('550 error creating directory: %s' %
751
 
                                  os.strerror(e.errno))
 
680
                    self.respond ('550 error creating directory: %s' % (e,))
752
681
                except:
753
682
                    self.respond ('550 error creating directory.')
754
683