浏览代码

Merge branch '1.0-maint'

Thomas Waldmann 8 年之前
父节点
当前提交
09e74af7bf

+ 15 - 9
Vagrantfile

@@ -25,6 +25,8 @@ def packages_debianoid
     # for building borgbackup and dependencies:
     # for building borgbackup and dependencies:
     apt-get install -y libssl-dev libacl1-dev liblz4-dev libfuse-dev fuse pkg-config
     apt-get install -y libssl-dev libacl1-dev liblz4-dev libfuse-dev fuse pkg-config
     usermod -a -G fuse $username
     usermod -a -G fuse $username
+    chgrp fuse /dev/fuse
+    chmod 666 /dev/fuse
     apt-get install -y fakeroot build-essential git
     apt-get install -y fakeroot build-essential git
     apt-get install -y python3-dev python3-setuptools
     apt-get install -y python3-dev python3-setuptools
     # for building python:
     # for building python:
@@ -45,6 +47,8 @@ def packages_redhatted
     # for building borgbackup and dependencies:
     # for building borgbackup and dependencies:
     yum install -y openssl-devel openssl libacl-devel libacl lz4-devel fuse-devel fuse pkgconfig
     yum install -y openssl-devel openssl libacl-devel libacl lz4-devel fuse-devel fuse pkgconfig
     usermod -a -G fuse vagrant
     usermod -a -G fuse vagrant
+    chgrp fuse /dev/fuse
+    chmod 666 /dev/fuse
     yum install -y fakeroot gcc git patch
     yum install -y fakeroot gcc git patch
     # needed to compile msgpack-python (otherwise it will use slow fallback code):
     # needed to compile msgpack-python (otherwise it will use slow fallback code):
     yum install -y gcc-c++
     yum install -y gcc-c++
@@ -96,6 +100,8 @@ def packages_freebsd
     kldload fuse
     kldload fuse
     sysctl vfs.usermount=1
     sysctl vfs.usermount=1
     pw groupmod operator -M vagrant
     pw groupmod operator -M vagrant
+    # /dev/fuse has group operator
+    chmod 666 /dev/fuse
     touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile
     touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile
     # install all the (security and other) updates, packages
     # install all the (security and other) updates, packages
     pkg update
     pkg update
@@ -106,10 +112,6 @@ end
 def packages_openbsd
 def packages_openbsd
   return <<-EOF
   return <<-EOF
     . ~/.profile
     . ~/.profile
-    mkdir -p /home/vagrant/borg
-    rsync -aH /vagrant/borg/ /home/vagrant/borg/
-    rm -rf /vagrant/borg
-    ln -sf /home/vagrant/borg /vagrant/
     pkg_add bash
     pkg_add bash
     chsh -s /usr/local/bin/bash vagrant
     chsh -s /usr/local/bin/bash vagrant
     pkg_add openssl
     pkg_add openssl
@@ -121,6 +123,8 @@ def packages_openbsd
     easy_install-3.4 pip
     easy_install-3.4 pip
     pip3 install virtualenv
     pip3 install virtualenv
     touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile
     touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile
+    # avoid that breaking llfuse install breaks borgbackup install under tox:
+    sed -i.bak '/fuse.txt/d' /vagrant/borg/borg/tox.ini
   EOF
   EOF
 end
 end
 
 
@@ -146,6 +150,8 @@ def packages_netbsd
     easy_install-3.4 pip
     easy_install-3.4 pip
     pip install virtualenv
     pip install virtualenv
     touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile
     touch ~vagrant/.bash_profile ; chown vagrant ~vagrant/.bash_profile
+    # fuse does not work good enough (see above), do not install llfuse:
+    sed -i.bak '/fuse.txt/d' /vagrant/borg/borg/tox.ini
   EOF
   EOF
 end
 end
 
 
@@ -273,13 +279,13 @@ def run_tests(boxname)
     . ~/.bash_profile
     . ~/.bash_profile
     cd /vagrant/borg/borg
     cd /vagrant/borg/borg
     . ../borg-env/bin/activate
     . ../borg-env/bin/activate
-    if which pyenv > /dev/null; then
+    if which pyenv 2> /dev/null; then
       # for testing, use the earliest point releases of the supported python versions:
       # for testing, use the earliest point releases of the supported python versions:
       pyenv global 3.4.0 3.5.0
       pyenv global 3.4.0 3.5.0
       pyenv local 3.4.0 3.5.0
       pyenv local 3.4.0 3.5.0
     fi
     fi
     # otherwise: just use the system python
     # otherwise: just use the system python
-    if which fakeroot > /dev/null; then
+    if which fakeroot 2> /dev/null; then
       echo "Running tox WITH fakeroot -u"
       echo "Running tox WITH fakeroot -u"
       fakeroot -u tox --skip-missing-interpreters
       fakeroot -u tox --skip-missing-interpreters
     else
     else
@@ -304,7 +310,7 @@ end
 
 
 Vagrant.configure(2) do |config|
 Vagrant.configure(2) do |config|
   # use rsync to copy content to the folder
   # use rsync to copy content to the folder
-  config.vm.synced_folder ".", "/vagrant/borg/borg", :type => "rsync", :rsync__args => ["--verbose", "--archive", "--delete", "-z"]
+  config.vm.synced_folder ".", "/vagrant/borg/borg", :type => "rsync", :rsync__args => ["--verbose", "--archive", "--delete", "-z"], :rsync__chown => false
   # do not let the VM access . on the host machine via the default shared folder!
   # do not let the VM access . on the host machine via the default shared folder!
   config.vm.synced_folder ".", "/vagrant", disabled: true
   config.vm.synced_folder ".", "/vagrant", disabled: true
 
 
@@ -443,7 +449,7 @@ Vagrant.configure(2) do |config|
   end
   end
 
 
   config.vm.define "openbsd64" do |b|
   config.vm.define "openbsd64" do |b|
-    b.vm.box = "kaorimatz/openbsd-5.9-amd64"
+    b.vm.box = "openbsd60-64"  # note: basic openbsd install for vagrant WITH sudo and rsync pre-installed
     b.vm.provider :virtualbox do |v|
     b.vm.provider :virtualbox do |v|
       v.memory = 768
       v.memory = 768
     end
     end
@@ -454,7 +460,7 @@ Vagrant.configure(2) do |config|
   end
   end
 
 
   config.vm.define "netbsd64" do |b|
   config.vm.define "netbsd64" do |b|
-    b.vm.box = "alex-skimlinks/netbsd-6.1.5-amd64"
+    b.vm.box = "netbsd70-64"
     b.vm.provider :virtualbox do |v|
     b.vm.provider :virtualbox do |v|
       v.memory = 768
       v.memory = 768
     end
     end

+ 4 - 0
docs/api.rst

@@ -26,6 +26,10 @@ API Documentation
     :members:
     :members:
     :undoc-members:
     :undoc-members:
 
 
+.. automodule:: borg.keymanager
+    :members:
+    :undoc-members:
+
 .. automodule:: borg.nonces
 .. automodule:: borg.nonces
     :members:
     :members:
     :undoc-members:
     :undoc-members:

+ 30 - 7
docs/changes.rst

@@ -218,8 +218,8 @@ Other changes:
   - ChunkBuffer: add test for leaving partial chunk in buffer, fixes #945
   - ChunkBuffer: add test for leaving partial chunk in buffer, fixes #945
 
 
 
 
-Version 1.0.8rc1 (not released yet)
------------------------------------
+Version 1.0.8rc1 (2016-10-17)
+-----------------------------
 
 
 Bug fixes:
 Bug fixes:
 
 
@@ -231,15 +231,22 @@ Bug fixes:
   also correctly processes broken symlinks. before this regressed to a crash
   also correctly processes broken symlinks. before this regressed to a crash
   (5b45385) a broken symlink would've been skipped.
   (5b45385) a broken symlink would've been skipped.
 - process_symlink: fix missing backup_io()
 - process_symlink: fix missing backup_io()
-  Fixes a chmod/chown/chgrp/unlink/rename/... crash race between getting dirents
-  and dispatching to process_symlink.
-- yes(): abort on wrong answers, saying so
+  Fixes a chmod/chown/chgrp/unlink/rename/... crash race between getting
+  dirents and dispatching to process_symlink.
+- yes(): abort on wrong answers, saying so, #1622
 - fixed exception borg serve raised when connection was closed before reposiory
 - fixed exception borg serve raised when connection was closed before reposiory
   was openend. add an error message for this.
   was openend. add an error message for this.
 - fix read-from-closed-FD issue, #1551
 - fix read-from-closed-FD issue, #1551
   (this seems not to get triggered in 1.0.x, but was discovered in master)
   (this seems not to get triggered in 1.0.x, but was discovered in master)
 - hashindex: fix iterators (always raise StopIteration when exhausted)
 - hashindex: fix iterators (always raise StopIteration when exhausted)
   (this seems not to get triggered in 1.0.x, but was discovered in master)
   (this seems not to get triggered in 1.0.x, but was discovered in master)
+- enable relative pathes in ssh:// repo URLs, via /./relpath hack, fixes #1655
+- allow repo pathes with colons, fixes #1705
+- update changed repo location immediately after acceptance, #1524
+- fix debug get-obj / delete-obj crash if object not found and remote repo,
+  #1684
+- pyinstaller: use a spec file to build borg.exe binary, exclude osxfuse dylib
+  on Mac OS X (avoids mismatch lib <-> driver), #1619
 
 
 New features:
 New features:
 
 
@@ -250,6 +257,8 @@ New features:
   special "paper" format with by line checksums for printed backups. For the
   special "paper" format with by line checksums for printed backups. For the
   paper format, the import is an interactive process which checks each line as
   paper format, the import is an interactive process which checks each line as
   soon as it is input.
   soon as it is input.
+- add "borg debug-refcount-obj" to determine a repo objects' referrer counts,
+  #1352
 
 
 Other changes:
 Other changes:
 
 
@@ -258,10 +267,19 @@ Other changes:
 - setup.py: Add subcommand support to build_usage.
 - setup.py: Add subcommand support to build_usage.
 - remote: change exception message for unexpected RPC data format to indicate
 - remote: change exception message for unexpected RPC data format to indicate
   dataflow direction.
   dataflow direction.
-- vagrant:
+- improved messages / error reporting:
+
+  - IntegrityError: add placeholder for message, so that the message we give
+    appears not only in the traceback, but also in the (short) error message,
+    #1572
+  - borg.key: include chunk id in exception msgs, #1571
+  - better messages for cache newer than repo, fixes #1700
+- vagrant (testing/build VMs):
 
 
   - upgrade OSXfuse / FUSE for macOS to 3.5.2
   - upgrade OSXfuse / FUSE for macOS to 3.5.2
-  - update Debian Wheezy boxes to 7.11
+  - update Debian Wheezy boxes, #1686
+  - openbsd / netbsd: use own boxes, fixes misc rsync installation and
+    fuse/llfuse related testing issues, #1695 #1696 #1670 #1671 #1728
 - docs:
 - docs:
 
 
   - add docs for "key export" and "key import" commands, #1641
   - add docs for "key export" and "key import" commands, #1641
@@ -277,12 +295,17 @@ Other changes:
   - add debug-info usage help file
   - add debug-info usage help file
   - internals.rst: fix typos
   - internals.rst: fix typos
   - setup.py: fix build_usage to always process all commands
   - setup.py: fix build_usage to always process all commands
+  - added docs explaining multiple --restrict-to-path flags, #1602
+  - add more specific warning about write-access debug commands, #1587
+  - clarify FAQ regarding backup of virtual machines, #1672
 - tests:
 - tests:
 
 
   - work around fuse xattr test issue with recent fakeroot
   - work around fuse xattr test issue with recent fakeroot
   - simplify repo/hashindex tests
   - simplify repo/hashindex tests
   - travis: test fuse-enabled borg, use trusty to have a recent FUSE
   - travis: test fuse-enabled borg, use trusty to have a recent FUSE
   - re-enable fuse tests for RemoteArchiver (no deadlocks any more)
   - re-enable fuse tests for RemoteArchiver (no deadlocks any more)
