Forráskód Böngészése

raise BackupOSError subclasses

Thomas Waldmann 1 éve
szülő
commit
97dd287584

+ 7 - 5
docs/internals/frontends.rst

@@ -713,15 +713,17 @@ Warnings
         {}: file changed while we backed it up
         {}: file changed while we backed it up
     IncludePatternNeverMatchedWarning rc: 101
     IncludePatternNeverMatchedWarning rc: 101
         Include pattern '{}' never matched.
         Include pattern '{}' never matched.
-    BackupWarning rc: 102
+    BackupError rc: 102
         {}: {}
         {}: {}
-    BackupOSWarning rc: 104
+    BackupRaceConditionError rc: 103
+        Error: {}
+    BackupOSError rc: 104
         {}: {}
         {}: {}
-    PermissionWarning rc: 105
+    BackupPermissionError rc: 105
         {}: {}
         {}: {}
-    IOWarning rc: 106
+    BackupIOError rc: 106
         {}: {}
         {}: {}
-    NotFoundWarning rc: 107
+    BackupFileNotFoundError rc: 107
         {}: {}
         {}: {}
 
 
 Operations
 Operations

+ 11 - 2
src/borg/archive.py

@@ -1,4 +1,5 @@
 import base64
 import base64
+import errno
 import json
 import json
 import os
 import os
 import stat
 import stat
@@ -26,7 +27,8 @@ from .crypto.key import key_factory, UnsupportedPayloadError
 from .compress import CompressionSpec
 from .compress import CompressionSpec
 from .constants import *  # NOQA
 from .constants import *  # NOQA
 from .crypto.low_level import IntegrityError as IntegrityErrorBase
 from .crypto.low_level import IntegrityError as IntegrityErrorBase
-from .helpers import BackupError, BackupOSError, BackupRaceConditionError
+from .helpers import BackupError, BackupRaceConditionError
+from .helpers import BackupOSError, BackupPermissionError, BackupFileNotFoundError, BackupIOError
 from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer
 from .hashindex import ChunkIndex, ChunkIndexEntry, CacheSynchronizer
 from .helpers import HardLinkManager
 from .helpers import HardLinkManager
 from .helpers import ChunkIteratorFileWrapper, open_item
 from .helpers import ChunkIteratorFileWrapper, open_item
@@ -194,7 +196,14 @@ class BackupIO:
 
 
     def __exit__(self, exc_type, exc_val, exc_tb):
     def __exit__(self, exc_type, exc_val, exc_tb):
         if exc_type and issubclass(exc_type, OSError):
         if exc_type and issubclass(exc_type, OSError):
-            raise BackupOSError(self.op, exc_val) from exc_val
+            E_MAP = {
+                errno.EPERM: BackupPermissionError,
+                errno.EACCES: BackupPermissionError,
+                errno.ENOENT: BackupFileNotFoundError,
+                errno.EIO: BackupIOError,
+            }
+            e_cls = E_MAP.get(exc_val.errno, BackupOSError)
+            raise e_cls(self.op, exc_val) from exc_val
 
 
 
 
 backup_io = BackupIO()
 backup_io = BackupIO()

+ 5 - 5
src/borg/archiver/__init__.py

@@ -26,7 +26,7 @@ try:
     from ..constants import *  # NOQA
     from ..constants import *  # NOQA
     from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE, classify_ec
     from ..helpers import EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, EXIT_SIGNAL_BASE, classify_ec
     from ..helpers import Error, CommandError, get_ec, modern_ec
     from ..helpers import Error, CommandError, get_ec, modern_ec
-    from ..helpers import add_warning, BorgWarning
+    from ..helpers import add_warning, BorgWarning, BackupWarning
     from ..helpers import format_file_size
     from ..helpers import format_file_size
     from ..helpers import remove_surrogates, text_to_json
     from ..helpers import remove_surrogates, text_to_json
     from ..helpers import DatetimeWrapper, replace_placeholders
     from ..helpers import DatetimeWrapper, replace_placeholders
@@ -144,10 +144,10 @@ class Archiver(
 
 
     def print_warning_instance(self, warning):
     def print_warning_instance(self, warning):
         assert isinstance(warning, BorgWarning)
         assert isinstance(warning, BorgWarning)
-        msg = type(warning).__doc__
-        msgid = type(warning).__qualname__
-        args = warning.args
-        self.print_warning(msg, *args, wc=warning.exit_code, wt="curly", msgid=msgid)
+        # if it is a BackupWarning, use the wrapped BackupError exception instance:
+        cls = type(warning.args[1]) if isinstance(warning, BackupWarning) else type(warning)
+        msg, msgid, args, wc = cls.__doc__, cls.__qualname__, warning.args, warning.exit_code
+        self.print_warning(msg, *args, wc=wc, wt="curly", msgid=msgid)
 
 
     def print_file_status(self, status, path):
     def print_file_status(self, status, path):
         # if we get called with status == None, the final file status was already printed
         # if we get called with status == None, the final file status was already printed

+ 7 - 13
src/borg/archiver/create_cmd.py

@@ -29,7 +29,7 @@ from ..helpers import prepare_subprocess_env
 from ..helpers import sig_int, ignore_sigint
 from ..helpers import sig_int, ignore_sigint
 from ..helpers import iter_separated
 from ..helpers import iter_separated
 from ..helpers import MakePathSafeAction
 from ..helpers import MakePathSafeAction
-from ..helpers import Error, CommandError, BackupWarning, BackupOSWarning, FileChangedWarning
+from ..helpers import Error, CommandError, BackupWarning, FileChangedWarning
 from ..manifest import Manifest
 from ..manifest import Manifest
 from ..patterns import PatternMatcher
 from ..patterns import PatternMatcher
 from ..platform import is_win32
 from ..platform import is_win32
@@ -87,7 +87,7 @@ class CreateMixIn:
                         rc = proc.wait()
                         rc = proc.wait()
                         if rc != 0:
                         if rc != 0:
                             raise CommandError("Command %r exited with status %d", args.paths[0], rc)
                             raise CommandError("Command %r exited with status %d", args.paths[0], rc)
-                    except BackupOSError as e:
+                    except BackupError as e:
                         raise Error("%s: %s", path, e)
                         raise Error("%s: %s", path, e)
                 else:
                 else:
                     status = "+"  # included
                     status = "+"  # included
@@ -121,9 +121,6 @@ class CreateMixIn:
                             read_special=args.read_special,
                             read_special=args.read_special,
                             dry_run=dry_run,
                             dry_run=dry_run,
                         )
                         )
-                    except BackupOSError as e:
-                        self.print_warning_instance(BackupOSWarning(path, e))
-                        status = "E"
                     except BackupError as e:
                     except BackupError as e:
                         self.print_warning_instance(BackupWarning(path, e))
                         self.print_warning_instance(BackupWarning(path, e))
                         status = "E"
                         status = "E"
@@ -151,8 +148,8 @@ class CreateMixIn:
                                 status = fso.process_pipe(
                                 status = fso.process_pipe(
                                     path=path, cache=cache, fd=sys.stdin.buffer, mode=mode, user=user, group=group
                                     path=path, cache=cache, fd=sys.stdin.buffer, mode=mode, user=user, group=group
                                 )
                                 )
-                            except BackupOSError as e:
-                                self.print_warning_instance(BackupOSWarning(path, e))
+                            except BackupError as e:
+                                self.print_warning_instance(BackupWarning(path, e))
                                 status = "E"
                                 status = "E"
                         else:
                         else:
                             status = "+"  # included
                             status = "+"  # included
@@ -183,9 +180,9 @@ class CreateMixIn:
                         # if we get back here, we've finished recursing into <path>,
                         # 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)
                         # 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))
                         skip_inodes.add((st.st_ino, st.st_dev))
-                    except BackupOSError as e:
+                    except BackupError as e:
                         # this comes from os.stat, self._rec_walk has own exception handler
                         # this comes from os.stat, self._rec_walk has own exception handler
-                        self.print_warning_instance(BackupOSWarning(path, e))
+                        self.print_warning_instance(BackupWarning(path, e))
                         continue
                         continue
             if not dry_run:
             if not dry_run:
                 if args.progress:
                 if args.progress:
@@ -368,7 +365,7 @@ class CreateMixIn:
                 else:
                 else:
                     self.print_warning("Unknown file type: %s", path)
                     self.print_warning("Unknown file type: %s", path)
                     return
                     return
-            except (BackupError, BackupOSError) as err:
+            except BackupError as err:
                 if isinstance(err, BackupOSError):
                 if isinstance(err, BackupOSError):
                     if err.errno in (errno.EPERM, errno.EACCES):
                     if err.errno in (errno.EPERM, errno.EACCES):
                         # Do not try again, such errors can not be fixed by retrying.
                         # Do not try again, such errors can not be fixed by retrying.
@@ -524,9 +521,6 @@ class CreateMixIn:
                                 dry_run=dry_run,
                                 dry_run=dry_run,
                             )
                             )
 
 
