Browse Source

Merge pull request #5725 from ThomasWaldmann/mount-name-mapping

mount: implement --numeric-owner (default: False!)
TW 4 years ago
parent
commit
18b58cc701
4 changed files with 101 additions and 12 deletions
  1. 19 6
      src/borg/archive.py
  2. 2 0
      src/borg/archiver.py
  3. 13 5
      src/borg/fuse.py
  4. 67 1
      src/borg/testsuite/archive.py

+ 19 - 6
src/borg/archive.py

@@ -367,6 +367,24 @@ class CacheChunkBuffer(ChunkBuffer):
         return id_
 
 
+def get_item_uid_gid(item, *, numeric, uid_forced=None, gid_forced=None, uid_default=0, gid_default=0):
+    if uid_forced is not None:
+        uid = uid_forced
+    else:
+        uid = None if numeric else user2uid(item.user)
+        uid = item.uid if uid is None else uid
+        if uid < 0:
+            uid = uid_default
+    if gid_forced is not None:
+        gid = gid_forced
+    else:
+        gid = None if numeric else group2gid(item.group)
+        gid = item.gid if gid is None else gid
+        if gid < 0:
+            gid = gid_default
+    return uid, gid
+
+
 class Archive:
 
     class DoesNotExist(Error):
@@ -809,12 +827,7 @@ Utilization of max. archive size: {csize_max:.0%}
         Does not access the repository.
         """
         backup_io.op = 'attrs'
-        uid = gid = None
-        if not self.numeric_owner:
-            uid = user2uid(item.user)
-            gid = group2gid(item.group)
-        uid = item.uid if uid is None else uid
-        gid = item.gid if gid is None else gid
+        uid, gid = get_item_uid_gid(item, numeric=self.numeric_owner)
         # This code is a bit of a mess due to os specific differences
         if not is_win32:
             try:

+ 2 - 0
src/borg/archiver.py

@@ -2822,6 +2822,8 @@ class Archiver:
                                 help='stay in foreground, do not daemonize')
             parser.add_argument('-o', dest='options', type=str,
                                 help='Extra mount options')
+            parser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true',
+                                  help='use numeric user and group identifiers from archive(s)')
             define_archive_filters_group(parser)
             parser.add_argument('paths', metavar='PATH', nargs='*', type=str,
                                    help='paths to extract; patterns are supported')

+ 13 - 5
src/borg/fuse.py

@@ -33,12 +33,13 @@ logger = create_logger()
 
 from .crypto.low_level import blake2b_128
 from .archiver import Archiver
-from .archive import Archive
+from .archive import Archive, get_item_uid_gid
 from .hashindex import FuseVersionsIndex
 from .helpers import daemonize, daemonizing, hardlinkable, signal_handler, format_file_size
 from .helpers import msgpack
 from .item import Item
 from .lrucache import LRUCache
+from .platform import uid2user, gid2group
 from .remote import RemoteRepository
 
 
@@ -240,6 +241,7 @@ class FuseBackend(object):
     def __init__(self, key, manifest, repository, args, decrypted_repository):
         self.repository_uncached = repository
         self._args = args
+        self.numeric_owner = args.numeric_owner
         self._manifest = manifest
         self.key = key
         # Maps inode numbers to Item instances. This is used for synthetic inodes, i.e. file-system objects that are
@@ -311,7 +313,7 @@ class FuseBackend(object):
         """
         ino = self._allocate_inode()
         if mtime is not None:
-            self._items[ino] = Item(**self.default_dir.as_dict())
+            self._items[ino] = Item(internal_dict=self.default_dir.as_dict())
             self._items[ino].mtime = mtime
         else:
             self._items[ino] = self.default_dir
@@ -530,8 +532,13 @@ class FuseOperations(llfuse.Operations, FuseBackend):
         self.umask = pop_option(options, 'umask', 0, 0, int, int_base=8)  # umask is octal, e.g. 222 or 0222
         dir_uid = self.uid_forced if self.uid_forced is not None else self.default_uid
         dir_gid = self.gid_forced if self.gid_forced is not None else self.default_gid
+        dir_user = uid2user(dir_uid)
+        dir_group = gid2group(dir_gid)
+        assert isinstance(dir_user, str)
+        assert isinstance(dir_group, str)
         dir_mode = 0o40755 & ~self.umask
-        self.default_dir = Item(mode=dir_mode, mtime=int(time.time() * 1e9), uid=dir_uid, gid=dir_gid)
+        self.default_dir = Item(mode=dir_mode, mtime=int(time.time() * 1e9),
+                                user=dir_user, group=dir_group, uid=dir_uid, gid=dir_gid)
         self._create_filesystem()
         llfuse.init(self, mountpoint, options)
         if not foreground:
@@ -581,8 +588,9 @@ class FuseOperations(llfuse.Operations, FuseBackend):
         entry.attr_timeout = 300
         entry.st_mode = item.mode & ~self.umask
         entry.st_nlink = item.get('nlink', 1)
-        entry.st_uid = self.uid_forced if self.uid_forced is not None else item.uid if item.uid >= 0 else self.default_uid
-        entry.st_gid = self.gid_forced if self.gid_forced is not None else item.gid if item.gid >= 0 else self.default_gid
+        entry.st_uid, entry.st_gid = get_item_uid_gid(item, numeric=self.numeric_owner,
+                                                      uid_default=self.default_uid, gid_default=self.default_gid,
+                                                      uid_forced=self.uid_forced, gid_forced=self.gid_forced)
         entry.st_rdev = item.get('rdev', 0)
         entry.st_size = item.get_size()
         entry.st_blksize = 512

+ 67 - 1
src/borg/testsuite/archive.py

@@ -8,10 +8,11 @@ import pytest
 from . import BaseTestCase
 from ..crypto.key import PlaintextKey
 from ..archive import Archive, CacheChunkBuffer, RobustUnpacker, valid_msgpacked_dict, ITEM_KEYS, Statistics
-from ..archive import BackupOSError, backup_io, backup_io_iter
+from ..archive import BackupOSError, backup_io, backup_io_iter, get_item_uid_gid
 from ..helpers import Manifest
 from ..helpers import msgpack
 from ..item import Item, ArchiveItem
+from ..platform import uid2user, gid2group
 
 
 @pytest.fixture()
@@ -249,3 +250,68 @@ def test_backup_io_iter():
     normal_iterator = Iterator(StopIteration)
     for _ in backup_io_iter(normal_iterator):
         assert False, 'StopIteration handled incorrectly'
+
+
+def test_get_item_uid_gid():
+    # test requires that:
+    # - a name for user 0 and group 0 exists, usually root:root or root:wheel.
+    # - a system user/group udoesnotexist:gdoesnotexist does NOT exist.
+
+    user0, group0 = uid2user(0), gid2group(0)
+
+    # this is intentionally a "strange" item, with not matching ids/names.
+    item = Item(path='filename', uid=1, gid=2, user=user0, group=group0)
+
+    uid, gid = get_item_uid_gid(item, numeric=False)
+    # these are found via a name-to-id lookup
+    assert uid == 0
+    assert gid == 0
+
+    uid, gid = get_item_uid_gid(item, numeric=True)
+    # these are directly taken from the item.uid and .gid
+    assert uid == 1
+    assert gid == 2
+
+    uid, gid = get_item_uid_gid(item, numeric=False, uid_forced=3, gid_forced=4)
+    # these are enforced (not from item metadata)
+    assert uid == 3
+    assert gid == 4
+
+    # item metadata broken, has negative ids.
+    item = Item(path='filename', uid=-1, gid=-2, user=user0, group=group0)
+
+    uid, gid = get_item_uid_gid(item, numeric=True)
+    # use the uid/gid defaults (which both default to 0).
+    assert uid == 0
+    assert gid == 0
+
+    uid, gid = get_item_uid_gid(item, numeric=True, uid_default=5, gid_default=6)
+    # use the uid/gid defaults (as given).
+    assert uid == 5
+    assert gid == 6
+
+    # item metadata broken, has negative ids and non-existing user/group names.
+    item = Item(path='filename', uid=-3, gid=-4, user='udoesnotexist', group='gdoesnotexist')
+
+    uid, gid = get_item_uid_gid(item, numeric=False)
+    # use the uid/gid defaults (which both default to 0).
+    assert uid == 0
+    assert gid == 0
+
+    uid, gid = get_item_uid_gid(item, numeric=True, uid_default=7, gid_default=8)
+    # use the uid/gid defaults (as given).
+    assert uid == 7
+    assert gid == 8
+
+    # item metadata has valid uid/gid, but non-existing user/group names.
+    item = Item(path='filename', uid=9, gid=10, user='udoesnotexist', group='gdoesnotexist')
+
+    uid, gid = get_item_uid_gid(item, numeric=False)
+    # because user/group name does not exist here, use valid numeric ids from item metadata.
+    assert uid == 9
+    assert gid == 10
+
+    uid, gid = get_item_uid_gid(item, numeric=False, uid_default=11, gid_default=12)
+    # because item uid/gid seems valid, do not use the given uid/gid defaults
+    assert uid == 9
+    assert gid == 10