+  - clean env for pytest based tests, #1714
+  - fuse_mount contextmanager: accept any options
 
 
 
 
 Version 1.0.7 (2016-08-19)
 Version 1.0.7 (2016-08-19)

+ 2 - 2
docs/deployment.rst

@@ -149,10 +149,10 @@ package manager to install and keep borg up-to-date.
     - authorized_key: user="{{ user }}"
     - authorized_key: user="{{ user }}"
                       key="{{ item.key }}"
                       key="{{ item.key }}"
                       key_options='command="cd {{ pool }}/{{ item.host }};borg serve --restrict-to-path {{ pool }}/{{ item.host }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc'
                       key_options='command="cd {{ pool }}/{{ item.host }};borg serve --restrict-to-path {{ pool }}/{{ item.host }}",no-port-forwarding,no-X11-forwarding,no-pty,no-agent-forwarding,no-user-rc'
-      with_items: auth_users
+      with_items: "{{ auth_users }}"
     - file: path="{{ home }}/.ssh/authorized_keys" owner="{{ user }}" group="{{ group }}" mode=0600 state=file
     - file: path="{{ home }}/.ssh/authorized_keys" owner="{{ user }}" group="{{ group }}" mode=0600 state=file
     - file: path="{{ pool }}/{{ item.host }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory
     - file: path="{{ pool }}/{{ item.host }}" owner="{{ user }}" group="{{ group }}" mode=0700 state=directory
-      with_items: auth_users
+      with_items: "{{ auth_users }}"
 
 
 Salt
 Salt
 ----
 ----

+ 6 - 1
docs/usage.rst

@@ -897,9 +897,14 @@ That's all to it.
 Drawbacks
 Drawbacks
 +++++++++
 +++++++++
 
 
-As data is only appended, and nothing deleted, commands like ``prune`` or ``delete``
+As data is only appended, and nothing removed, commands like ``prune`` or ``delete``
 won't free disk space, they merely tag data as deleted in a new transaction.
 won't free disk space, they merely tag data as deleted in a new transaction.
 
 
+Be aware that as soon as you write to the repo in non-append-only mode (e.g. prune,
+delete or create archives from an admin machine), it will remove the deleted objects
+permanently (including the ones that were already marked as deleted, but not removed,
+in append-only mode).
+
 Note that you can go back-and-forth between normal and append-only operation by editing
 Note that you can go back-and-forth between normal and append-only operation by editing
 the configuration file, it's not a "one way trip".
 the configuration file, it's not a "one way trip".
 
 

+ 1 - 1
requirements.d/fuse.txt

@@ -1,4 +1,4 @@
 # low-level FUSE support library for "borg mount"
 # low-level FUSE support library for "borg mount"
-# see comments setup.py about this version requirement.
+# please see the comments in setup.py about llfuse.
 llfuse<2.0
 llfuse<2.0
 
 

+ 5 - 0
setup.py

@@ -23,12 +23,17 @@ on_rtd = os.environ.get('READTHEDOCS')
 # Also, we might use some rather recent API features.
 # Also, we might use some rather recent API features.
 install_requires = ['msgpack-python>=0.4.6', ]
 install_requires = ['msgpack-python>=0.4.6', ]
 
 
