mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-31 13:43:02 +02:00
124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
import fnmatch
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
|
|
import borgmatic.actions.restore
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
IS_A_HOOK = False
|
|
|
|
|
|
def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_name):
|
|
'''
|
|
Given a borgmatic runtime directory and a data source hook name, construct a data source dump
|
|
path.
|
|
'''
|
|
return os.path.join(borgmatic_runtime_directory, data_source_hook_name)
|
|
|
|
|
|
def make_data_source_dump_filename(dump_path, name, hostname=None, port=None, label=None):
|
|
'''
|
|
Based on the given dump directory path, data source name, hostname, and port, return a filename
|
|
to use for the data source dump. The hostname defaults to localhost.
|
|
|
|
Raise ValueError if the data source name is invalid.
|
|
'''
|
|
if os.path.sep in name:
|
|
raise ValueError(f'Invalid data source name {name}')
|
|
|
|
identifier = (
|
|
label if label else (hostname or 'localhost') + ('' if port is None else f':{port}')
|
|
)
|
|
|
|
return os.path.join(dump_path, identifier, name)
|
|
|
|
|
|
def write_data_source_dumps_metadata(borgmatic_runtime_directory, hook_name, dumps_metadata):
|
|
'''
|
|
Given the borgmatic runtime directory, a data source hook name, and a sequence of
|
|
borgmatic.actions.restore.Dump instances of dump metadata, write a metadata file describing all
|
|
of those dumps. This metadata is being dumped so that it's available upon restore, e.g. to
|
|
support the user selecting which data source(s) should be restored.
|
|
|
|
Raise ValueError if writing to the file results in an operating system error.
|
|
'''
|
|
dumps_metadata_path = os.path.join(borgmatic_runtime_directory, hook_name, 'dumps.json')
|
|
|
|
try:
|
|
with open(dumps_metadata_path, 'w', encoding='utf-8') as metadata_file:
|
|
json.dump(
|
|
{
|
|
'dumps': [dump._asdict() for dump in dumps_metadata],
|
|
},
|
|
metadata_file,
|
|
sort_keys=True,
|
|
)
|
|
except OSError as error:
|
|
raise ValueError(f'Error writing to dumps metadata at {dumps_metadata_path}: {error}')
|
|
|
|
|
|
def parse_data_source_dumps_metadata(dumps_json, dumps_metadata_path):
|
|
'''
|
|
Given a dumps metadata JSON string as extracted from an archive and its path within the archive,
|
|
parse it into a tuple of borgmatic.actions.restore.Dump instances and return them.
|
|
|
|
Raise ValueError if parsing the JSON results in a JSON decode error or the data does not have
|
|
the expected keys.
|
|
'''
|
|
try:
|
|
return tuple(
|
|
borgmatic.actions.restore.Dump(**dump) for dump in json.loads(dumps_json)['dumps']
|
|
)
|
|
except (json.JSONDecodeError, TypeError, KeyError) as error:
|
|
raise ValueError(
|
|
f'Cannot read archive data source dumps metadata at {dumps_metadata_path} due to invalid JSON: {error}',
|
|
)
|
|
|
|
|
|
def create_parent_directory_for_dump(dump_path):
|
|
'''
|
|
Create a directory to contain the given dump path.
|
|
'''
|
|
os.makedirs(os.path.dirname(dump_path), mode=0o700, exist_ok=True)
|
|
|
|
|
|
def create_named_pipe_for_dump(dump_path):
|
|
'''
|
|
Create a named pipe at the given dump path.
|
|
'''
|
|
create_parent_directory_for_dump(dump_path)
|
|
os.mkfifo(dump_path, mode=0o600)
|
|
|
|
|
|
def remove_data_source_dumps(dump_path, data_source_type_name, dry_run):
|
|
'''
|
|
Remove all data source dumps in the given dump directory path (including the directory itself).
|
|
If this is a dry run, then don't actually remove anything.
|
|
'''
|
|
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
|
|
|
logger.debug(f'Removing {data_source_type_name} data source dumps{dry_run_label}')
|
|
|
|
if dry_run:
|
|
return
|
|
|
|
if os.path.exists(dump_path):
|
|
shutil.rmtree(dump_path)
|
|
|
|
|
|
def convert_glob_patterns_to_borg_pattern(patterns):
|
|
'''
|
|
Convert a sequence of shell glob patterns like "/etc/*", "/tmp/*" to the corresponding Borg
|
|
regular expression archive pattern as a single string like "re:etc/.*|tmp/.*".
|
|
'''
|
|
# Remove the "\Z" generated by fnmatch.translate() because we don't want the pattern to match
|
|
# only at the end of a path, as directory format dumps require extracting files with paths
|
|
# longer than the pattern. E.g., a pattern of "borgmatic/*/foo_databases/test" should also match
|
|
# paths like "borgmatic/*/foo_databases/test/toc.dat"
|
|
return 're:' + '|'.join(
|
|
fnmatch.translate(pattern.lstrip('/')).replace('\\Z', '') for pattern in patterns
|
|
)
|