-        except BackupOSError as e:
-            self.print_warning_instance(BackupOSWarning(path, e))
-            status = "E"
         except BackupError as e:
         except BackupError as e:
             self.print_warning_instance(BackupWarning(path, e))
             self.print_warning_instance(BackupWarning(path, e))
             status = "E"
             status = "E"

+ 6 - 8
src/borg/archiver/extract_cmd.py

@@ -6,13 +6,13 @@ import stat
 
 
 from ._common import with_repository, with_archive
 from ._common import with_repository, with_archive
 from ._common import build_filter, build_matcher
 from ._common import build_filter, build_matcher
-from ..archive import BackupError, BackupOSError
+from ..archive import BackupError
 from ..constants import *  # NOQA
 from ..constants import *  # NOQA
 from ..helpers import archivename_validator, PathSpec
 from ..helpers import archivename_validator, PathSpec
 from ..helpers import remove_surrogates
 from ..helpers import remove_surrogates
 from ..helpers import HardLinkManager
 from ..helpers import HardLinkManager
 from ..helpers import ProgressIndicatorPercent
 from ..helpers import ProgressIndicatorPercent
-from ..helpers import BackupWarning, BackupOSWarning, IncludePatternNeverMatchedWarning
+from ..helpers import BackupWarning, IncludePatternNeverMatchedWarning
 from ..manifest import Manifest
 from ..manifest import Manifest
 
 
 from ..logger import create_logger
 from ..logger import create_logger
@@ -65,8 +65,8 @@ class ExtractMixIn:
                     dir_item = dirs.pop(-1)
                     dir_item = dirs.pop(-1)
                     try:
                     try:
                         archive.extract_item(dir_item, stdout=stdout)
                         archive.extract_item(dir_item, stdout=stdout)
-                    except BackupOSError as e:
-                        self.print_warning_instance(BackupOSWarning(remove_surrogates(dir_item.path), e))
+                    except BackupError as e:
+                        self.print_warning_instance(BackupWarning(remove_surrogates(dir_item.path), e))
             if output_list:
             if output_list:
                 logging.getLogger("borg.output.list").info(remove_surrogates(item.path))
                 logging.getLogger("borg.output.list").info(remove_surrogates(item.path))
             try:
             try:
@@ -80,8 +80,6 @@ class ExtractMixIn:
                         archive.extract_item(
                         archive.extract_item(
                             item, stdout=stdout, sparse=sparse, hlm=hlm, pi=pi, continue_extraction=continue_extraction
                             item, stdout=stdout, sparse=sparse, hlm=hlm, pi=pi, continue_extraction=continue_extraction
                         )
                         )
-            except BackupOSError as e:
-                self.print_warning_instance(BackupOSWarning(remove_surrogates(orig_path), e))
             except BackupError as e:
             except BackupError as e:
                 self.print_warning_instance(BackupWarning(remove_surrogates(orig_path), e))
                 self.print_warning_instance(BackupWarning(remove_surrogates(orig_path), e))
         if pi:
         if pi:
@@ -96,8 +94,8 @@ class ExtractMixIn:
                 dir_item = dirs.pop(-1)
                 dir_item = dirs.pop(-1)
                 try:
                 try:
                     archive.extract_item(dir_item, stdout=stdout)
                     archive.extract_item(dir_item, stdout=stdout)
-                except BackupOSError as e:
-                    self.print_warning_instance(BackupOSWarning(remove_surrogates(dir_item.path), e))
+                except BackupError as e:
+                    self.print_warning_instance(BackupWarning(remove_surrogates(dir_item.path), e))
         for pattern in matcher.get_unmatched_include_patterns():
         for pattern in matcher.get_unmatched_include_patterns():
             self.print_warning_instance(IncludePatternNeverMatchedWarning(pattern))
             self.print_warning_instance(IncludePatternNeverMatchedWarning(pattern))
         if pi:
         if pi:

+ 2 - 1
src/borg/helpers/__init__.py

@@ -13,8 +13,9 @@ from .checks import check_extension_modules, check_python
 from .datastruct import StableDict, Buffer, EfficientCollectionQueue
 from .datastruct import StableDict, Buffer, EfficientCollectionQueue
 from .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError, CancelledByUser, CommandError
 from .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError, CancelledByUser, CommandError
 from .errors import RTError, modern_ec
 from .errors import RTError, modern_ec
-from .errors import BorgWarning, FileChangedWarning, BackupWarning, BackupOSWarning, IncludePatternNeverMatchedWarning
+from .errors import BorgWarning, FileChangedWarning, BackupWarning, IncludePatternNeverMatchedWarning
 from .errors import BackupError, BackupOSError, BackupRaceConditionError
 from .errors import BackupError, BackupOSError, BackupRaceConditionError
+from .errors import BackupPermissionError, BackupIOError, BackupFileNotFoundError
 from .fs import ensure_dir, join_base_dir, get_socket_filename
 from .fs import ensure_dir, join_base_dir, get_socket_filename
 from .fs import get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir, get_runtime_dir
 from .fs import get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir, get_runtime_dir
 from .fs import dir_is_tagged, dir_is_cachedir, remove_dotdot_prefixes, make_path_safe, scandir_inorder
 from .fs import dir_is_tagged, dir_is_cachedir, remove_dotdot_prefixes, make_path_safe, scandir_inorder

+ 38 - 49
src/borg/helpers/errors.py

@@ -1,4 +1,3 @@
-import errno
 import os
 import os
 
 
 from ..constants import *  # NOQA
 from ..constants import *  # NOQA
@@ -112,70 +111,42 @@ class IncludePatternNeverMatchedWarning(BorgWarning):
 class BackupWarning(BorgWarning):
 class BackupWarning(BorgWarning):
     """{}: {}"""
     """{}: {}"""
 
 
-    exit_mcode = 102
-
-
-class BackupOSWarning(BorgWarning):
-    """{}: {}"""
-
-    exit_mcode = 104
+    # this is to wrap a caught BackupError exception, so it can be given to print_warning_instance
 
 
     @property
     @property
     def exit_code(self):
     def exit_code(self):
         if not modern_ec:
         if not modern_ec:
             return EXIT_WARNING
             return EXIT_WARNING
         exc = self.args[1]
         exc = self.args[1]
-        assert isinstance(exc, BackupOSError)
-        if exc.errno in (errno.EPERM, errno.EACCES):
-            return PermissionWarning.exit_mcode
-        elif exc.errno in (errno.ENOENT,):
-            return NotFoundWarning.exit_mcode
-        elif exc.errno in (errno.EIO,):
-            return IOWarning.exit_mcode
-        else:
-            return self.exit_mcode
-
-
-class PermissionWarning(BorgWarning):
-    """{}: {}"""
-
-    exit_mcode = 105
-
-
-class IOWarning(BorgWarning):
-    """{}: {}"""
-
-    exit_mcode = 106
+        assert isinstance(exc, BackupError)
+        return exc.exit_mcode
 
 
 
 
-class NotFoundWarning(BorgWarning):
-    """{}: {}"""
-
-    exit_mcode = 107
+class BackupError(Error):
+    """{}: backup error"""
 
 
-
-class BackupError(Exception):
-    """
-    Exception raised for non-OSError-based exceptions while accessing backup files.
-    """
+    # Exception raised for non-OSError-based exceptions while accessing backup files.
+    exit_mcode = 102
 
 
 
 
 class BackupRaceConditionError(BackupError):
 class BackupRaceConditionError(BackupError):
-    """
-    Exception raised when encountering a critical race condition while trying to back up a file.
-    """
+    """{}: file type or inode changed while we backed it up (race condition, skipped file)"""
 
 
+    # Exception raised when encountering a critical race condition while trying to back up a file.
+    exit_mcode = 103
 
 
-class BackupOSError(Exception):
-    """
-    Wrapper for OSError raised while accessing backup files.
 
 
-    Borg does different kinds of IO, and IO failures have different consequences.
-    This wrapper represents failures of input file or extraction IO.
-    These are non-critical and are only reported (exit code = 1, warning).
+class BackupOSError(BackupError):
+    """{}: {}"""
 
 
-    Any unwrapped IO error is critical and aborts execution (for example repository IO failure).
-    """
+    # Wrapper for OSError raised while accessing backup files.
+    #
+    # Borg does different kinds of IO, and IO failures have different consequences.
+    # This wrapper represents failures of input file or extraction IO.
+    # These are non-critical and are only reported (warnings).
+    #
+    # Any unwrapped IO error is critical and aborts execution (for example repository IO failure).
+    exit_mcode = 104
 
 
     def __init__(self, op, os_error):
     def __init__(self, op, os_error):
         self.op = op
         self.op = op
@@ -189,3 +160,21 @@ class BackupOSError(Exception):
             return f"{self.op}: {self.os_error}"
             return f"{self.op}: {self.os_error}"
         else:
         else:
             return str(self.os_error)
             return str(self.os_error)
+
+
+class BackupPermissionError(BackupOSError):
+    """{}: {}"""
+
+    exit_mcode = 105
+
+
+class BackupIOError(BackupOSError):
+    """{}: {}"""
+
+    exit_mcode = 106
+
+
+class BackupFileNotFoundError(BackupOSError):
+    """{}: {}"""
+
+    exit_mcode = 107