+# note for package maintainers: if you package borgbackup for distribution,
+# please add llfuse as a *requirement* on all platforms that have a working
+# llfuse package. "borg mount" needs llfuse to work.
+# if you do not have llfuse, do not require it, most of borgbackup will work.
 extras_require = {
 extras_require = {
     # llfuse 0.40 (tested, proven, ok), needs FUSE version >= 2.8.0
     # llfuse 0.40 (tested, proven, ok), needs FUSE version >= 2.8.0
     # llfuse 0.41 (tested shortly, looks ok), needs FUSE version >= 2.8.0
     # llfuse 0.41 (tested shortly, looks ok), needs FUSE version >= 2.8.0
     # llfuse 0.41.1 (tested shortly, looks ok), needs FUSE version >= 2.8.0
     # llfuse 0.41.1 (tested shortly, looks ok), needs FUSE version >= 2.8.0
     # llfuse 0.42 (tested shortly, looks ok), needs FUSE version >= 2.8.0
     # llfuse 0.42 (tested shortly, looks ok), needs FUSE version >= 2.8.0
     # llfuse 1.0 (tested shortly, looks ok), needs FUSE version >= 2.8.0
     # llfuse 1.0 (tested shortly, looks ok), needs FUSE version >= 2.8.0
+    # llfuse 1.1.1 (tested shortly, looks ok), needs FUSE version >= 2.8.0
     # llfuse 2.0 will break API
     # llfuse 2.0 will break API
     'fuse': ['llfuse<2.0', ],
     'fuse': ['llfuse<2.0', ],
 }
 }

+ 4 - 1
src/borg/__init__.py

@@ -1,3 +1,6 @@
-# This is a python package
+from distutils.version import LooseVersion
 
 
 from ._version import version as __version__
 from ._version import version as __version__
+
+
+__version_tuple__ = tuple(LooseVersion(__version__).version[:3])

+ 48 - 5
src/borg/archiver.py

@@ -1176,7 +1176,7 @@ class Archiver:
         else:
         else:
             try:
             try:
                 data = repository.get(id)
                 data = repository.get(id)
-            except repository.ObjectNotFound:
+            except Repository.ObjectNotFound:
                 print("object %s not found." % hex_id)
                 print("object %s not found." % hex_id)
             else:
             else:
                 with open(args.path, "wb") as f:
                 with open(args.path, "wb") as f:
@@ -1210,13 +1210,29 @@ class Archiver:
                     repository.delete(id)
                     repository.delete(id)
                     modified = True
                     modified = True
                     print("object %s deleted." % hex_id)
                     print("object %s deleted." % hex_id)
-                except repository.ObjectNotFound:
+                except Repository.ObjectNotFound:
                     print("object %s not found." % hex_id)
                     print("object %s not found." % hex_id)
         if modified:
         if modified:
             repository.commit()
             repository.commit()
         print('Done.')
         print('Done.')
         return EXIT_SUCCESS
         return EXIT_SUCCESS
 
 
+    @with_repository(manifest=False, exclusive=True, cache=True)
+    def do_debug_refcount_obj(self, args, repository, manifest, key, cache):
+        """display refcounts for the objects with the given IDs"""
+        for hex_id in args.ids:
+            try:
+                id = unhexlify(hex_id)
+            except ValueError:
+                print("object id %s is invalid." % hex_id)
+            else:
+                try:
+                    refcount = cache.chunks[id][0]
+                    print("object %s has %d referrers [info from chunks cache]." % (hex_id, refcount))
+                except KeyError:
+                    print("object %s not found [info from chunks cache]." % hex_id)
+        return EXIT_SUCCESS
+
     @with_repository(lock=False, manifest=False)
     @with_repository(lock=False, manifest=False)
     def do_break_lock(self, args, repository):
     def do_break_lock(self, args, repository):
         """Break the repository lock (e.g. in case it was left by a dead borg."""
         """Break the repository lock (e.g. in case it was left by a dead borg."""
@@ -1344,7 +1360,19 @@ class Archiver:
 
 
         {borgversion}
         {borgversion}
 
 
-            The version of borg.
+            The version of borg, e.g.: 1.0.8rc1
+
+        {borgmajor}
+
+            The version of borg, only the major version, e.g.: 1
+
+        {borgminor}
+
+            The version of borg, only major and minor version, e.g.: 1.0
+
+        {borgpatch}
+
+            The version of borg, only major, minor and patch version, e.g.: 1.0.8
 
 
         Examples::
         Examples::
 
 
@@ -1777,8 +1805,8 @@ class Archiver:
         '.checkpoint.N' (with N being a number), because these names are used for
         '.checkpoint.N' (with N being a number), because these names are used for
         checkpoints and treated in special ways.
         checkpoints and treated in special ways.
 
 
