Repository: borgbackup/borg

Issue:
borg 1.2.0a2 cannot create backup of "/"
## Have you checked borgbackup docs, FAQ, and open Github issues?
Yes – I was advised by TW to create this issue.

## Is this a BUG / ISSUE report or a QUESTION?
BUG

## System information. For client/server mode post info for both machines.

#### Your borg version (borg -V).
```borg 1.2.0a2```

#### Operating system (distribution) and version.
Debian Stretch

#### Hardware / network configuration, and filesystems used.
ext4

#### How much data is handled by borg?
~ 100 GB

#### Full borg commandline that lead to the problem (leave away excludes and passwords)
```borg create /repo::complete_manual_4 /```

## Describe the problem you're observing.
Borg outputs error message:
```/: [Errno 2] No such file or directory: ''```

#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.
YES

#### Include any warning/errors/backtraces from the system logs
none



Discussion:

Resolving patch:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -50,7 +50,7 @@
 from .helpers import safe_encode, remove_surrogates, bin_to_hex, prepare_dump_dict
 from .helpers import interval, prune_within, prune_split, PRUNING_PATTERNS
 from .helpers import timestamp
-from .helpers import get_cache_dir
+from .helpers import get_cache_dir, os_stat
 from .helpers import Manifest, AI_HUMAN_SORT_KEYS
 from .helpers import hardlinkable
 from .helpers import StableDict
@@ -486,9 +486,11 @@ def create_inner(archive, cache, fso):
                 path = os.path.normpath(path)
                 parent_dir = os.path.dirname(path) or '.'
                 name = os.path.basename(path)
+                # note: for path == '/':  name == '' and parent_dir == '/'.
+                # the empty name will trigger a fall-back to path-based processing in os_stat and os_open.
                 with OsOpen(path=parent_dir, flags=flags_root, noatime=True, op='open_root') as parent_fd:
                     try:
-                        st = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
+                        st = os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)
                     except OSError as e:
                         self.print_warning('%s: %s', path, e)
                         continue
@@ -565,7 +567,7 @@ def _process(self, *, path, parent_fd=None, name=None,
             recurse_excluded_dir = False
             if matcher.match(path):
                 with backup_io('stat'):
-                    st = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
+                    st = os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)
             else:
                 self.print_file_status('x', path)
                 # get out here as quickly as possible:
@@ -575,7 +577,7 @@ def _process(self, *, path, parent_fd=None, name=None,
                 if not matcher.recurse_dir:
                     return
                 with backup_io('stat'):
-                    st = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
+                    st = os_stat(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)
                 recurse_excluded_dir = stat.S_ISDIR(st.st_mode)
                 if not recurse_excluded_dir:
                     return
diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py
--- a/src/borg/helpers/fs.py
+++ b/src/borg/helpers/fs.py
@@ -224,7 +224,11 @@ def os_open(*, flags, path=None, parent_fd=None, name=None, noatime=False):
     :param noatime: True if access time shall be preserved
     :return: file descriptor
     """
-    fname = name if name is not None and parent_fd is not None else path
+    if name and parent_fd is not None:
+        # name is neither None nor empty, parent_fd given.
+        fname = name  # use name relative to parent_fd
+    else:
+        fname, parent_fd = path, None  # just use the path
     _flags_normal = flags
     if noatime:
         _flags_noatime = _flags_normal | O_('NOATIME')
@@ -242,6 +246,26 @@ def os_open(*, flags, path=None, parent_fd=None, name=None, noatime=False):
     return fd
 
 
+def os_stat(*, path=None, parent_fd=None, name=None, follow_symlinks=False):
+    """
+    Use os.stat to open a fs item.
+
+    If parent_fd and name are given, they are preferred and statat will be used,
+    path is not used in this case.
+
+    :param path: full (but not necessarily absolute) path
+    :param parent_fd: open directory file descriptor
+    :param name: name relative to parent_fd
+    :return: stat info
+    """
+    if name and parent_fd is not None:
+        # name is neither None nor empty, parent_fd given.
+        fname = name  # use name relative to parent_fd
+    else:
+        fname, parent_fd = path, None  # just use the path
+    return os.stat(fname, dir_fd=parent_fd, follow_symlinks=follow_symlinks)
+
+
 def umount(mountpoint):
     env = prepare_subprocess_env(system=True)
     try:


Test patch: