raise BackupOSError subclasses

This commit is contained in:
Thomas Waldmann 2023-11-27 17:02:23 +01:00
parent 9ac4672254
commit 97dd287584
No known key found for this signature in database
GPG Key ID: 243ACFA951F78E01
7 changed files with 77 additions and 84 deletions

View File

@ -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

View File

@ -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()

View File

@ -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__ # if it is a BackupWarning, use the wrapped BackupError exception instance:
msgid = type(warning).__qualname__ cls = type(warning.args[1]) if isinstance(warning, BackupWarning) else type(warning)
args = warning.args msg, msgid, args, wc = cls.__doc__, cls.__qualname__, warning.args, warning.exit_code
self.print_warning(msg, *args, wc=warning.exit_code, wt="curly", msgid=msgid) 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

View File

@ -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: except BackupError as e:
self.print_warning_instance(BackupOSWarning(path, 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"

View File

@ -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: except BackupError as e:
self.print_warning_instance(BackupOSWarning(remove_surrogates(dir_item.path), 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: except BackupError as e:
self.print_warning_instance(BackupOSWarning(remove_surrogates(dir_item.path), 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:

View File

@ -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

View File

@ -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 # this is to wrap a caught BackupError exception, so it can be given to print_warning_instance
class BackupOSWarning(BorgWarning):
"""{}: {}"""
exit_mcode = 104
@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) assert isinstance(exc, BackupError)
if exc.errno in (errno.EPERM, errno.EACCES): return exc.exit_mcode
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): class BackupError(Error):
"""{}: {}""" """{}: backup error"""
exit_mcode = 105 # Exception raised for non-OSError-based exceptions while accessing backup files.
exit_mcode = 102
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.
"""
class BackupRaceConditionError(BackupError): class BackupRaceConditionError(BackupError):
""" """{}: 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.
""" # Exception raised when encountering a critical race condition while trying to back up a file.
exit_mcode = 103
class BackupOSError(Exception): class BackupOSError(BackupError):
""" """{}: {}"""
Wrapper for OSError raised while accessing backup files.
Borg does different kinds of IO, and IO failures have different consequences. # Wrapper for OSError raised while accessing backup files.
This wrapper represents failures of input file or extraction IO. #
These are non-critical and are only reported (exit code = 1, warning). # Borg does different kinds of IO, and IO failures have different consequences.
# This wrapper represents failures of input file or extraction IO.
Any unwrapped IO error is critical and aborts execution (for example repository IO failure). # 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