-        In the archive name, you may use the following format tags:
-        {now}, {utcnow}, {fqdn}, {hostname}, {user}, {pid}, {uuid4}, {borgversion}
+        In the archive name, you may use the following placeholders:
+        {now}, {utcnow}, {fqdn}, {hostname}, {user} and some others.
 
 
         To speed up pulling backups over sshfs and similar network file systems which do
         To speed up pulling backups over sshfs and similar network file systems which do
         not provide correct inode information the --ignore-inode flag can be used. This
         not provide correct inode information the --ignore-inode flag can be used. This
@@ -2541,6 +2569,21 @@ class Archiver:
         subparser.add_argument('ids', metavar='IDs', nargs='+', type=str,
         subparser.add_argument('ids', metavar='IDs', nargs='+', type=str,
                                help='hex object ID(s) to delete from the repo')
                                help='hex object ID(s) to delete from the repo')
 
 
+        debug_refcount_obj_epilog = textwrap.dedent("""
+        This command displays the reference count for objects from the repository.
+        """)
+        subparser = debug_parsers.add_parser('refcount-obj', parents=[common_parser], add_help=False,
+                                          description=self.do_debug_refcount_obj.__doc__,
+                                          epilog=debug_refcount_obj_epilog,
+                                          formatter_class=argparse.RawDescriptionHelpFormatter,
+                                          help='show refcount for object from repository (debug)')
+        subparser.set_defaults(func=self.do_debug_refcount_obj)
+        subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='',
+                               type=location_validator(archive=False),
+                               help='repository to use')
+        subparser.add_argument('ids', metavar='IDs', nargs='+', type=str,
+                               help='hex object ID(s) to show refcounts for')
+
         return parser
         return parser
 
 
     @staticmethod
     @staticmethod

+ 15 - 5
src/borg/helpers.py

@@ -34,6 +34,7 @@ from .logger import create_logger
 logger = create_logger()
 logger = create_logger()
 
 
 from . import __version__ as borg_version
 from . import __version__ as borg_version
+from . import __version_tuple__ as borg_version_tuple
 from . import chunker
 from . import chunker
 from . import crypto
 from . import crypto
 from . import hashindex
 from . import hashindex
@@ -664,6 +665,9 @@ def replace_placeholders(text):
         'user': uid2user(os.getuid(), os.getuid()),
         'user': uid2user(os.getuid(), os.getuid()),
         'uuid4': str(uuid.uuid4()),
         'uuid4': str(uuid.uuid4()),
         'borgversion': borg_version,
         'borgversion': borg_version,
+        'borgmajor': '%d' % borg_version_tuple[:1],
+        'borgminor': '%d.%d' % borg_version_tuple[:2],
+        'borgpatch': '%d.%d.%d' % borg_version_tuple[:3],
     }
     }
     return format_line(text, data)
     return format_line(text, data)
 
 
@@ -945,26 +949,32 @@ class Location:
         return True
         return True
 
 
     def _parse(self, text):
     def _parse(self, text):
+        def normpath_special(p):
+            # avoid that normpath strips away our relative path hack and even makes p absolute
+            relative = p.startswith('/./')
+            p = os.path.normpath(p)
+            return ('/.' + p) if relative else p
+
         m = self.ssh_re.match(text)
         m = self.ssh_re.match(text)
         if m:
         if m:
             self.proto = m.group('proto')
             self.proto = m.group('proto')
             self.user = m.group('user')
             self.user = m.group('user')
             self.host = m.group('host')
             self.host = m.group('host')
             self.port = m.group('port') and int(m.group('port')) or None
             self.port = m.group('port') and int(m.group('port')) or None
-            self.path = os.path.normpath(m.group('path'))
+            self.path = normpath_special(m.group('path'))
             self.archive = m.group('archive')
             self.archive = m.group('archive')
             return True
             return True
         m = self.file_re.match(text)
         m = self.file_re.match(text)
         if m:
         if m:
             self.proto = m.group('proto')
             self.proto = m.group('proto')
-            self.path = os.path.normpath(m.group('path'))
+            self.path = normpath_special(m.group('path'))
             self.archive = m.group('archive')
             self.archive = m.group('archive')
             return True
             return True
         m = self.scp_re.match(text)
         m = self.scp_re.match(text)
         if m:
         if m:
             self.user = m.group('user')
             self.user = m.group('user')
             self.host = m.group('host')
             self.host = m.group('host')
-            self.path = os.path.normpath(m.group('path'))
+            self.path = normpath_special(m.group('path'))
             self.archive = m.group('archive')
             self.archive = m.group('archive')
             self.proto = self.host and 'ssh' or 'file'
             self.proto = self.host and 'ssh' or 'file'
             return True
             return True
@@ -995,9 +1005,9 @@ class Location:
             return self.path
             return self.path
         else:
         else:
             if self.path and self.path.startswith('~'):
             if self.path and self.path.startswith('~'):
-                path = '/' + self.path
+                path = '/' + self.path  # /~/x = path x relative to home dir
             elif self.path and not self.path.startswith('/'):
             elif self.path and not self.path.startswith('/'):
-                path = '/~/' + self.path
+                path = '/./' + self.path  # /./x = path x relative to cwd
             else:
             else:
                 path = self.path
                 path = self.path
             return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '',
             return 'ssh://{}{}{}{}'.format('{}@'.format(self.user) if self.user else '',

+ 4 - 2
src/borg/remote.py

@@ -149,8 +149,10 @@ class RepositoryServer:  # pragma: no cover
 
 
     def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False):
     def open(self, path, create=False, lock_wait=None, lock=True, exclusive=None, append_only=False):
         path = os.fsdecode(path)
         path = os.fsdecode(path)
-        if path.startswith('/~'):
-            path = os.path.join(get_home_dir(), path[2:])
+        if path.startswith('/~'):  # /~/x = path x relative to home dir, /~username/x = relative to "user" home dir
+            path = os.path.join(get_home_dir(), path[2:])  # XXX check this (see also 1.0-maint), is it correct for ~u?
+        elif path.startswith('/./'):  # /./x = path x relative to cwd
+            path = path[3:]
         path = os.path.realpath(path)
         path = os.path.realpath(path)
         if self.restrict_to_paths:
         if self.restrict_to_paths:
             # if --restrict-to-path P is given, we make sure that we only operate in/below path P.
             # if --restrict-to-path P is given, we make sure that we only operate in/below path P.

+ 11 - 2
src/borg/testsuite/__init__.py

@@ -117,6 +117,15 @@ def is_utime_fully_supported():
         return False
         return False
 
 
 
 
+def no_selinux(x):
+    # selinux fails our FUSE tests, thus ignore selinux xattrs
+    SELINUX_KEY = 'security.selinux'
+    if isinstance(x, dict):
+        return {k: v for k, v in x.items() if k != SELINUX_KEY}
+    if isinstance(x, list):
+        return [k for k in x if k != SELINUX_KEY]
+
+
 class BaseTestCase(unittest.TestCase):
 class BaseTestCase(unittest.TestCase):
     """
     """
     """
     """
@@ -176,8 +185,8 @@ class BaseTestCase(unittest.TestCase):
                 else:
                 else:
                     d1.append(round(s1.st_mtime_ns, st_mtime_ns_round))
                     d1.append(round(s1.st_mtime_ns, st_mtime_ns_round))
                     d2.append(round(s2.st_mtime_ns, st_mtime_ns_round))
                     d2.append(round(s2.st_mtime_ns, st_mtime_ns_round))
-            d1.append(get_all(path1, follow_symlinks=False))
-            d2.append(get_all(path2, follow_symlinks=False))
+            d1.append(no_selinux(get_all(path1, follow_symlinks=False)))
+            d2.append(no_selinux(get_all(path2, follow_symlinks=False)))
             self.assert_equal(d1, d2)
             self.assert_equal(d1, d2)
         for sub_diff in diff.subdirs.values():
         for sub_diff in diff.subdirs.values():
             self._assert_dirs_equal_cmp(sub_diff)
             self._assert_dirs_equal_cmp(sub_diff)

