Repository: borgbackup/borg

Issue:
borg create crashes if parent dir of recursion root can not be opened
<!--
Thank you for reporting an issue.

*IMPORTANT* -  *before* creating a new issue please look around:
 - Borgbackup documentation: http://borgbackup.readthedocs.io/en/stable/index.html
 - FAQ: https://borgbackup.readthedocs.io/en/stable/faq.html
 and
 - open issues in Github tracker: https://github.com/borgbackup/borg/issues
  
If you cannot find a similar problem, then create a new issue.

Please fill in as much of the template as possible.
-->

## Have you checked borgbackup docs, FAQ, and open Github issues?

Yes

## 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.0

#### Operating system (distribution) and version.
Linux xxxx 5.16.0-1-amd64 #1 SMP PREEMPT Debian 5.16.7-2 (2022-02-09) x86_64 GNU/Linux

#### Hardware / network configuration, and filesystems used.
ext4, nothing fancy

## Describe the problem you're observing.
After upgrading to 1.2 borg crashes when it cannot find file from the configuration. In 1.1.x version it skipped it with warning.

#### Can you reproduce the problem? If so, describe how. If not, describe troubleshooting steps you took before opening the issue.
Add non exisiting file to backup script.

#### Include any warning/errors/backtraces from the system logs

```
Local Exception
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/borg/archive.py", line 264, in OsOpen
    fd = os_open(path=path, parent_fd=parent_fd, name=name, flags=flags, noatime=noatime)
  File "/usr/lib/python3/dist-packages/borg/helpers/fs.py", line 290, in os_open
    fd = os.open(fname, _flags_noatime, dir_fd=parent_fd)
FileNotFoundError: [Errno 2] No such file or directory: '/etc/dnsmasq.d'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 5089, in main
    exit_code = archiver.run(args)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 5020, in run
    return set_ec(func(args))
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 183, in wrapper
    return method(self, args, repository=repository, **kwargs)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 668, in do_create
    create_inner(archive, cache, fso)
  File "/usr/lib/python3/dist-packages/borg/archiver.py", line 595, in create_inner
    with OsOpen(path=parent_dir, flags=flags_root, noatime=True, op='open_root') as parent_fd:
  File "/usr/lib/python3.9/contextlib.py", line 119, in __enter__
    return next(self.gen)
  File "/usr/lib/python3/dist-packages/borg/archive.py", line 264, in OsOpen
    fd = os_open(path=path, parent_fd=parent_fd, name=name, flags=flags, noatime=noatime)
  File "/usr/lib/python3/dist-packages/borg/archive.py", line 218, in __exit__
    raise BackupOSError(self.op, exc_val) from exc_val
borg.archive.BackupOSError: open_root: [Errno 2] No such file or directory: '/etc/dnsmasq.d'

Platform: Linux xxxxx 5.16.0-1-amd64 #1 SMP PREEMPT Debian 5.16.7-2 (2022-02-09) x86_64
Linux: Unknown Linux
Borg: 1.2.0  Python: CPython 3.9.10 msgpack: 1.0.3 fuse: pyfuse3 3.2.0 [pyfuse3,llfuse]
```


Discussion:
Please give the precise command that led to that traceback.
Standard `borg create`:
```
borg create                                             \
    --verbose                                           \
    --filter=AME                                        \
    --list                                              \
    --stats                                             \
    --show-rc                                           \
    --compression=lzma,9                                \
    --exclude 'lost+found/*'                            \
    --exclude '*.git/'                                  \
    --exclude '*~'                                      \
                                                        \
    ::'{hostname}-{now}'                                \
    ...                                                 \
    /etc/dnsmasq.d/`hostname`.conf                      \
    ...
```
And this computer don't have `/etc/dnsmasq.d/` currently, but I never modfied the backup script...

Resolving patch:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -590,26 +590,31 @@ 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(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)
-                        except OSError as e:
-                            self.print_warning('%s: %s', path, e)
-                            continue
-                        if args.one_file_system:
-                            restrict_dev = st.st_dev
-                        else:
-                            restrict_dev = None
-                        self._rec_walk(path=path, parent_fd=parent_fd, name=name,
-                                       fso=fso, cache=cache, matcher=matcher,
-                                       exclude_caches=args.exclude_caches, exclude_if_present=args.exclude_if_present,
-                                       keep_exclude_tags=args.keep_exclude_tags, skip_inodes=skip_inodes,
-                                       restrict_dev=restrict_dev, read_special=args.read_special, dry_run=dry_run)
-                        # if we get back here, we've finished recursing into <path>,
-                        # we do not ever want to get back in there (even if path is given twice as recursion root)
-                        skip_inodes.add((st.st_ino, st.st_dev))
+                    try:
+                        # 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(path=path, parent_fd=parent_fd, name=name, follow_symlinks=False)
+                            except OSError as e:
+                                self.print_warning('%s: %s', path, e)
+                                continue
+                            if args.one_file_system:
+                                restrict_dev = st.st_dev
+                            else:
+                                restrict_dev = None
+                            self._rec_walk(path=path, parent_fd=parent_fd, name=name,
+                                           fso=fso, cache=cache, matcher=matcher,
+                                           exclude_caches=args.exclude_caches, exclude_if_present=args.exclude_if_present,
+                                           keep_exclude_tags=args.keep_exclude_tags, skip_inodes=skip_inodes,
+                                           restrict_dev=restrict_dev, read_special=args.read_special, dry_run=dry_run)
+                            # if we get back here, we've finished recursing into <path>,
+                            # we do not ever want to get back in there (even if path is given twice as recursion root)
+                            skip_inodes.add((st.st_ino, st.st_dev))
+                    except (BackupOSError, BackupError) as e:
+                        # this comes from OsOpen, self._rec_walk has own exception handler
+                        self.print_warning('%s: %s', path, e)
+                        continue
             if not dry_run:
                 if args.progress:
                     archive.stats.show_progress(final=True)


Test patch: