diff --git a/docs/internals/frontends.rst b/docs/internals/frontends.rst index 2003bad3..777a4964 100644 --- a/docs/internals/frontends.rst +++ b/docs/internals/frontends.rst @@ -713,15 +713,17 @@ Warnings {}: file changed while we backed it up IncludePatternNeverMatchedWarning rc: 101 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 diff --git a/src/borg/archive.py b/src/borg/archive.py index e8ce53fc..d4449ec4 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1,4 +1,5 @@ import base64 +import errno import json import os import stat @@ -26,7 +27,8 @@ from .crypto.key import key_factory, UnsupportedPayloadError from .compress import CompressionSpec from .constants import * # NOQA 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 .helpers import HardLinkManager from .helpers import ChunkIteratorFileWrapper, open_item @@ -194,7 +196,14 @@ class BackupIO: def __exit__(self, exc_type, exc_val, exc_tb): 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() diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 20abf5ef..d05ba9b5 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -26,7 +26,7 @@ try: from ..constants import * # NOQA 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 add_warning, BorgWarning + from ..helpers import add_warning, BorgWarning, BackupWarning from ..helpers import format_file_size from ..helpers import remove_surrogates, text_to_json from ..helpers import DatetimeWrapper, replace_placeholders @@ -144,10 +144,10 @@ class Archiver( def print_warning_instance(self, warning): 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): # if we get called with status == None, the final file status was already printed diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 0dfaee95..67fe121d 100644 --- a/src/borg/archiver/create_cmd.py +++ b/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 iter_separated from ..helpers import MakePathSafeAction -from ..helpers import Error, CommandError, BackupWarning, BackupOSWarning, FileChangedWarning +from ..helpers import Error, CommandError, BackupWarning, FileChangedWarning from ..manifest import Manifest from ..patterns import PatternMatcher from ..platform import is_win32 @@ -87,7 +87,7 @@ class CreateMixIn: rc = proc.wait() if rc != 0: 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) else: status = "+" # included @@ -121,9 +121,6 @@ class CreateMixIn: read_special=args.read_special, dry_run=dry_run, ) - except BackupOSError as e: - self.print_warning_instance(BackupOSWarning(path, e)) - status = "E" except BackupError as e: self.print_warning_instance(BackupWarning(path, e)) status = "E" @@ -151,8 +148,8 @@ class CreateMixIn: status = fso.process_pipe( 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" else: status = "+" # included @@ -183,9 +180,9 @@ class CreateMixIn: # if we get back here, we've finished recursing into , # 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 as e: + except BackupError as e: # 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 if not dry_run: if args.progress: @@ -368,7 +365,7 @@ class CreateMixIn: else: self.print_warning("Unknown file type: %s", path) return - except (BackupError, BackupOSError) as err: + except BackupError as err: if isinstance(err, BackupOSError): if err.errno in (errno.EPERM, errno.EACCES): # Do not try again, such errors can not be fixed by retrying. @@ -524,9 +521,6 @@ class CreateMixIn: dry_run=dry_run, ) - except BackupOSError as e: - self.print_warning_instance(BackupOSWarning(path, e)) - status = "E" except BackupError as e: self.print_warning_instance(BackupWarning(path, e)) status = "E" diff --git a/src/borg/archiver/extract_cmd.py b/src/borg/archiver/extract_cmd.py index 5732f2bf..92cc7fa2 100644 --- a/src/borg/archiver/extract_cmd.py +++ b/src/borg/archiver/extract_cmd.py @@ -6,13 +6,13 @@ import stat from ._common import with_repository, with_archive from ._common import build_filter, build_matcher -from ..archive import BackupError, BackupOSError +from ..archive import BackupError from ..constants import * # NOQA from ..helpers import archivename_validator, PathSpec from ..helpers import remove_surrogates from ..helpers import HardLinkManager from ..helpers import ProgressIndicatorPercent -from ..helpers import BackupWarning, BackupOSWarning, IncludePatternNeverMatchedWarning +from ..helpers import BackupWarning, IncludePatternNeverMatchedWarning from ..manifest import Manifest from ..logger import create_logger @@ -65,8 +65,8 @@ class ExtractMixIn: dir_item = dirs.pop(-1) try: 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: logging.getLogger("borg.output.list").info(remove_surrogates(item.path)) try: @@ -80,8 +80,6 @@ class ExtractMixIn: archive.extract_item( 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: self.print_warning_instance(BackupWarning(remove_surrogates(orig_path), e)) if pi: @@ -96,8 +94,8 @@ class ExtractMixIn: dir_item = dirs.pop(-1) try: 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(): self.print_warning_instance(IncludePatternNeverMatchedWarning(pattern)) if pi: diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index ef8c9f97..af914e67 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -13,8 +13,9 @@ from .checks import check_extension_modules, check_python from .datastruct import StableDict, Buffer, EfficientCollectionQueue from .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError, CancelledByUser, CommandError 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 BackupPermissionError, BackupIOError, BackupFileNotFoundError 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 dir_is_tagged, dir_is_cachedir, remove_dotdot_prefixes, make_path_safe, scandir_inorder diff --git a/src/borg/helpers/errors.py b/src/borg/helpers/errors.py index b40005d9..015dda1b 100644 --- a/src/borg/helpers/errors.py +++ b/src/borg/helpers/errors.py @@ -1,4 +1,3 @@ -import errno import os from ..constants import * # NOQA @@ -112,70 +111,42 @@ class IncludePatternNeverMatchedWarning(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 def exit_code(self): if not modern_ec: return EXIT_WARNING 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 + assert isinstance(exc, BackupError) + return exc.exit_mcode -class PermissionWarning(BorgWarning): - """{}: {}""" +class BackupError(Error): + """{}: backup error""" - exit_mcode = 105 - - -class IOWarning(BorgWarning): - """{}: {}""" - - exit_mcode = 106 - - -class NotFoundWarning(BorgWarning): - """{}: {}""" - - exit_mcode = 107 - - -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): - """ - 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. +class BackupOSError(BackupError): + """{}: {}""" - 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). - - 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): self.op = op @@ -189,3 +160,21 @@ class BackupOSError(Exception): return f"{self.op}: {self.os_error}" else: return str(self.os_error) + + +class BackupPermissionError(BackupOSError): + """{}: {}""" + + exit_mcode = 105 + + +class BackupIOError(BackupOSError): + """{}: {}""" + + exit_mcode = 106 + + +class BackupFileNotFoundError(BackupOSError): + """{}: {}""" + + exit_mcode = 107