+ 10 - 2
src/borg/testsuite/archiver.py

@@ -39,8 +39,10 @@ from ..keymanager import RepoIdMismatch, NotABorgKeyFile
 from ..remote import RemoteRepository, PathNotAllowed
 from ..remote import RemoteRepository, PathNotAllowed
 from ..repository import Repository
 from ..repository import Repository
 from . import has_lchflags, has_llfuse
 from . import has_lchflags, has_llfuse
-from . import BaseTestCase, changedir, environment_variable
+from . import BaseTestCase, changedir, environment_variable, no_selinux
 from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported
 from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported
+from .platform import fakeroot_detected
+
 
 
 src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 
 
@@ -1428,7 +1430,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             in_fn = 'input/fusexattr'
             in_fn = 'input/fusexattr'
             out_fn = os.path.join(mountpoint, 'input', 'fusexattr')
             out_fn = os.path.join(mountpoint, 'input', 'fusexattr')
             if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
             if not xattr.XATTR_FAKEROOT and xattr.is_enabled(self.input_path):
-                assert xattr.listxattr(out_fn) == ['user.foo', ]
+                assert no_selinux(xattr.listxattr(out_fn)) == ['user.foo', ]
                 assert xattr.getxattr(out_fn, 'user.foo') == b'bar'
                 assert xattr.getxattr(out_fn, 'user.foo') == b'bar'
             else:
             else:
                 assert xattr.listxattr(out_fn) == []
                 assert xattr.listxattr(out_fn) == []
@@ -1988,6 +1990,12 @@ class ArchiverTestCaseBinary(ArchiverTestCase):
     def test_overwrite(self):
     def test_overwrite(self):
         pass
         pass
 
 
+    def test_fuse(self):
+        if fakeroot_detected():
+            unittest.skip('test_fuse with the binary is not compatible with fakeroot')
+        else:
+            super().test_fuse()
+
 
 
 class ArchiverCheckTestCase(ArchiverTestCaseBase):
 class ArchiverCheckTestCase(ArchiverTestCaseBase):
 
 

+ 9 - 1
src/borg/testsuite/helpers.py

@@ -84,6 +84,8 @@ class TestLocationWithoutEnv:
             "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')"
             "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')"
         assert repr(Location('/some/absolute/path')) == \
         assert repr(Location('/some/absolute/path')) == \
             "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)"
             "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)"
+        assert repr(Location('ssh://user@host/some/path')) == \
+               "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
 
 
     def test_relpath(self, monkeypatch):
     def test_relpath(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
@@ -91,6 +93,12 @@ class TestLocationWithoutEnv:
             "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')"
             "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')"
         assert repr(Location('some/relative/path')) == \
         assert repr(Location('some/relative/path')) == \
             "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"
             "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"
+        assert repr(Location('ssh://user@host/./some/path')) == \
+               "Location(proto='ssh', user='user', host='host', port=None, path='/./some/path', archive=None)"
+        assert repr(Location('ssh://user@host/~/some/path')) == \
+               "Location(proto='ssh', user='user', host='host', port=None, path='/~/some/path', archive=None)"
+        assert repr(Location('ssh://user@host/~user/some/path')) == \
+               "Location(proto='ssh', user='user', host='host', port=None, path='/~user/some/path', archive=None)"
 
 
     def test_with_colons(self, monkeypatch):
     def test_with_colons(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)
@@ -122,7 +130,7 @@ class TestLocationWithoutEnv:
                      'ssh://user@host:1234/some/path::archive']
                      'ssh://user@host:1234/some/path::archive']
         for location in locations:
         for location in locations:
             assert Location(location).canonical_path() == \
             assert Location(location).canonical_path() == \
-                Location(Location(location).canonical_path()).canonical_path()
+                Location(Location(location).canonical_path()).canonical_path(), "failed: %s" % location
 
 
     def test_format_path(self, monkeypatch):
     def test_format_path(self, monkeypatch):
         monkeypatch.delenv('BORG_REPO', raising=False)
         monkeypatch.delenv('BORG_REPO', raising=False)