Rev 4702: (andrew) Implement home directory relative URLs for bzr:// and in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Fri Sep 18 09:02:44 BST 2009
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 4702 [merge]
revision-id: pqm at pqm.ubuntu.com-20090918080243-b04lrnure68z8rzc
parent: pqm at pqm.ubuntu.com-20090918033431-imjyd17yze1okeap
parent: andrew.bennetts at canonical.com-20090918071636-gxjzqyweltmxsm9b
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Fri 2009-09-18 09:02:43 +0100
message:
(andrew) Implement home directory relative URLs for bzr:// and
bzr+ssh://. (#109143)
added:
bzrlib/transport/pathfilter.py pathfilter.py-20090911074646-d3befzxpj1zglo7s-1
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/smart/server.py server.py-20061110062051-chzu10y32vx8gvur-1
bzrlib/tests/__init__.py selftest.py-20050531073622-8d0e3c8845c97a64
bzrlib/tests/blackbox/test_serve.py test_serve.py-20060913064329-8t2pvmsikl4s3xhl-1
bzrlib/tests/test_transport.py testtransport.py-20050718175618-e5cdb99f4555ddce
bzrlib/transport/__init__.py transport.py-20050711165921-4978aa7ce1285ad5
bzrlib/transport/chroot.py chroot.py-20061011104729-0us9mgm97z378vnt-1
doc/developers/network-protocol.txt networkprotocol.txt-20070903044232-woustorrjbmg5zol-1
doc/en/user-guide/server.txt server.txt-20060913044801-h939fvbwzz39gf7g-1
=== modified file 'NEWS'
--- a/NEWS 2009-09-17 22:48:24 +0000
+++ b/NEWS 2009-09-18 06:06:41 +0000
@@ -2,6 +2,16 @@
Bazaar Release Notes
####################
+bzr 2.0.1
+##########
+
+Bug Fixes
+*********
+
+* Make sure that we unlock the tree if we fail to create a TreeTransform
+ object when doing a merge, and there is limbo, or pending-deletions
+ directory. (Gary van der Merwe, #427773)
+
.. contents:: List of Releases
:depth: 1
@@ -15,6 +25,25 @@
New Features
************
+* ``bzr+ssh`` and ``bzr`` paths can now be relative to home directories
+ specified in the URL. Paths starting with a path segment of ``~`` are
+ relative to the home directory of the user running the server, and paths
+ starting with ``~user`` are relative to the home directory of the named
+ user. For example, for a user "bob" with a home directory of
+ ``/home/bob``, these URLs are all equivalent:
+
+ * ``bzr+ssh://bob@host/~/repo``
+ * ``bzr+ssh://bob@host/~bob/repo``
+ * ``bzr+ssh://bob@host/home/bob/repo``
+
+ If ``bzr serve`` was invoked with a ``--directory`` argument, then no
+ home directories outside that directory will be accessible via this
+ method.
+
+ This is a feature of ``bzr serve``, so pre-2.1 clients will
+ automatically benefit from this feature when ``bzr`` on the server is
+ upgraded. (Andrew Bennetts, #109143)
+
* Give more control on BZR_PLUGIN_PATH by providing a way to refer to or
disable the user, site and core plugin directories.
(Vincent Ladeuil, #412930, #316192, #145612)
=== modified file 'bzrlib/smart/server.py'
--- a/bzrlib/smart/server.py 2009-07-20 11:27:05 +0000
+++ b/bzrlib/smart/server.py 2009-09-18 05:45:06 +0000
@@ -17,6 +17,7 @@
"""Server for smart-server protocol."""
import errno
+import os.path
import socket
import sys
import threading
@@ -30,6 +31,14 @@
from bzrlib.lazy_import import lazy_import
lazy_import(globals(), """
from bzrlib.smart import medium
+from bzrlib.transport import (
+ chroot,
+ get_transport,
+ pathfilter,
+ )
+from bzrlib import (
+ urlutils,
+ )
""")
@@ -313,34 +322,132 @@
return transport.get_transport(url)
-def serve_bzr(transport, host=None, port=None, inet=False):
- from bzrlib import lockdir, ui
- from bzrlib.transport import get_transport
- from bzrlib.transport.chroot import ChrootServer
- chroot_server = ChrootServer(transport)
- chroot_server.setUp()
- transport = get_transport(chroot_server.get_url())
- if inet:
- smart_server = medium.SmartServerPipeStreamMedium(
- sys.stdin, sys.stdout, transport)
+def _local_path_for_transport(transport):
+ """Return a local path for transport, if reasonably possible.
+
+ This function works even if transport's url has a "readonly+" prefix,
+ unlike local_path_from_url.
+
+ This essentially recovers the --directory argument the user passed to "bzr
+ serve" from the transport passed to serve_bzr.
+ """
+ try:
+ base_url = transport.external_url()
+ except (errors.InProcessTransport, NotImplementedError):
+ return None
else:
- if host is None:
- host = medium.BZR_DEFAULT_INTERFACE
- if port is None:
- port = medium.BZR_DEFAULT_PORT
- smart_server = SmartTCPServer(transport, host=host, port=port)
- trace.note('listening on port: %s' % smart_server.port)
- # For the duration of this server, no UI output is permitted. note
- # that this may cause problems with blackbox tests. This should be
- # changed with care though, as we dont want to use bandwidth sending
- # progress over stderr to smart server clients!
- old_factory = ui.ui_factory
- old_lockdir_timeout = lockdir._DEFAULT_TIMEOUT_SECONDS
- try:
+ # Strip readonly prefix
+ if base_url.startswith('readonly+'):
+ base_url = base_url[len('readonly+'):]
+ try:
+ return urlutils.local_path_from_url(base_url)
+ except errors.InvalidURL:
+ return None
+
+
+class BzrServerFactory(object):
+ """Helper class for serve_bzr."""
+
+ def __init__(self, userdir_expander=None, get_base_path=None):
+ self.cleanups = []
+ self.base_path = None
+ self.backing_transport = None
+ if userdir_expander is None:
+ userdir_expander = os.path.expanduser
+ self.userdir_expander = userdir_expander
+ if get_base_path is None:
+ get_base_path = _local_path_for_transport
+ self.get_base_path = get_base_path
+
+ def _expand_userdirs(self, path):
+ """Translate /~/ or /~user/ to e.g. /home/foo, using
+ self.userdir_expander (os.path.expanduser by default).
+
+ If the translated path would fall outside base_path, or the path does
+ not start with ~, then no translation is applied.
+
+ If the path is inside, it is adjusted to be relative to the base path.
+
+ e.g. if base_path is /home, and the expanded path is /home/joe, then
+ the translated path is joe.
+ """
+ result = path
+ if path.startswith('~'):
+ expanded = self.userdir_expander(path)
+ if not expanded.endswith('/'):
+ expanded += '/'
+ if expanded.startswith(self.base_path):
+ result = expanded[len(self.base_path):]
+ return result
+
+ def _make_expand_userdirs_filter(self, transport):
+ return pathfilter.PathFilteringServer(transport, self._expand_userdirs)
+
+ def _make_backing_transport(self, transport):
+ """Chroot transport, and decorate with userdir expander."""
+ self.base_path = self.get_base_path(transport)
+ chroot_server = chroot.ChrootServer(transport)
+ chroot_server.setUp()
+ self.cleanups.append(chroot_server.tearDown)
+ transport = get_transport(chroot_server.get_url())
+ if self.base_path is not None:
+ # Decorate the server's backing transport with a filter that can
+ # expand homedirs.
+ expand_userdirs = self._make_expand_userdirs_filter(transport)
+ expand_userdirs.setUp()
+ self.cleanups.append(expand_userdirs.tearDown)
+ transport = get_transport(expand_userdirs.get_url())
+ self.transport = transport
+
+ def _make_smart_server(self, host, port, inet):
+ if inet:
+ smart_server = medium.SmartServerPipeStreamMedium(
+ sys.stdin, sys.stdout, self.transport)
+ else:
+ if host is None:
+ host = medium.BZR_DEFAULT_INTERFACE
+ if port is None:
+ port = medium.BZR_DEFAULT_PORT
+ smart_server = SmartTCPServer(self.transport, host=host, port=port)
+ trace.note('listening on port: %s' % smart_server.port)
+ self.smart_server = smart_server
+
+ def _change_globals(self):
+ from bzrlib import lockdir, ui
+ # For the duration of this server, no UI output is permitted. note
+ # that this may cause problems with blackbox tests. This should be
+ # changed with care though, as we dont want to use bandwidth sending
+ # progress over stderr to smart server clients!
+ old_factory = ui.ui_factory
+ old_lockdir_timeout = lockdir._DEFAULT_TIMEOUT_SECONDS
+ def restore_default_ui_factory_and_lockdir_timeout():
+ ui.ui_factory = old_factory
+ lockdir._DEFAULT_TIMEOUT_SECONDS = old_lockdir_timeout
+ self.cleanups.append(restore_default_ui_factory_and_lockdir_timeout)
ui.ui_factory = ui.SilentUIFactory()
lockdir._DEFAULT_TIMEOUT_SECONDS = 0
- smart_server.serve()
+
+ def set_up(self, transport, host, port, inet):
+ self._make_backing_transport(transport)
+ self._make_smart_server(host, port, inet)
+ self._change_globals()
+
+ def tear_down(self):
+ for cleanup in reversed(self.cleanups):
+ cleanup()
+
+
+def serve_bzr(transport, host=None, port=None, inet=False):
+ """This is the default implementation of 'bzr serve'.
+
+ It creates a TCP or pipe smart server on 'transport, and runs it. The
+ transport will be decorated with a chroot and pathfilter (using
+ os.path.expanduser).
+ """
+ bzr_server = BzrServerFactory()
+ try:
+ bzr_server.set_up(transport, host, port, inet)
+ bzr_server.smart_server.serve()
finally:
- ui.ui_factory = old_factory
- lockdir._DEFAULT_TIMEOUT_SECONDS = old_lockdir_timeout
+ bzr_server.tear_down()
=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py 2009-09-18 02:03:31 +0000
+++ b/bzrlib/tests/__init__.py 2009-09-18 06:06:41 +0000
@@ -91,7 +91,7 @@
deprecated_passed,
)
import bzrlib.trace
-from bzrlib.transport import chroot, get_transport
+from bzrlib.transport import get_transport, pathfilter
import bzrlib.transport
from bzrlib.transport.local import LocalURLServer
from bzrlib.transport.memory import MemoryServer
@@ -983,12 +983,11 @@
def _preopen_isolate_transport(self, transport):
"""Check that all transport openings are done in the test work area."""
- if isinstance(transport, chroot.ChrootTransport):
- # Unwrap chrooted transports
- url = transport.server.backing_transport.clone(
- transport._safe_relpath('.')).base
- else:
- url = transport.base
+ while isinstance(transport, pathfilter.PathFilteringTransport):
+ # Unwrap pathfiltered transports
+ transport = transport.server.backing_transport.clone(
+ transport._filter('.'))
+ url = transport.base
# ReadonlySmartTCPServer_for_testing decorates the backing transport
# urls it is given by prepending readonly+. This is appropriate as the
# client shouldn't know that the server is readonly (or not readonly).
=== modified file 'bzrlib/tests/blackbox/test_serve.py'
--- a/bzrlib/tests/blackbox/test_serve.py 2009-09-17 22:26:36 +0000
+++ b/bzrlib/tests/blackbox/test_serve.py 2009-09-18 06:06:41 +0000
@@ -18,6 +18,7 @@
"""Tests of the bzr serve command."""
import os
+import os.path
import signal
import subprocess
import sys
@@ -25,18 +26,21 @@
import threading
from bzrlib import (
- config,
+ builtins,
errors,
osutils,
revision as _mod_revision,
- transport,
)
from bzrlib.branch import Branch
from bzrlib.bzrdir import BzrDir
from bzrlib.errors import ParamikoNotPresent
from bzrlib.smart import client, medium
-from bzrlib.smart.server import SmartTCPServer
-from bzrlib.tests import TestCaseWithTransport, TestSkipped
+from bzrlib.smart.server import BzrServerFactory, SmartTCPServer
+from bzrlib.tests import (
+ TestCaseWithTransport,
+ TestCaseWithMemoryTransport,
+ TestSkipped,
+ )
from bzrlib.trace import mutter
from bzrlib.transport import get_transport, remote
@@ -327,4 +331,61 @@
client_medium.disconnect()
+class TestUserdirExpansion(TestCaseWithMemoryTransport):
+
+ def fake_expanduser(self, path):
+ """A simple, environment-independent, function for the duration of this
+ test.
+
+ Paths starting with a path segment of '~user' will expand to start with
+ '/home/user/'. Every other path will be unchanged.
+ """
+ if path.split('/', 1)[0] == '~user':
+ return '/home/user' + path[len('~user'):]
+ return path
+
+ def make_test_server(self, base_path='/'):
+ """Make and setUp a BzrServerFactory, backed by a memory transport, and
+ creat '/home/user' in that transport.
+ """
+ bzr_server = BzrServerFactory(
+ self.fake_expanduser, lambda t: base_path)
+ mem_transport = self.get_transport()
+ mem_transport.mkdir_multi(['home', 'home/user'])
+ bzr_server.set_up(mem_transport, None, None, inet=True)
+ self.addCleanup(bzr_server.tear_down)
+ return bzr_server
+
+ def test_bzr_serve_expands_userdir(self):
+ bzr_server = self.make_test_server()
+ self.assertTrue(bzr_server.smart_server.backing_transport.has('~user'))
+
+ def test_bzr_serve_does_not_expand_userdir_outside_base(self):
+ bzr_server = self.make_test_server('/foo')
+ self.assertFalse(bzr_server.smart_server.backing_transport.has('~user'))
+
+ def test_get_base_path(self):
+ """cmd_serve will turn the --directory option into a LocalTransport
+ (optionally decorated with 'readonly+'). BzrServerFactory can
+ determine the original --directory from that transport.
+ """
+ # Define a fake 'protocol' to capture the transport that cmd_serve
+ # passes to serve_bzr.
+ def capture_transport(transport, host, port, inet):
+ self.bzr_serve_transport = transport
+ cmd = builtins.cmd_serve()
+ # Read-only
+ cmd.run(directory='/a/b/c', protocol=capture_transport)
+ server_maker = BzrServerFactory()
+ self.assertEqual(
+ 'readonly+file:///a/b/c/', self.bzr_serve_transport.base)
+ self.assertEqual(
+ u'/a/b/c/', server_maker.get_base_path(self.bzr_serve_transport))
+ # Read-write
+ cmd.run(directory='/a/b/c', protocol=capture_transport,
+ allow_writes=True)
+ server_maker = BzrServerFactory()
+ self.assertEqual('file:///a/b/c/', self.bzr_serve_transport.base)
+ self.assertEqual(
+ u'/a/b/c/', server_maker.get_base_path(self.bzr_serve_transport))
=== modified file 'bzrlib/tests/test_transport.py'
--- a/bzrlib/tests/test_transport.py 2009-08-27 22:17:35 +0000
+++ b/bzrlib/tests/test_transport.py 2009-09-18 07:16:36 +0000
@@ -48,6 +48,7 @@
from bzrlib.transport.memory import MemoryTransport
from bzrlib.transport.local import (LocalTransport,
EmulatedWin32LocalTransport)
+from bzrlib.transport.pathfilter import PathFilteringServer
# TODO: Should possibly split transport-specific tests into their own files.
@@ -80,7 +81,8 @@
register_lazy_transport('bar', 'bzrlib.tests.test_transport',
'TestTransport.SampleHandler')
self.assertEqual([SampleHandler.__module__,
- 'bzrlib.transport.chroot'],
+ 'bzrlib.transport.chroot',
+ 'bzrlib.transport.pathfilter'],
_get_transport_modules())
finally:
_set_protocol_handlers(handlers)
@@ -446,6 +448,90 @@
server.tearDown()
+class PathFilteringDecoratorTransportTest(TestCase):
+ """Pathfilter decoration specific tests."""
+
+ def test_abspath(self):
+ # The abspath is always relative to the base of the backing transport.
+ server = PathFilteringServer(get_transport('memory:///foo/bar/'),
+ lambda x: x)
+ server.setUp()
+ transport = get_transport(server.get_url())
+ self.assertEqual(server.get_url(), transport.abspath('/'))
+
+ subdir_transport = transport.clone('subdir')
+ self.assertEqual(server.get_url(), subdir_transport.abspath('/'))
+ server.tearDown()
+
+ def make_pf_transport(self, filter_func=None):
+ """Make a PathFilteringTransport backed by a MemoryTransport.
+
+ :param filter_func: by default this will be a no-op function. Use this
+ parameter to override it."""
+ if filter_func is None:
+ filter_func = lambda x: x
+ server = PathFilteringServer(
+ get_transport('memory:///foo/bar/'), filter_func)
+ server.setUp()
+ self.addCleanup(server.tearDown)
+ return get_transport(server.get_url())
+
+ def test__filter(self):
+ # _filter (with an identity func as filter_func) always returns
+ # paths relative to the base of the backing transport.
+ transport = self.make_pf_transport()
+ self.assertEqual('foo', transport._filter('foo'))
+ self.assertEqual('foo/bar', transport._filter('foo/bar'))
+ self.assertEqual('', transport._filter('..'))
+ self.assertEqual('', transport._filter('/'))
+ # The base of the pathfiltering transport is taken into account too.
+ transport = transport.clone('subdir1/subdir2')
+ self.assertEqual('subdir1/subdir2/foo', transport._filter('foo'))
+ self.assertEqual(
+ 'subdir1/subdir2/foo/bar', transport._filter('foo/bar'))
+ self.assertEqual('subdir1', transport._filter('..'))
+ self.assertEqual('', transport._filter('/'))
+
+ def test_filter_invocation(self):
+ filter_log = []
+ def filter(path):
+ filter_log.append(path)
+ return path
+ transport = self.make_pf_transport(filter)
+ transport.has('abc')
+ self.assertEqual(['abc'], filter_log)
+ del filter_log[:]
+ transport.clone('abc').has('xyz')
+ self.assertEqual(['abc/xyz'], filter_log)
+ del filter_log[:]
+ transport.has('/abc')
+ self.assertEqual(['abc'], filter_log)
+
+ def test_clone(self):
+ transport = self.make_pf_transport()
+ # relpath from root and root path are the same
+ relpath_cloned = transport.clone('foo')
+ abspath_cloned = transport.clone('/foo')
+ self.assertEqual(transport.server, relpath_cloned.server)
+ self.assertEqual(transport.server, abspath_cloned.server)
+
+ def test_url_preserves_pathfiltering(self):
+ """Calling get_transport on a pathfiltered transport's base should
+ produce a transport with exactly the same behaviour as the original
+ pathfiltered transport.
+
+ This is so that it is not possible to escape (accidentally or
+ otherwise) the filtering by doing::
+ url = filtered_transport.base
+ parent_url = urlutils.join(url, '..')
+ new_transport = get_transport(parent_url)
+ """
+ transport = self.make_pf_transport()
+ new_transport = get_transport(transport.base)
+ self.assertEqual(transport.server, new_transport.server)
+ self.assertEqual(transport.base, new_transport.base)
+
+
class ReadonlyDecoratorTransportTest(TestCase):
"""Readonly decoration specific tests."""
=== modified file 'bzrlib/transport/__init__.py'
--- a/bzrlib/transport/__init__.py 2009-08-04 11:40:59 +0000
+++ b/bzrlib/transport/__init__.py 2009-09-14 05:39:09 +0000
@@ -90,8 +90,10 @@
modules.add(factory._module_name)
else:
modules.add(factory._obj.__module__)
- # Add chroot directly, because there is no handler registered for it.
+ # Add chroot and pathfilter directly, because there is no handler
+ # registered for it.
modules.add('bzrlib.transport.chroot')
+ modules.add('bzrlib.transport.pathfilter')
result = list(modules)
result.sort()
return result
=== modified file 'bzrlib/transport/chroot.py'
--- a/bzrlib/transport/chroot.py 2009-03-23 14:59:43 +0000
+++ b/bzrlib/transport/chroot.py 2009-09-16 04:37:33 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006 Canonical Ltd
+# Copyright (C) 2006-2009 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,21 +17,18 @@
"""Implementation of Transport that prevents access to locations above a set
root.
"""
-from urlparse import urlparse
-from bzrlib import errors, urlutils
from bzrlib.transport import (
get_transport,
+ pathfilter,
register_transport,
Server,
Transport,
unregister_transport,
)
-from bzrlib.transport.decorator import TransportDecorator, DecoratorServer
-from bzrlib.transport.memory import MemoryTransport
-
-
-class ChrootServer(Server):
+
+
+class ChrootServer(pathfilter.PathFilteringServer):
"""User space 'chroot' facility.
The server's get_url returns the url for a chroot transport mapped to the
@@ -39,126 +36,40 @@
directories of the backing transport are not visible. The chroot url will
not allow '..' sequences to result in requests to the chroot affecting
directories outside the backing transport.
+
+ PathFilteringServer does all the path sanitation needed to enforce a
+ chroot, so this is a simple subclass of PathFilteringServer that ignores
+ filter_func.
"""
def __init__(self, backing_transport):
- self.backing_transport = backing_transport
+ pathfilter.PathFilteringServer.__init__(self, backing_transport, None)
def _factory(self, url):
return ChrootTransport(self, url)
- def get_url(self):
- return self.scheme
-
def setUp(self):
self.scheme = 'chroot-%d:///' % id(self)
register_transport(self.scheme, self._factory)
- def tearDown(self):
- unregister_transport(self.scheme, self._factory)
-
-
-class ChrootTransport(Transport):
+
+class ChrootTransport(pathfilter.PathFilteringTransport):
"""A ChrootTransport.
Please see ChrootServer for details.
"""
- def __init__(self, server, base):
- self.server = server
- if not base.endswith('/'):
- base += '/'
- Transport.__init__(self, base)
- self.base_path = self.base[len(self.server.scheme)-1:]
- self.scheme = self.server.scheme
-
- def _call(self, methodname, relpath, *args):
- method = getattr(self.server.backing_transport, methodname)
- return method(self._safe_relpath(relpath), *args)
-
- def _safe_relpath(self, relpath):
- safe_relpath = self._combine_paths(self.base_path, relpath)
- if not safe_relpath.startswith('/'):
- raise ValueError(safe_relpath)
- return safe_relpath[1:]
-
- # Transport methods
- def abspath(self, relpath):
- return self.scheme + self._safe_relpath(relpath)
-
- def append_file(self, relpath, f, mode=None):
- return self._call('append_file', relpath, f, mode)
-
- def _can_roundtrip_unix_modebits(self):
- return self.server.backing_transport._can_roundtrip_unix_modebits()
-
- def clone(self, relpath):
- return ChrootTransport(self.server, self.abspath(relpath))
-
- def delete(self, relpath):
- return self._call('delete', relpath)
-
- def delete_tree(self, relpath):
- return self._call('delete_tree', relpath)
-
- def external_url(self):
- """See bzrlib.transport.Transport.external_url."""
- # Chroots, like MemoryTransport depend on in-process
- # state and thus the base cannot simply be handed out.
- # See the base class docstring for more details and
- # possible directions. For now we return the chrooted
- # url.
- return self.server.backing_transport.external_url()
-
- def get(self, relpath):
- return self._call('get', relpath)
-
- def has(self, relpath):
- return self._call('has', relpath)
-
- def is_readonly(self):
- return self.server.backing_transport.is_readonly()
-
- def iter_files_recursive(self):
- backing_transport = self.server.backing_transport.clone(
- self._safe_relpath('.'))
- return backing_transport.iter_files_recursive()
-
- def listable(self):
- return self.server.backing_transport.listable()
-
- def list_dir(self, relpath):
- return self._call('list_dir', relpath)
-
- def lock_read(self, relpath):
- return self._call('lock_read', relpath)
-
- def lock_write(self, relpath):
- return self._call('lock_write', relpath)
-
- def mkdir(self, relpath, mode=None):
- return self._call('mkdir', relpath, mode)
-
- def open_write_stream(self, relpath, mode=None):
- return self._call('open_write_stream', relpath, mode)
-
- def put_file(self, relpath, f, mode=None):
- return self._call('put_file', relpath, f, mode)
-
- def rename(self, rel_from, rel_to):
- return self._call('rename', rel_from, self._safe_relpath(rel_to))
-
- def rmdir(self, relpath):
- return self._call('rmdir', relpath)
-
- def stat(self, relpath):
- return self._call('stat', relpath)
+ def _filter(self, relpath):
+ # A simplified version of PathFilteringTransport's _filter that omits
+ # the call to self.server.filter_func.
+ return self._relpath_from_server_root(relpath)
class TestingChrootServer(ChrootServer):
def __init__(self):
"""TestingChrootServer is not usable until setUp is called."""
+ ChrootServer.__init__(self, None)
def setUp(self, backing_server=None):
"""Setup the Chroot on backing_server."""
@@ -171,5 +82,4 @@
def get_test_permutations():
"""Return the permutations to be used in testing."""
- return [(ChrootTransport, TestingChrootServer),
- ]
+ return [(ChrootTransport, TestingChrootServer)]
=== added file 'bzrlib/transport/pathfilter.py'
--- a/bzrlib/transport/pathfilter.py 1970-01-01 00:00:00 +0000
+++ b/bzrlib/transport/pathfilter.py 2009-09-16 05:00:54 +0000
@@ -0,0 +1,195 @@
+# Copyright (C) 2009 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""A transport decorator that filters all paths that are passed to it."""
+
+
+from bzrlib.transport import (
+ get_transport,
+ register_transport,
+ Server,
+ Transport,
+ unregister_transport,
+ )
+
+
+class PathFilteringServer(Server):
+ """Transport server for PathFilteringTransport.
+
+ It holds the backing_transport and filter_func for PathFilteringTransports.
+ All paths will be passed through filter_func before calling into the
+ backing_transport.
+
+ Note that paths returned from the backing transport are *not* altered in
+ anyway. So, depending on the filter_func, PathFilteringTransports might
+ not conform to the usual expectations of Transport behaviour; e.g. 'name'
+ in t.list_dir('dir') might not imply t.has('dir/name') is True! A filter
+ that merely prefixes a constant path segment will be essentially
+ transparent, whereas a filter that does rot13 to paths will break
+ expectations and probably cause confusing errors. So choose your
+ filter_func with care.
+ """
+
+ def __init__(self, backing_transport, filter_func):
+ """Constructor.
+
+ :param backing_transport: a transport
+ :param filter_func: a callable that takes paths, and translates them
+ into paths for use with the backing transport.
+ """
+ self.backing_transport = backing_transport
+ self.filter_func = filter_func
+
+ def _factory(self, url):
+ return PathFilteringTransport(self, url)
+
+ def get_url(self):
+ return self.scheme
+
+ def setUp(self):
+ self.scheme = 'filtered-%d:///' % id(self)
+ register_transport(self.scheme, self._factory)
+
+ def tearDown(self):
+ unregister_transport(self.scheme, self._factory)
+
+
+class PathFilteringTransport(Transport):
+ """A PathFilteringTransport.
+
+ Please see PathFilteringServer for details.
+ """
+
+ def __init__(self, server, base):
+ self.server = server
+ if not base.endswith('/'):
+ base += '/'
+ Transport.__init__(self, base)
+ self.base_path = self.base[len(self.server.scheme)-1:]
+ self.scheme = self.server.scheme
+
+ def _relpath_from_server_root(self, relpath):
+ unfiltered_path = self._combine_paths(self.base_path, relpath)
+ if not unfiltered_path.startswith('/'):
+ raise ValueError(unfiltered_path)
+ return unfiltered_path[1:]
+
+ def _filter(self, relpath):
+ return self.server.filter_func(self._relpath_from_server_root(relpath))
+
+ def _call(self, methodname, relpath, *args):
+ """Helper for Transport methods of the form:
+ operation(path, [other args ...])
+ """
+ backing_method = getattr(self.server.backing_transport, methodname)
+ return backing_method(self._filter(relpath), *args)
+
+ # Transport methods
+ def abspath(self, relpath):
+ # We do *not* want to filter at this point; e.g if the filter is
+ # homedir expansion, self.base == 'this:///' and relpath == '~/foo',
+ # then the abspath should be this:///~/foo (not this:///home/user/foo).
+ # Instead filtering should happen when self's base is passed to the
+ # backing.
+ return self.scheme + self._relpath_from_server_root(relpath)
+
+ def append_file(self, relpath, f, mode=None):
+ return self._call('append_file', relpath, f, mode)
+
+ def _can_roundtrip_unix_modebits(self):
+ return self.server.backing_transport._can_roundtrip_unix_modebits()
+
+ def clone(self, relpath):
+ return self.__class__(self.server, self.abspath(relpath))
+
+ def delete(self, relpath):
+ return self._call('delete', relpath)
+
+ def delete_tree(self, relpath):
+ return self._call('delete_tree', relpath)
+
+ def external_url(self):
+ """See bzrlib.transport.Transport.external_url."""
+ # PathFilteringTransports, like MemoryTransport, depend on in-process
+ # state and thus the base cannot simply be handed out. See the base
+ # class docstring for more details and possible directions. For now we
+ # return the path-filtered URL.
+ return self.server.backing_transport.external_url()
+
+ def get(self, relpath):
+ return self._call('get', relpath)
+
+ def has(self, relpath):
+ return self._call('has', relpath)
+
+ def is_readonly(self):
+ return self.server.backing_transport.is_readonly()
+
+ def iter_files_recursive(self):
+ backing_transport = self.server.backing_transport.clone(
+ self._filter('.'))
+ return backing_transport.iter_files_recursive()
+
+ def listable(self):
+ return self.server.backing_transport.listable()
+
+ def list_dir(self, relpath):
+ return self._call('list_dir', relpath)
+
+ def lock_read(self, relpath):
+ return self._call('lock_read', relpath)
+
+ def lock_write(self, relpath):
+ return self._call('lock_write', relpath)
+
+ def mkdir(self, relpath, mode=None):
+ return self._call('mkdir', relpath, mode)
+
+ def open_write_stream(self, relpath, mode=None):
+ return self._call('open_write_stream', relpath, mode)
+
+ def put_file(self, relpath, f, mode=None):
+ return self._call('put_file', relpath, f, mode)
+
+ def rename(self, rel_from, rel_to):
+ return self._call('rename', rel_from, self._filter(rel_to))
+
+ def rmdir(self, relpath):
+ return self._call('rmdir', relpath)
+
+ def stat(self, relpath):
+ return self._call('stat', relpath)
+
+
+class TestingPathFilteringServer(PathFilteringServer):
+
+ def __init__(self):
+ """TestingChrootServer is not usable until setUp is called."""
+
+ def setUp(self, backing_server=None):
+ """Setup the Chroot on backing_server."""
+ if backing_server is not None:
+ self.backing_transport = get_transport(backing_server.get_url())
+ else:
+ self.backing_transport = get_transport('.')
+ self.backing_transport.clone('added-by-filter').ensure_base()
+ self.filter_func = lambda x: 'added-by-filter/' + x
+ PathFilteringServer.setUp(self)
+
+
+def get_test_permutations():
+ """Return the permutations to be used in testing."""
+ return [(PathFilteringTransport, TestingPathFilteringServer)]
=== modified file 'doc/developers/network-protocol.txt'
--- a/doc/developers/network-protocol.txt 2009-04-04 02:50:01 +0000
+++ b/doc/developers/network-protocol.txt 2009-09-17 03:21:14 +0000
@@ -431,11 +431,10 @@
tricks). The default implementation in bzrlib does this using the
`bzrlib.transport.chroot` module.
-URLs that include ~ should probably be passed across to the server
-verbatim and the server can expand them. This will proably not be
-meaningful when limited to a directory? See `bug 109143`_.
-
-.. _bug 109143: https://bugs.launchpad.net/bzr/+bug/109143
+URLs that include ~ are passed across to the server verbatim and the
+server can expand them. The default implementation in bzrlib does this
+using `bzrlib.transport.pathfilter` and `os.path.expanduser`, taking care
+to respect the virtual root.
Requests
=== modified file 'doc/en/user-guide/server.txt'
--- a/doc/en/user-guide/server.txt 2009-02-22 16:54:02 +0000
+++ b/doc/en/user-guide/server.txt 2009-09-17 03:16:05 +0000
@@ -36,18 +36,24 @@
SSH
~~~
-Using Bazaar over SSH requires no special configuration on the server::
+Using Bazaar over SSH requires no special configuration on the server; so long
+as Bazaar is installed on the server you can use ``bzr+ssh`` URLs, e.g.::
+
+ bzr log bzr+ssh://host/path/to/branch
+
+If `bzr` is not installed system-wide on the server you may need to explicitly
+tell the local `bzr` where to find the remote `bzr`::
BZR_REMOTE_PATH=~/bin/bzr bzr log bzr+ssh://host/path/to/branch
The ``BZR_REMOTE_PATH`` environment variable adjusts how `bzr` will be
invoked on the remote system. By default, just `bzr` will be invoked,
-which requires the `bzr` executable to be on the default search path.
+which requires the `bzr` executable to be on the default search path. You can
+also set this permanently per-location in ``locations.conf``.
-The ``bzr+ssh://`` URL scheme only supports absolute paths from the
-root of the filesystem. Future versions are expected to support ``~``
-in the same way as ``sftp://`` URLs
-(https://bugs.launchpad.net/bzr/+bug/109143).
+Like SFTP, paths starting with ``~`` are relative to your home directory, e.g.
+``bzr+ssh://example.com/~/code/proj``. Additionally, paths starting with
+``~user`` will be relative to that user's home directory.
inetd
~~~~~
@@ -65,6 +71,10 @@
bzr log bzr://host/branchname
+If possible, paths starting with ``~`` and ``~user`` will be expanded as for
+``bzr+ssh``. Home directories outside the ``--directory`` specified to ``bzr
+serve`` will not be accessible.
+
Dedicated
~~~~~~~~~
More information about the bazaar-commits
mailing list