~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ftp.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-05-03 19:49:12 UTC
  • mfrom: (2476.1.1 shared_repo_layouts)
  • Revision ID: pqm@pqm.ubuntu.com-20070503194912-pzlcms91kk2uqfdo
(John Arbash Meinel) Add doc/shared_repository_layouts.txt

Show diffs side-by-side

added added

removed removed

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