Repository: borgbackup/borg

Issue:
review metadata processing: empty/0 vs not knowing
the code could be reviewed whether this distinction is cleanly handled everywhere.

`borg create` has some switches like `--noacls`, `--noxattrs`, `--nobsdflags`, `--noatime` that can be used to not store such metadata into the archive.

as a consequence, the state represented by the archive says e.g. **"I do not know about ACLs"** which is a different thing than **"there were no ACLs"**.

similar situation exists for other metadata also, see the analysis below.

as we still have archives from the past, `borg transfer` time (when people upgrade to borg 2.0) would be a good opportunity to clean up the metadata according to the new rules. see #6993 for a related issue.


Discussion:

Resolving patch:
diff --git a/src/borg/archive.py b/src/borg/archive.py
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -920,11 +920,7 @@ def restore_attrs(self, path, item, symlink=False, fd=None):
                     if not symlink:
                         os.chmod(path, item.mode)
             mtime = item.mtime
-            if "atime" in item:
-                atime = item.atime
-            else:
-                # old archives only had mtime in item metadata
-                atime = mtime
+            atime = item.atime if "atime" in item else mtime
             if "birthtime" in item:
                 birthtime = item.birthtime
                 try:
@@ -947,10 +943,10 @@ def restore_attrs(self, path, item, symlink=False, fd=None):
                 pass
             if not self.noacls:
                 acl_set(path, item, self.numeric_ids, fd=fd)
-            if not self.noxattrs:
+            if not self.noxattrs and "xattrs" in item:
                 # chown removes Linux capabilities, so set the extended attributes at the end, after chown, since they include
                 # the Linux capabilities in the "security.capability" attribute.
-                warning = xattr.set_all(fd or path, item.get("xattrs", {}), follow_symlinks=False)
+                warning = xattr.set_all(fd or path, item.xattrs, follow_symlinks=False)
                 if warning:
                     set_ec(EXIT_WARNING)
             # bsdflags include the immutable flag and need to be set last:
@@ -1119,10 +1115,12 @@ def __init__(self, *, noatime, noctime, nobirthtime, numeric_ids, noflags, noacl
         self.nobirthtime = nobirthtime
 
     def stat_simple_attrs(self, st):
-        attrs = dict(mode=st.st_mode, uid=st.st_uid, gid=st.st_gid, mtime=safe_ns(st.st_mtime_ns))
+        attrs = {}
+        attrs["mode"] = st.st_mode
         # borg can work with archives only having mtime (very old borg archives do not have
         # atime/ctime). it can be useful to omit atime/ctime, if they change without the
         # file content changing - e.g. to get better metadata deduplication.
+        attrs["mtime"] = safe_ns(st.st_mtime_ns)
         if not self.noatime:
             attrs["atime"] = safe_ns(st.st_atime_ns)
         if not self.noctime:
@@ -1130,6 +1128,8 @@ def stat_simple_attrs(self, st):
         if not self.nobirthtime and hasattr(st, "st_birthtime"):
             # sadly, there's no stat_result.st_birthtime_ns
             attrs["birthtime"] = safe_ns(int(st.st_birthtime * 10**9))
+        attrs["uid"] = st.st_uid
+        attrs["gid"] = st.st_gid
         if not self.numeric_ids:
             user = uid2user(st.st_uid)
             if user is not None:
@@ -1144,13 +1144,11 @@ def stat_ext_attrs(self, st, path, fd=None):
         if not self.noflags:
             with backup_io("extended stat (flags)"):
                 flags = get_flags(path, st, fd=fd)
-            if flags:
-                attrs["bsdflags"] = flags
+            attrs["bsdflags"] = flags
         if not self.noxattrs:
             with backup_io("extended stat (xattrs)"):
                 xattrs = xattr.get_all(fd or path, follow_symlinks=False)
-            if xattrs:
-                attrs["xattrs"] = StableDict(xattrs)
+            attrs["xattrs"] = StableDict(xattrs)
         if not self.noacls:
             with backup_io("extended stat (ACLs)"):
                 acl_get(path, attrs, st, self.numeric_ids, fd=fd)
diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py
--- a/src/borg/helpers/parseformat.py
+++ b/src/borg/helpers/parseformat.py
@@ -834,7 +834,7 @@ def get_item_data(self, item):
         item_data["source"] = source
         item_data["linktarget"] = source
         item_data["hlid"] = hlid
-        item_data["flags"] = item.get("bsdflags", 0)
+        item_data["flags"] = item.get("bsdflags")  # int if flags known, else (if flags unknown) None
         for key in self.used_call_keys:
             item_data[key] = self.call_keys[key](item)
         return item_data
diff --git a/src/borg/upgrade.py b/src/borg/upgrade.py
--- a/src/borg/upgrade.py
+++ b/src/borg/upgrade.py
@@ -93,6 +93,11 @@ def upgrade_item(self, *, item):
         # - 'acl' remnants of bug in attic <= 0.13
         # - 'hardlink_master' (superseded by hlid)
         new_item_dict = {key: value for key, value in item.as_dict().items() if key in ITEM_KEY_WHITELIST}
+        # remove some pointless entries older borg put in there:
+        for key in "user", "group":
+            if key in new_item_dict and new_item_dict[key] is None:
+                del new_item_dict[key]
+        assert not any(value is None for value in new_item_dict.values()), f"found None value in {new_item_dict}"
         new_item = Item(internal_dict=new_item_dict)
         new_item.get_size(memorize=True)  # if not already present: compute+remember size for items with chunks
         assert all(key in new_item for key in REQUIRED_ITEM_KEYS)


Test patch: