Repository: borgbackup/borg

Issue:
hardlink_master bug
BUG: borg 1.x does not set `item.hardlink_master = <boolean>` for fifos, block and char devices (but only for regular files).

note: it is not needed to be set for directories and symlinks, because they are not considered hard-linkable for borg1.

the borg2 upgrader code used for `borg transfer` used this up to 2.0.0b4:
```
    def borg1_hardlink_master(self, item):  # legacy
        return item.get("hardlink_master", True) and "source" not in item and self.borg1_hardlinkable(item.mode)
```
So:
- not hardlinked regular files: had `item.hardlink_master = False` set, ok
- hardlink master regular files: had `item.hardlink_master = True` set, ok
- fifos, block and char devices never had hard `.hardlink_master` set (neither True, nor False), so they **defaulted to True, which would be rather unpretty and unneeded in borg2 archives, because they all would get `item.hlid` set for no reason.** BUG
- hardlink slaves (all hardlinkable types) have `.source` set (although they are not symlinks), ok



Discussion:
Fix for borg 1.x:

Store hardlink_master also for fifos, block and char devices (see code for regular files).

Fix for borg2:
```
    def borg1_hardlink_master(self, item):  # legacy
        return item.get("hardlink_master", False) and "source" not in item and self.borg1_hardlinkable(item.mode)
```

For old borg 1.x archives with the bug, this will extract originally hardlinked fifos, block and char devices as individual (not hardlinked) files. Guess this would rarely be a problem, who hardlinks such stuff anyway?

For newer borg 1.x archives (made after fixing this) and also archives transferred to borg2, it will behave correctly for hardlinks of all file types.
Fix for master branch applied by c343673.

Resolving patch:
diff --git a/src/borg/archive.py b/src/borg/archive.py
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -1350,6 +1350,8 @@ def process_fifo(self, *, path, parent_fd, name, st):
             with OsOpen(path=path, parent_fd=parent_fd, name=name, flags=flags_normal, noatime=True) as fd:
                 with backup_io('fstat'):
                     st = stat_update_check(st, os.fstat(fd))
+                if not hardlinked or hardlink_master:
+                    item.hardlink_master = hardlinked
                 item.update(self.metadata_collector.stat_attrs(st, path, fd=fd))
                 return status
 
@@ -1359,6 +1361,8 @@ def process_dev(self, *, path, parent_fd, name, st, dev_type):
             with backup_io('stat'):
                 st = stat_update_check(st, os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False))
             item.rdev = st.st_rdev
+            if not hardlinked or hardlink_master:
+                item.hardlink_master = hardlinked
             item.update(self.metadata_collector.stat_attrs(st, path))
             return status
 
@@ -1395,6 +1399,7 @@ def process_pipe(self, *, path, cache, fd, mode, user, group):
         self.process_file_chunks(item, cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(fd)))
         item.get_size(memorize=True)
         self.stats.nfiles += 1
+        item.hardlink_master = False
         self.add_item(item, stats=self.stats)
         return status
 


Test patch: