mount: implement --numeric-owner (default: False!), fixes #2377

this is different default behaviour than in borg < 1.2:

default (numeric_owner=False) is to use the user/group name from the archive,
look up the local uid / gid and then use that for the FUSE fs.

when --numeric-owner is given (numeric_owner=True), then the uid/gid
from the archive is directly used (as it was the default behaviour in
borg < 1.2).

this was implemented like this (changing the default behaviour) to make
borg mount and borg extract behave more similar considering usage of
user/group numeric archived ids or archived names mapped to corresponding
numeric local system ids.

also, both now use the same function to get the uid/gid from the item.

fuse:
- add user and group name entries to default_dir
- also: set internal_dict(!) of new Item with data from Item.as_dict()
This commit is contained in:
Thomas Waldmann 2021-03-07 00:27:07 +01:00
parent 1ef7519823
commit bbccdbd81c
4 changed files with 101 additions and 12 deletions

View File

@ -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:

View File

@ -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')

View File

@ -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

View File

@ -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