mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 10:13:00 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ac2c86d2d | ||
|
|
b7ecc9c6c3 | ||
|
|
4f728b1cd4 | ||
|
|
78db924b70 | ||
|
|
b55834c1ca |
@@ -1,28 +1,9 @@
|
||||
2.1.7.dev0
|
||||
* #1309: Add support for the "--quick-stats" flag and the "quick_statistics" option to the "prune"
|
||||
action. Borg >= 1.4.5 and < 2 only.
|
||||
* #1317: Add an "archive_hostname" option and a corresponding "--archive-hostname" flag for
|
||||
overriding the hostname used for the "{hostname}" placeholder in the "archive_name_format"
|
||||
option. Also add an "archive_username" option and corresponding "--archive-username" flag to
|
||||
override the "{user}" plaecholder. Both options/flags are Borg 1.4.5+ only.
|
||||
* #1319: Fix the ZFS hook's overzealous unmounting of snapshot paths when a source dataset is at
|
||||
"/".
|
||||
* #1322: Fix for the "restore" action sometimes failing to find a database dump that was dumped
|
||||
with a default port.
|
||||
* #1324: For the MariaDB and MySQL hooks, add "events", "routines", and "tablespaces" options for
|
||||
disabling dumping of scheduled events, stored routines, and tablespaces, respectively.
|
||||
* #1327: Fix an error from the "diff" action when exclude options are configured.
|
||||
* #1331: Fix the "repo-create" action to more surgically suppress Borg "Repository does not exist"
|
||||
logs and avoid inadvertently suppressing other error logs.
|
||||
* #1333: Fix the "--progress" flag on the "compact" action to actually update the progress of
|
||||
segment compaction.
|
||||
* #1334: Add the "CAP_FOWNER" capability to "CapabilityBoundingSet" in the sample systemd service,
|
||||
so that Borg can open source files without changing file access times.
|
||||
* Fix a bug in which the "compact" action does not pass a compact threshold of zero to Borg.
|
||||
|
||||
2.1.6
|
||||
* #1256: Fix a race condition in which borgmatic sometimes swallows Borg error output without
|
||||
logging it.
|
||||
2.1.6.dev0
|
||||
* #1267: Support multiple borgmatic instances run in parallel—as long as they're operating on
|
||||
different Borg repositories. This required an internal change to the runtime directory path,
|
||||
which may cause a one-time performance hit (Borg cache misses) for Borg 1.x users of database and
|
||||
filesystem hooks. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/how-to/make-per-application-backups/#parallelism
|
||||
* #1300: Fix the "source_directories_must_exist" option to support source directories relative to a
|
||||
"working_directory".
|
||||
* #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes
|
||||
@@ -32,16 +13,6 @@
|
||||
* #1303: For the MariaDB hook, include only a subset of system data when dumping the "mysql" system
|
||||
database (or "all" databases), so the dump is actually restorable. See the documentation for more
|
||||
information: https://torsion.org/borgmatic/reference/configuration/data-sources/mariadb/
|
||||
* #1308: Update the Apprise monitoring hook's "url" option to support loading credentials with the
|
||||
"{credential ...}" syntax. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/reference/configuration/credentials/
|
||||
* #1309: Add a "--quick-stats" flag and corresponding "quick_statistics" option for showing only
|
||||
abbreviated statistics for the "create" action. Borg 1.4.5+ only.
|
||||
* #1314: Add a minimal stand-alone borgmatic binary in addition to the standard one. The minimal
|
||||
binary omits support for the Apprise monitoring hook.
|
||||
* Add an experimental "browse" action providing a console UI for browsing your backups. See the
|
||||
documentation for more information:
|
||||
https://torsion.org/borgmatic/how-to/inspect-your-backups/#browsing-backups
|
||||
* Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the
|
||||
documentation for more information:
|
||||
https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/
|
||||
|
||||
@@ -106,16 +106,16 @@ reference guides</a>.
|
||||
## Hosting providers
|
||||
|
||||
Need somewhere to store your encrypted off-site backups? The following hosting
|
||||
provider includes specific support for Borg/borgmatic—and funds borgmatic
|
||||
development and hosting when you use this referral links to sign up:
|
||||
providers include specific support for Borg/borgmatic—and fund borgmatic
|
||||
development and hosting when you use these referral links to sign up:
|
||||
|
||||
<ul>
|
||||
<li class="referral"><a href="https://www.borgbase.com/?utm_source=borgmatic">BorgBase</a>: Borg hosting service with support for monitoring, 2FA, and append-only repos</li>
|
||||
<li class="referral"><a href="https://hetzner.cloud/?ref=v9dOJ98Ic9I8">Hetzner</a>: A "storage box" that includes support for Borg</li>
|
||||
</ul>
|
||||
|
||||
Additionally, Hetzner and rsync\.net have compatible storage offerings, but do
|
||||
not fund borgmatic development or hosting.
|
||||
|
||||
Additionally, rsync.net has a compatible storage offering, but does not fund
|
||||
borgmatic development or hosting.
|
||||
|
||||
## Support and contributing
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
apprise==1.10.0 # via -r binary_requirements.in
|
||||
attrs==26.1.0 # via jsonschema, referencing, -r binary_requirements.in
|
||||
. # via -r binary_requirements.in
|
||||
certifi==2026.5.20 # via apprise, requests, -r binary_requirements.in
|
||||
certifi==2026.4.22 # via apprise, requests, -r binary_requirements.in
|
||||
charset-normalizer==3.4.7 # via requests, -r binary_requirements.in
|
||||
click==8.4.1 # via apprise, -r binary_requirements.in
|
||||
idna==3.16 # via requests, -r binary_requirements.in
|
||||
click==8.3.3 # via apprise, -r binary_requirements.in
|
||||
idna==3.13 # via requests, -r binary_requirements.in
|
||||
jsonschema==4.26.0 # via borgmatic, -r binary_requirements.in
|
||||
jsonschema-specifications==2025.9.1 # via jsonschema, -r binary_requirements.in
|
||||
markdown==3.10.2 # via apprise, -r binary_requirements.in
|
||||
@@ -14,8 +14,8 @@ oauthlib==3.3.1 # via requests-oauthlib, -r binary_requirements.in
|
||||
packaging==26.2 # via borgmatic, -r binary_requirements.in
|
||||
pyyaml==6.0.3 # via apprise, -r binary_requirements.in
|
||||
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r binary_requirements.in
|
||||
requests==2.34.2 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in
|
||||
requests==2.33.1 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in
|
||||
requests-oauthlib==2.0.0 # via apprise, -r binary_requirements.in
|
||||
rpds-py==0.30.0 # via jsonschema, referencing, -r binary_requirements.in
|
||||
ruamel-yaml==0.19.1 # via borgmatic, -r binary_requirements.in
|
||||
urllib3==2.7.0 # via requests, -r binary_requirements.in
|
||||
urllib3==2.6.3 # via requests, -r binary_requirements.in
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import signal
|
||||
|
||||
import textual.app
|
||||
import textual.binding
|
||||
import textual.widgets
|
||||
|
||||
import borgmatic.actions.browse.carousel
|
||||
import borgmatic.actions.browse.configuration_files_list
|
||||
import borgmatic.actions.browse.logs
|
||||
import borgmatic.actions.browse.repositories_list
|
||||
|
||||
|
||||
class Browse_app(textual.app.App):
|
||||
'''
|
||||
The main app / entry point for the browse action UI.
|
||||
'''
|
||||
|
||||
BINDINGS = (
|
||||
textual.binding.Binding(key='q', action='quit', description='quit'),
|
||||
textual.binding.Binding(key='v', action='toggle_logs', description='view logs'),
|
||||
textual.binding.Binding(
|
||||
key='c', action='command_palette', description='commands', show=False
|
||||
),
|
||||
)
|
||||
COMMAND_PALETTE_BINDING = 'c'
|
||||
CSS = '''
|
||||
.panel {
|
||||
border: round $primary;
|
||||
border-title-color: $text-primary;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#logs {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
display: none;
|
||||
}
|
||||
'''
|
||||
|
||||
def __init__(self, configs):
|
||||
self.configs = configs
|
||||
|
||||
super().__init__()
|
||||
|
||||
def get_system_commands(self, screen): # pragma: no cover
|
||||
'''
|
||||
Remove the screenshot system command because it produces broken screenshots. (Emoji
|
||||
weirdness, etc.) Also remove minimize and maximize because they don't accomplish much with
|
||||
out particular layout.
|
||||
'''
|
||||
yield from (
|
||||
command
|
||||
for command in super().get_system_commands(screen)
|
||||
if command.title not in {'Screenshot', 'Minimize', 'Maximize'}
|
||||
)
|
||||
|
||||
def compose(self):
|
||||
'''
|
||||
Compose a UI consisting of:
|
||||
|
||||
* a header with the application name
|
||||
* a carousel container that contains the main UI panels
|
||||
* a logs panel where Python logs show up (panel hidden by default)
|
||||
* a footer with available keys listed
|
||||
'''
|
||||
yield textual.widgets.Header()
|
||||
yield borgmatic.actions.browse.carousel.Carousel(
|
||||
[
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list(
|
||||
self.configs
|
||||
)
|
||||
]
|
||||
if len(self.configs) > 1
|
||||
else [
|
||||
borgmatic.actions.browse.repositories_list.Repositories_list(
|
||||
next(iter(self.configs.values()))
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
logs_panel = borgmatic.actions.browse.logs.Logs()
|
||||
yield logs_panel
|
||||
yield textual.widgets.Footer()
|
||||
|
||||
borgmatic.actions.browse.logs.log_to_widget(logs_panel)
|
||||
|
||||
def on_mount(self):
|
||||
'''
|
||||
Set the application title, which ends up in the header.
|
||||
'''
|
||||
self.title = 'borgmatic browse'
|
||||
|
||||
def action_toggle_logs(self):
|
||||
'''
|
||||
Toggle the show/hide status of the logs panel.
|
||||
'''
|
||||
logs_panel = self.query_one('#logs')
|
||||
logs_panel.styles.display = 'none' if logs_panel.styles.display == 'block' else 'block'
|
||||
|
||||
def exit(self): # pragma: no cover
|
||||
'''
|
||||
Exit the application. But first raise a SIGTERM (handled in borgmatic/signals.py) to
|
||||
encourage a fast exit by killing any ongoing Borg subprocesses.
|
||||
'''
|
||||
signal.raise_signal(signal.SIGTERM)
|
||||
|
||||
super().exit()
|
||||
@@ -1,143 +0,0 @@
|
||||
import argparse
|
||||
import collections
|
||||
import enum
|
||||
import json
|
||||
import logging
|
||||
|
||||
import binaryornot.helpers
|
||||
|
||||
import borgmatic.borg.extract
|
||||
import borgmatic.borg.list
|
||||
import borgmatic.borg.repo_list
|
||||
import borgmatic.borg.version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Path_type(enum.Enum):
|
||||
DIRECTORY = 'd'
|
||||
LINK = 'l'
|
||||
PIPE = 'p'
|
||||
FILE = '-'
|
||||
|
||||
|
||||
# A data structure capturing a path stored in a Borg archive.
|
||||
Archive_path = collections.namedtuple(
|
||||
'Archive_path',
|
||||
('path_type', 'file_path', 'link_target'),
|
||||
)
|
||||
|
||||
|
||||
def get_repository_archives(config, repository):
|
||||
'''
|
||||
Given a configuration dict and a repository dict, return a list of the repository's archives,
|
||||
one dict per archive.
|
||||
'''
|
||||
with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])):
|
||||
logger.info('Listing repository')
|
||||
repo_list_arguments = argparse.Namespace(
|
||||
repository=repository['path'],
|
||||
short=None,
|
||||
format=None,
|
||||
json=True,
|
||||
prefix=None,
|
||||
match_archives=None,
|
||||
sort_by=None,
|
||||
first=None,
|
||||
last=None,
|
||||
)
|
||||
global_arguments = argparse.Namespace()
|
||||
local_path = config.get('local_path', 'borg')
|
||||
remote_path = config.get('remote_path')
|
||||
local_borg_version = borgmatic.borg.version.local_borg_version(config, local_path)
|
||||
|
||||
return json.loads(
|
||||
borgmatic.borg.repo_list.list_repository(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
repo_list_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_archive_paths(config, repository, archive_name):
|
||||
'''
|
||||
Given a configuration dict, a repository dict, and an archive name in that repository, get a
|
||||
generator of files, directories, symlinks, etc. found in the archive, each as an Archive_path
|
||||
instance.
|
||||
'''
|
||||
with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])):
|
||||
logger.info(f'Listing archive {archive_name}')
|
||||
|
||||
global_arguments = argparse.Namespace()
|
||||
local_path = config.get('local_path', 'borg')
|
||||
remote_path = config.get('remote_path')
|
||||
local_borg_version = borgmatic.borg.version.local_borg_version(config, local_path)
|
||||
|
||||
return (
|
||||
Archive_path(
|
||||
path_data['type'],
|
||||
path_data['path'],
|
||||
path_data.get('linktarget'),
|
||||
)
|
||||
for path_data in borgmatic.borg.list.capture_archive_listing(
|
||||
repository['path'],
|
||||
archive_name,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
READLINES_HINT_BYTES = 100000
|
||||
TRUNCATION_MESSAGE = '[... truncated for display ...]'
|
||||
|
||||
|
||||
def get_archive_file_content(config, repository, archive_name, file_path):
|
||||
'''
|
||||
Given a configuration dict, a repository dict, an archive name in that repository, and a file
|
||||
path in that archive, return the file's contents or None if the file can't be loaded, e.g.
|
||||
because it's binary or can't be decoded.
|
||||
|
||||
If the file is too large, then truncate the returned content.
|
||||
'''
|
||||
with borgmatic.logger.Log_prefix(repository.get('label', repository['path'])):
|
||||
logger.info(f'Getting archive content of file {file_path}')
|
||||
local_path = config.get('local_path', 'borg')
|
||||
remote_path = config.get('remote_path')
|
||||
|
||||
lines = borgmatic.borg.extract.extract_archive(
|
||||
dry_run=False,
|
||||
repository=repository['path'],
|
||||
archive=archive_name,
|
||||
paths=(file_path,),
|
||||
config=config,
|
||||
local_borg_version=borgmatic.borg.version.local_borg_version(config, local_path),
|
||||
global_arguments=argparse.Namespace(),
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
destination_path=None,
|
||||
strip_components=None,
|
||||
extract_to_stdout=True,
|
||||
).stdout.readlines(READLINES_HINT_BYTES)
|
||||
|
||||
content = b''.join(lines)
|
||||
|
||||
if binaryornot.helpers.is_binary_string(content):
|
||||
return None
|
||||
|
||||
try:
|
||||
return (
|
||||
content.decode()
|
||||
if len(content) < READLINES_HINT_BYTES
|
||||
else f'{content.decode()}\n{TRUNCATION_MESSAGE}'
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
@@ -1,89 +0,0 @@
|
||||
import textual.widgets
|
||||
|
||||
import borgmatic.actions.browse.bindings
|
||||
import borgmatic.actions.browse.loading
|
||||
import borgmatic.actions.browse.workers
|
||||
|
||||
|
||||
class Archives_list(textual.widgets.OptionList):
|
||||
'''
|
||||
A widget for selecting a single Borg archive from among the archives in a repository. The item
|
||||
selection event is handled in a Carousel instance, the parent widget of an Archives_list.
|
||||
'''
|
||||
|
||||
BINDINGS = borgmatic.actions.browse.bindings.OPTION_LIST_BINDINGS
|
||||
|
||||
def __init__(self, config, repository):
|
||||
'''
|
||||
Given a configuration dict and a repository dict, prepare to load the archives from the
|
||||
repository for eventual display in this widget. Actual loading kicks off in on_mount()
|
||||
below.
|
||||
'''
|
||||
self.config = config
|
||||
self.repository = repository
|
||||
|
||||
super().__init__(classes='panel')
|
||||
self.border_title = '📚 archives'
|
||||
self.highlighted_option_changed = False
|
||||
self.archive_loaded = borgmatic.actions.browse.workers.Archive_loaded(
|
||||
self, 'archive loaded'
|
||||
)
|
||||
|
||||
self.loading_timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self)
|
||||
|
||||
def on_mount(self):
|
||||
'''
|
||||
When this widget gets mounted in the DOM, subscribe to archive loaded events so that we can
|
||||
find out about archives as they load. Also start loading archives from the repository.
|
||||
|
||||
Loading is started *after* subscribing to the archive loaded signal so that there's not a
|
||||
gap where we might miss out on signal publishes.
|
||||
'''
|
||||
self.archive_loaded.subscribe(self, self.on_archive_loaded)
|
||||
|
||||
borgmatic.actions.browse.workers.add_repository_archives(
|
||||
self.app,
|
||||
archive_loaded=self.archive_loaded,
|
||||
config=self.config,
|
||||
repository=self.repository,
|
||||
)
|
||||
|
||||
def on_archive_loaded(self, archive_name):
|
||||
'''
|
||||
When an archive loads, add it as an option to this archives list. But if we get a
|
||||
signal that all path loading is complete, stop and remove our loading indicator.
|
||||
'''
|
||||
if archive_name is borgmatic.actions.browse.workers.LOADING_DONE:
|
||||
self.loading_timer.stop()
|
||||
self.remove_option('loading-indicator')
|
||||
return
|
||||
|
||||
label_pieces = (
|
||||
(archive_name, '[dim](latest)[/dim]') if len(self.options) == 1 else (archive_name,)
|
||||
)
|
||||
highlighted_option = self.highlighted_option
|
||||
|
||||
loading_indicator = self.get_option('loading-indicator')
|
||||
self.remove_option('loading-indicator')
|
||||
self.add_options(
|
||||
(
|
||||
textual.widgets.option_list.Option(' '.join(label_pieces), id=archive_name),
|
||||
loading_indicator,
|
||||
),
|
||||
)
|
||||
|
||||
# Retain the highlighted option position even as other options load around it.
|
||||
self.highlighted = (
|
||||
self.get_option_index(highlighted_option.id)
|
||||
if highlighted_option and self.highlighted_option_changed
|
||||
else 0
|
||||
)
|
||||
|
||||
def on_option_list_option_highlighted(self, event):
|
||||
'''
|
||||
When the highlighted option changes, record that fact. This flag is consumed in
|
||||
borgmatic.actions.browse.workers.add_repository_archives() in order to retain the
|
||||
highlighted option even as other options load around it.
|
||||
'''
|
||||
if self.highlighted not in {None, 0}:
|
||||
self.highlighted_option_changed = True
|
||||
@@ -1,22 +0,0 @@
|
||||
import textual.binding
|
||||
import textual.widgets
|
||||
|
||||
OPTION_LIST_BINDINGS = (
|
||||
*textual.widgets.OptionList.BINDINGS,
|
||||
textual.binding.Binding(
|
||||
key='up,k', action='cursor_up', description='scroll up', show=True, priority=True
|
||||
),
|
||||
textual.binding.Binding(
|
||||
key='down,j', action='cursor_down', description='scroll down', show=True, priority=True
|
||||
),
|
||||
textual.binding.Binding(
|
||||
key='pageup', action='page_up', description='page up', show=True, priority=True
|
||||
),
|
||||
textual.binding.Binding(
|
||||
key='pagedown', action='page_down', description='page down', show=True, priority=True
|
||||
),
|
||||
textual.binding.Binding(
|
||||
key='enter', action='select', description='select', show=True, priority=True
|
||||
),
|
||||
textual.binding.Binding(key='right,l', action='select', description='select', show=False),
|
||||
)
|
||||
@@ -1,153 +0,0 @@
|
||||
import os
|
||||
|
||||
import textual.binding
|
||||
import textual.containers
|
||||
|
||||
import borgmatic.actions.browse.archive
|
||||
import borgmatic.actions.browse.archives_list
|
||||
import borgmatic.actions.browse.configuration_files_list
|
||||
import borgmatic.actions.browse.directory_list
|
||||
import borgmatic.actions.browse.file_preview
|
||||
import borgmatic.actions.browse.icons
|
||||
import borgmatic.actions.browse.repositories_list
|
||||
|
||||
|
||||
def make_next_panel(focused_panel, option_id):
|
||||
'''
|
||||
Given a focused panel widget and the selected option ID, return the next panel corresponding to
|
||||
that selection. This is the mechanism by which the user can successively drill down from
|
||||
configuration file to repository to archive to root directory to non-root directory or file.
|
||||
|
||||
If the particular option ID on the focused panel doesn't have a supported next panel, then
|
||||
return None.
|
||||
'''
|
||||
if isinstance(
|
||||
focused_panel, borgmatic.actions.browse.configuration_files_list.Configuration_files_list
|
||||
):
|
||||
return borgmatic.actions.browse.repositories_list.Repositories_list(
|
||||
config=focused_panel.configs[option_id]
|
||||
)
|
||||
|
||||
if isinstance(focused_panel, borgmatic.actions.browse.repositories_list.Repositories_list):
|
||||
return borgmatic.actions.browse.archives_list.Archives_list(
|
||||
config=focused_panel.config, repository=focused_panel.repositories[option_id]
|
||||
)
|
||||
|
||||
if isinstance(focused_panel, borgmatic.actions.browse.archives_list.Archives_list):
|
||||
return borgmatic.actions.browse.directory_list.Directory_list(
|
||||
config=focused_panel.config, repository=focused_panel.repository, archive_name=option_id
|
||||
)
|
||||
|
||||
if isinstance(focused_panel, borgmatic.actions.browse.directory_list.Directory_list):
|
||||
option = focused_panel.get_option(option_id)
|
||||
|
||||
if option.prompt.startswith(
|
||||
borgmatic.actions.browse.icons.PATH_TYPE_ICONS[
|
||||
borgmatic.actions.browse.archive.Path_type.DIRECTORY.value
|
||||
]
|
||||
):
|
||||
return borgmatic.actions.browse.directory_list.Directory_list(
|
||||
focused_panel.config,
|
||||
focused_panel.repository,
|
||||
focused_panel.archive_name,
|
||||
path_loaded=focused_panel.path_loaded,
|
||||
path_components=(*focused_panel.path_components, option_id),
|
||||
)
|
||||
|
||||
if option.prompt.startswith(
|
||||
borgmatic.actions.browse.icons.PATH_TYPE_ICONS[
|
||||
borgmatic.actions.browse.archive.Path_type.FILE.value
|
||||
]
|
||||
):
|
||||
return borgmatic.actions.browse.file_preview.File_preview(
|
||||
focused_panel.config,
|
||||
focused_panel.repository,
|
||||
focused_panel.archive_name,
|
||||
file_path=os.path.sep.join((*focused_panel.path_components, option_id)),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class Carousel(textual.containers.Horizontal):
|
||||
BINDINGS = (
|
||||
textual.binding.Binding(
|
||||
key='left,h', action='previous', description='previous', priority=True
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, panels):
|
||||
self.panels = panels
|
||||
self.focused_panel = panels[0]
|
||||
|
||||
super().__init__()
|
||||
|
||||
def compose(self):
|
||||
'''
|
||||
Compose with each of the contained panels and focus the first one.
|
||||
'''
|
||||
yield from self.panels
|
||||
|
||||
self.focused_panel.focus()
|
||||
|
||||
def action_previous(self):
|
||||
'''
|
||||
Make the previous panel into the focused panel.
|
||||
'''
|
||||
previous_panel_index = self.panels.index(self.focused_panel) - 1
|
||||
|
||||
if previous_panel_index < 0:
|
||||
return
|
||||
|
||||
self.focused_panel.styles.display = 'none'
|
||||
|
||||
self.focused_panel = self.panels[previous_panel_index]
|
||||
self.focused_panel.styles.display = 'block'
|
||||
self.focused_panel.focus()
|
||||
|
||||
def action_next(self, option_id):
|
||||
'''
|
||||
Hide the current focused panel and create the next one.
|
||||
'''
|
||||
next_panel_index = self.panels.index(self.focused_panel) + 1
|
||||
|
||||
if next_panel_index < len(self.panels):
|
||||
next_panel = self.panels[next_panel_index]
|
||||
next_panel.styles.display = 'block'
|
||||
else:
|
||||
next_panel = make_next_panel(self.focused_panel, option_id)
|
||||
|
||||
if next_panel is None:
|
||||
self.notify('Cannot display this content', severity='warning')
|
||||
return
|
||||
|
||||
self.panels.append(next_panel)
|
||||
next_panel.highlighted = 0
|
||||
|
||||
self.focused_panel.styles.display = 'none'
|
||||
self.focused_panel = next_panel
|
||||
self.focused_panel.focus()
|
||||
self.mount(self.focused_panel)
|
||||
|
||||
def on_option_list_option_highlighted(self, event):
|
||||
'''
|
||||
The highlighted option has changed, so truncate any next panels.
|
||||
'''
|
||||
next_panel_index = self.panels.index(self.focused_panel) + 1
|
||||
|
||||
del self.panels[next_panel_index:]
|
||||
|
||||
def on_option_list_option_selected(self, event):
|
||||
'''
|
||||
An option has been selected, so advance to the next panel—unless the option selected is
|
||||
"..", in which case go to the previous panel.
|
||||
'''
|
||||
if (
|
||||
event.option_list != self.focused_panel or event.option_id == 'loading-indicator'
|
||||
): # pragma: no cover
|
||||
return
|
||||
|
||||
if event.option_id == '..':
|
||||
self.action_previous()
|
||||
else:
|
||||
self.action_next(event.option_id)
|
||||
@@ -1,33 +0,0 @@
|
||||
import os
|
||||
|
||||
import textual.widgets
|
||||
|
||||
import borgmatic.actions.browse.bindings
|
||||
|
||||
|
||||
class Configuration_files_list(textual.widgets.OptionList):
|
||||
'''
|
||||
A widget for selecting a single borgmatic configuration file from among available configuration
|
||||
files. The item selection event is handled in a Carousel instance, the parent widget of an
|
||||
Configuration_files_list.
|
||||
'''
|
||||
|
||||
BINDINGS = borgmatic.actions.browse.bindings.OPTION_LIST_BINDINGS
|
||||
|
||||
def __init__(self, configs):
|
||||
'''
|
||||
Given a dict mapping from configuration path to corresponding configuration dict, add each
|
||||
configuration path as an option to this widget.
|
||||
'''
|
||||
self.configs = configs
|
||||
home_directory = os.path.expanduser('~')
|
||||
|
||||
super().__init__(
|
||||
*(
|
||||
textual.widgets.option_list.Option(unexpanded_path, id=config_path)
|
||||
for config_path in configs
|
||||
for unexpanded_path in (config_path.replace(home_directory, '~'),)
|
||||
),
|
||||
classes='panel',
|
||||
)
|
||||
self.border_title = '📄 configuration files'
|
||||
@@ -1,211 +0,0 @@
|
||||
import os
|
||||
|
||||
import textual.widgets
|
||||
|
||||
import borgmatic.actions.browse.bindings
|
||||
import borgmatic.actions.browse.icons
|
||||
import borgmatic.actions.browse.loading
|
||||
import borgmatic.actions.browse.workers
|
||||
|
||||
|
||||
def get_relative_archive_path_components(archive_path, current_directory_path_components):
|
||||
'''
|
||||
Given an Archive_path instance and a tuple of path components for the currently browsed
|
||||
directory, get the path components as a tuple for the archive path relative to that directory.
|
||||
|
||||
For instance, given an archive path with a path of 'foo/bar/baz/quux.txt' and current
|
||||
directory path components of ('foo', 'bar'), return ('baz', 'quux.txt').
|
||||
|
||||
If the archive path is not actually relative to the current directory, return None.
|
||||
'''
|
||||
archive_path_components = tuple(archive_path.file_path.split(os.path.sep))
|
||||
|
||||
if not current_directory_path_components:
|
||||
return archive_path_components
|
||||
|
||||
# If the loaded path doesn't match this directory list's own path, then we don't care about
|
||||
# it for purposes of displaying this particular directory.
|
||||
if (
|
||||
tuple(archive_path_components[: len(current_directory_path_components)])
|
||||
!= current_directory_path_components
|
||||
):
|
||||
return None
|
||||
|
||||
# Strip off the portion of the archive path that matches the directory list's own path.
|
||||
return archive_path_components[len(current_directory_path_components) :]
|
||||
|
||||
|
||||
def make_directory_list_option(archive_path, relative_path_components):
|
||||
'''
|
||||
Given an Archive_path instance and a tuple of relative path components for it, make a
|
||||
textual.widgets.option_list.Option for the path. Use an the icon based on whether this looks
|
||||
like a terminal filename or a directory.
|
||||
'''
|
||||
pieces = (
|
||||
borgmatic.actions.browse.icons.PATH_TYPE_ICONS.get(
|
||||
archive_path.path_type if len(relative_path_components) == 1 else 'd', '❓'
|
||||
),
|
||||
relative_path_components[0],
|
||||
) + (('→', archive_path.link_target) if archive_path.link_target else ())
|
||||
|
||||
return textual.widgets.option_list.Option(
|
||||
prompt=' '.join(pieces), id=relative_path_components[0]
|
||||
)
|
||||
|
||||
|
||||
def add_archive_paths(
|
||||
directory_list,
|
||||
config,
|
||||
repository,
|
||||
archive_name,
|
||||
archive_paths,
|
||||
):
|
||||
'''
|
||||
Given a DirectoryList instance, a configuration dict, a repository dict, an archive name, and a
|
||||
sequence of ArchivePath instances, add the paths to the directory list as options, sorting and
|
||||
deduplicating the resulting directory list's options.
|
||||
|
||||
After all of this reshuffling, make sure the original highlighted option remains highlighted.
|
||||
'''
|
||||
highlighted_option = directory_list.highlighted_option
|
||||
original_options_count = len(directory_list.options)
|
||||
|
||||
sorted_options = sorted(
|
||||
(
|
||||
*directory_list.options,
|
||||
*(
|
||||
make_directory_list_option(archive_path, relative_path_components)
|
||||
for archive_path in archive_paths
|
||||
for relative_path_components in (
|
||||
get_relative_archive_path_components(
|
||||
archive_path,
|
||||
directory_list.path_components,
|
||||
),
|
||||
)
|
||||
if relative_path_components
|
||||
if relative_path_components[0] not in directory_list._id_to_option
|
||||
),
|
||||
),
|
||||
# The loading indicator "option" always goes to the bottom.
|
||||
key=lambda option: ((option.id == 'loading-indicator'), option.prompt),
|
||||
)
|
||||
|
||||
# If there aren't actually any options to add (due to deduplication), bail.
|
||||
if len(sorted_options) == original_options_count:
|
||||
return
|
||||
|
||||
# Retain the highlighted option position even as other options load around it.
|
||||
directory_list.set_options(sorted_options)
|
||||
directory_list.highlighted = (
|
||||
directory_list.get_option_index(highlighted_option.id)
|
||||
if highlighted_option and directory_list.highlighted_option_changed
|
||||
else 0
|
||||
)
|
||||
|
||||
|
||||
class Directory_list(textual.widgets.OptionList):
|
||||
'''
|
||||
A widget for selecting a path from among the contents of a particular directory in a Borg
|
||||
archive. The item selection event is handled in a Carousel instance, the parent widget of a
|
||||
Directory_list.
|
||||
'''
|
||||
|
||||
BINDINGS = borgmatic.actions.browse.bindings.OPTION_LIST_BINDINGS
|
||||
|
||||
def __init__(self, config, repository, archive_name, path_loaded=None, path_components=None):
|
||||
'''
|
||||
Given a configuration dict, a repository dict, an archive name, an optional
|
||||
Archive_path_loaded instance for signalling new paths as they load, and an optional tuple of
|
||||
path components indicating this directory's position in the backed up filesystem, prepare to
|
||||
load paths from the archive for eventual display in this widget. Actual loading kicks off in
|
||||
on_mount() below.
|
||||
'''
|
||||
self.config = config
|
||||
self.repository = repository
|
||||
self.archive_name = archive_name
|
||||
self.path_components = path_components or ()
|
||||
self.highlighted_option_changed = False
|
||||
|
||||
super().__init__(classes='panel')
|
||||
|
||||
self.border_title = ' '.join(
|
||||
(
|
||||
'📁',
|
||||
os.path.sep.join(self.path_components)
|
||||
if self.path_components
|
||||
else f'{archive_name}',
|
||||
)
|
||||
)
|
||||
|
||||
if self.path_components:
|
||||
self.add_option(
|
||||
textual.widgets.option_list.Option(
|
||||
'📁 ..',
|
||||
id='..',
|
||||
),
|
||||
)
|
||||
|
||||
self.path_loaded = path_loaded or borgmatic.actions.browse.workers.Archive_path_loaded(
|
||||
self, 'archive path loaded'
|
||||
)
|
||||
|
||||
if not self.path_loaded.complete:
|
||||
self.timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self)
|
||||
|
||||
def on_mount(self):
|
||||
'''
|
||||
When this widget gets mounted in the DOM, subscribe to path loaded events so that we can
|
||||
find out about relevant archive paths as they load. And if this is a root directory list,
|
||||
start loading paths from the archive. If this is a non-root directory list, add any already
|
||||
loaded archive paths to this widget as options.
|
||||
|
||||
Loading is started *after* subscribing to path loaded signals so that there's not a gap
|
||||
where we might miss out on any paths.
|
||||
'''
|
||||
self.path_loaded.subscribe(self, self.on_archive_path_loaded)
|
||||
|
||||
if self.path_components:
|
||||
add_archive_paths(
|
||||
directory_list=self,
|
||||
config=self.config,
|
||||
repository=self.repository,
|
||||
archive_name=self.archive_name,
|
||||
archive_paths=borgmatic.actions.browse.workers.get_paths(
|
||||
self.path_loaded.path_hierarchy, self.path_components
|
||||
),
|
||||
)
|
||||
else:
|
||||
borgmatic.actions.browse.workers.load_archive_paths(
|
||||
self.app,
|
||||
path_loaded=self.path_loaded,
|
||||
config=self.config,
|
||||
repository=self.repository,
|
||||
archive_name=self.archive_name,
|
||||
)
|
||||
|
||||
def on_archive_path_loaded(self, data):
|
||||
'''
|
||||
When an archive path loads, add it as an option to this directory list. But if we get a
|
||||
signal that all path loading is complete, stop and remove our loading indicator.
|
||||
'''
|
||||
if data is borgmatic.actions.browse.workers.LOADING_DONE:
|
||||
self.timer.stop()
|
||||
self.remove_option('loading-indicator')
|
||||
return
|
||||
|
||||
add_archive_paths(
|
||||
directory_list=self,
|
||||
config=self.config,
|
||||
repository=self.repository,
|
||||
archive_name=self.archive_name,
|
||||
archive_paths=(data,),
|
||||
)
|
||||
|
||||
def on_option_list_option_highlighted(self, event):
|
||||
'''
|
||||
When the highlighted option changes, record that fact. This flag is consumed in
|
||||
add_archive_paths() in order to retain the highlighted option even as other options load
|
||||
around it.
|
||||
'''
|
||||
if self.highlighted not in {None, 0}:
|
||||
self.highlighted_option_changed = True
|
||||
@@ -1,87 +0,0 @@
|
||||
import logging
|
||||
|
||||
import rich.syntax
|
||||
import textual.binding
|
||||
import textual.widgets
|
||||
|
||||
import borgmatic.actions.browse.loading
|
||||
import borgmatic.actions.browse.workers
|
||||
|
||||
logger = logging.getLogger('__name__')
|
||||
|
||||
|
||||
class File_preview(textual.widgets.RichLog):
|
||||
'''
|
||||
A widget for extracting and previewing the contents of a file stored in a Borg archive.
|
||||
'''
|
||||
|
||||
BINDINGS = (
|
||||
*textual.widgets.RichLog.BINDINGS,
|
||||
textual.binding.Binding(
|
||||
key='up,k', action='scroll_up', description='scroll up', show=True, priority=True
|
||||
),
|
||||
textual.binding.Binding(
|
||||
key='down,j', action='scroll_down', description='scroll down', show=True, priority=True
|
||||
),
|
||||
textual.binding.Binding(
|
||||
key='pageup', action='page_up', description='page up', show=True, priority=True
|
||||
),
|
||||
textual.binding.Binding(
|
||||
key='pagedown', action='page_down', description='page down', show=True, priority=True
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, config, repository, archive_name, file_path):
|
||||
'''
|
||||
Given a configuration dict, a repository dict, an archive name, and the path of a file in
|
||||
the archive, prepare to load the file's contents for eventual display in this widget. Actual
|
||||
loading kicks off in on_mount() below.
|
||||
'''
|
||||
self.config = config
|
||||
self.repository = repository
|
||||
self.archive_name = archive_name
|
||||
self.file_path = file_path
|
||||
|
||||
super().__init__(classes='panel')
|
||||
self.border_title = f'📄 {self.file_path} preview'
|
||||
self.auto_scroll = False
|
||||
self.file_preview_loaded = borgmatic.actions.browse.workers.File_preview_loaded(
|
||||
self, 'file preview loaded'
|
||||
)
|
||||
|
||||
self.loading_timer = borgmatic.actions.browse.loading.add_inline_loading_indicator(self)
|
||||
|
||||
def on_mount(self):
|
||||
'''
|
||||
When this widget gets mounted in the DOM, subscribe to archive loaded events so that we can
|
||||
find out about archives as they load. Also start loading file contents from the archive.
|
||||
|
||||
Loading is started *after* subscribing to the file preview loaded signal so that there's not
|
||||
a gap where we might miss out on signal publishes.
|
||||
'''
|
||||
self.file_preview_loaded.subscribe(self, self.on_file_preview_loaded)
|
||||
|
||||
borgmatic.actions.browse.workers.load_file_preview(
|
||||
self.app,
|
||||
file_preview_loaded=self.file_preview_loaded,
|
||||
config=self.config,
|
||||
repository=self.repository,
|
||||
archive_name=self.archive_name,
|
||||
file_path=self.file_path,
|
||||
)
|
||||
|
||||
def on_file_preview_loaded(self, file_contents):
|
||||
'''
|
||||
When a file loads, write its contents (syntax highlighted) to this file preview widget.
|
||||
'''
|
||||
self.loading_timer.stop()
|
||||
self.clear()
|
||||
|
||||
if file_contents is None:
|
||||
self.write('Cannot display a preview for this file')
|
||||
else:
|
||||
# Only pass the file path and not its contents to guess_lexer(). Passing the contents is
|
||||
# more accurate, but also much slower.
|
||||
self.write(
|
||||
rich.syntax.Syntax(file_contents, rich.syntax.Syntax.guess_lexer(self.file_path))
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
import borgmatic.actions.browse.archive
|
||||
|
||||
PATH_TYPE_ICONS = {
|
||||
borgmatic.actions.browse.archive.Path_type.DIRECTORY.value: '📁',
|
||||
borgmatic.actions.browse.archive.Path_type.LINK.value: '🔗',
|
||||
borgmatic.actions.browse.archive.Path_type.PIPE.value: '🚰',
|
||||
borgmatic.actions.browse.archive.Path_type.FILE.value: '📄',
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import contextlib
|
||||
import functools
|
||||
import logging
|
||||
|
||||
import textual.widgets
|
||||
import textual.widgets.option_list
|
||||
|
||||
LOADING_DOT_INTERVAL_SECONDS = 0.3
|
||||
|
||||
|
||||
logger = logging.getLogger('__name__')
|
||||
|
||||
|
||||
def update_inline_loading_indicator(widget):
|
||||
'''
|
||||
Given a textual.widgets.OptionList or a textual.widgets.RichLog instance, animate the existing
|
||||
loading indicator inside it.
|
||||
'''
|
||||
if isinstance(widget, textual.widgets.OptionList):
|
||||
with contextlib.suppress(textual.widgets.option_list.OptionDoesNotExist):
|
||||
widget.replace_option_prompt(
|
||||
'loading-indicator',
|
||||
(str(widget.get_option('loading-indicator').prompt) + '.').replace('....', ''),
|
||||
)
|
||||
elif isinstance(widget, textual.widgets.RichLog):
|
||||
with contextlib.suppress(IndexError):
|
||||
loading_message = str(widget.lines[0].text)
|
||||
widget.clear()
|
||||
widget.write((loading_message + '.').replace('....', ''))
|
||||
else:
|
||||
raise ValueError(f'Unsupported widget type: {type(widget)}')
|
||||
|
||||
|
||||
LOADING_MESSAGE = '⏳ loading...'
|
||||
|
||||
|
||||
def add_inline_loading_indicator(widget):
|
||||
'''
|
||||
Given a textual.widgets.OptionList or a textual.widgets.RichLog instance, add a loading
|
||||
indicator to it.
|
||||
'''
|
||||
if isinstance(widget, textual.widgets.OptionList):
|
||||
loading_option = textual.widgets.option_list.Option(LOADING_MESSAGE, id='loading-indicator')
|
||||
widget.add_option(loading_option)
|
||||
widget.highlighted = None
|
||||
elif isinstance(widget, textual.widgets.RichLog):
|
||||
widget.write(LOADING_MESSAGE)
|
||||
else:
|
||||
raise ValueError(f'Unsupported widget type: {type(widget)}')
|
||||
|
||||
return widget.set_interval(
|
||||
LOADING_DOT_INTERVAL_SECONDS,
|
||||
functools.partial(update_inline_loading_indicator, widget),
|
||||
)
|
||||
@@ -1,102 +0,0 @@
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
import textual._context
|
||||
import textual.widgets
|
||||
import textual.worker
|
||||
|
||||
import borgmatic.logger
|
||||
|
||||
|
||||
class Rich_color_formatter(logging.Formatter):
|
||||
'''
|
||||
A Python logging formatter that formats log records with Rich-compatible color markup according
|
||||
to their levels.
|
||||
'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.prefix = None
|
||||
super().__init__(
|
||||
'{prefix}{message}',
|
||||
*args,
|
||||
style='{',
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def format(self, record):
|
||||
'''
|
||||
Given a log record, format it with Rich-compatibe color markup corresponding to its log
|
||||
level.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
color = {
|
||||
logging.CRITICAL: 'bright_red',
|
||||
logging.ERROR: 'bright_red',
|
||||
logging.WARNING: 'bright_yellow',
|
||||
logging.ANSWER: 'bright_magenta',
|
||||
logging.INFO: 'bright_green',
|
||||
logging.DEBUG: 'bright_cyan',
|
||||
}.get(record.levelno)
|
||||
record.prefix = f'{self.prefix}: ' if self.prefix else ''
|
||||
|
||||
return f'[{color}]{super().format(record)}[/{color}]'
|
||||
|
||||
|
||||
class Browse_log_handler(logging.Handler):
|
||||
'''
|
||||
A Python log handler that writes any log records to a logging widget.
|
||||
'''
|
||||
|
||||
def __init__(self, logs_widget):
|
||||
'''
|
||||
Given a logs widget, save it for use below.
|
||||
'''
|
||||
self.logs_widget = logs_widget
|
||||
|
||||
super().__init__()
|
||||
|
||||
def emit(self, record):
|
||||
'''
|
||||
Given a log record, format it and log it to the logs widgets. This works whether or not the
|
||||
logging is happening in the main thread.
|
||||
'''
|
||||
message = self.format(record)
|
||||
|
||||
try:
|
||||
textual.worker.get_current_worker()
|
||||
self.logs_widget.app.call_from_thread(self.logs_widget.write, message)
|
||||
except (RuntimeError, textual.worker.NoActiveWorker):
|
||||
with contextlib.suppress(textual._context.NoActiveAppError):
|
||||
self.logs_widget.write(message)
|
||||
|
||||
|
||||
def log_to_widget(logs_widget):
|
||||
'''
|
||||
Given a Textual RichLog logs widget, add a log handler and formatter that logs to it. Also
|
||||
remove the default borgmatic console log handler so it doesn't try to log all over our UI.
|
||||
'''
|
||||
handler = Browse_log_handler(logs_widget)
|
||||
handler.setFormatter(Rich_color_formatter())
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(min(handler.level for handler in logger.handlers))
|
||||
logger.addHandler(handler)
|
||||
|
||||
with contextlib.suppress(StopIteration):
|
||||
console_handler = next(
|
||||
handler
|
||||
for handler in logging.getLogger().handlers
|
||||
if isinstance(handler, borgmatic.logger.Multi_stream_handler)
|
||||
)
|
||||
logger.removeHandler(console_handler)
|
||||
|
||||
|
||||
class Logs(textual.widgets.RichLog):
|
||||
'''
|
||||
A widget for viewing borgmatic logs in realtime. The log level is determined by borgmatic's
|
||||
current verbosity level.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(markup=True, id='logs', classes='panel')
|
||||
self.border_title = '🪵 logs'
|
||||
@@ -1,30 +0,0 @@
|
||||
import textual.widgets
|
||||
|
||||
import borgmatic.actions.browse.bindings
|
||||
|
||||
|
||||
class Repositories_list(textual.widgets.OptionList):
|
||||
'''
|
||||
A widget for selecting a single Borg repository from among the repositories in a borgmatic
|
||||
configuration file. The item selection event is handled in a Carousel instance, the parent
|
||||
widget of a Repositories_list.
|
||||
'''
|
||||
|
||||
BINDINGS = borgmatic.actions.browse.bindings.OPTION_LIST_BINDINGS
|
||||
|
||||
def __init__(self, config):
|
||||
'''
|
||||
Given a configuration dict, populate the repositories in this widget.
|
||||
'''
|
||||
self.config = config
|
||||
self.repositories = config['repositories']
|
||||
|
||||
super().__init__(
|
||||
*(
|
||||
textual.widgets.option_list.Option(label, id=index)
|
||||
for index, repository in enumerate(self.repositories)
|
||||
for label in (repository.get('label', repository.get('path')),)
|
||||
),
|
||||
classes='panel',
|
||||
)
|
||||
self.border_title = '📦 repositories'
|
||||
@@ -1,29 +0,0 @@
|
||||
import logging
|
||||
|
||||
|
||||
def run_browse(
|
||||
diff_arguments,
|
||||
global_arguments,
|
||||
configs,
|
||||
):
|
||||
'''
|
||||
Run the "browse" action for the given borgmatic configurations. This launches a console UI.
|
||||
|
||||
Raise ValueError if the Textual library (a prerequisite for this action) can't be imported.
|
||||
'''
|
||||
if not configs:
|
||||
return
|
||||
|
||||
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
||||
|
||||
try:
|
||||
import textual # noqa: F401, PLC0415
|
||||
except ImportError: # pragma: no cover
|
||||
raise ValueError(
|
||||
'Unable to import the Textual library for the browse action; try installing "borgmatic[browse]"'
|
||||
)
|
||||
|
||||
import borgmatic.actions.browse.app # noqa: PLC0415
|
||||
|
||||
app = borgmatic.actions.browse.app.Browse_app(configs)
|
||||
app.run()
|
||||
@@ -1,192 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import textual
|
||||
import textual.signal
|
||||
|
||||
import borgmatic.actions.browse.archive
|
||||
|
||||
logger = logging.getLogger('__name__')
|
||||
|
||||
|
||||
LOADING_DONE = object()
|
||||
|
||||
|
||||
class Archive_loaded(textual.signal.Signal):
|
||||
'''
|
||||
A signal that publishes when each subsequent archive is loaded from a repository, intended for
|
||||
consumption in widgets that display archives as they are loaded. This signal also publishes
|
||||
when loading is complete.
|
||||
|
||||
Each subscribed callback call includes the archive as an archive name string. Given the lack of
|
||||
other identifying information (configuration file, repository), there should be a separate
|
||||
Archive_loaded instance per repository.
|
||||
'''
|
||||
|
||||
|
||||
@textual.work(thread=True)
|
||||
def add_repository_archives(browse_app, archive_loaded, config, repository):
|
||||
'''
|
||||
Given a running Browse_app instance, an Archive_loaded instance, a configuration dict, and a
|
||||
repository dict, load a list of the archives from the repository and add them as options in the
|
||||
archives list. Reverse the order so the most recent archive is first.
|
||||
|
||||
This function runs in a separate thread from the main UI. When loading is complete, publish a
|
||||
loading done signal.
|
||||
'''
|
||||
archives_data = borgmatic.actions.browse.archive.get_repository_archives(config, repository)
|
||||
|
||||
# Reverse the archives, so the common case of accessing the latest archive is easy because it's
|
||||
# at the top.
|
||||
for archive in reversed(archives_data['archives']):
|
||||
archive_loaded.publish(archive['archive'])
|
||||
|
||||
archive_loaded.publish(LOADING_DONE)
|
||||
|
||||
|
||||
def record_path(archive_path, hierarchy, path_components):
|
||||
'''
|
||||
Given an Archive_path instance, a dict capturing a filesystem hierarchy of paths, and a tuple of
|
||||
path components for the archive path, set the archive path into the hierarchy data structure.
|
||||
|
||||
For instance, if given an archive path and path components representing "foo/bar/baz.txt",
|
||||
produce a hierarchy that looks like:
|
||||
|
||||
{'foo': {'bar': {'baz.txt': Archive_path('-', 'foo/bar/baz.txt', '')}}}
|
||||
|
||||
Note that the hierarchy is modified in place, so any existing paths there are retained.
|
||||
'''
|
||||
if len(path_components) == 1:
|
||||
hierarchy[path_components[0]] = {} if archive_path.path_type == 'd' else archive_path
|
||||
return
|
||||
|
||||
record_path(archive_path, hierarchy.setdefault(path_components[0], {}), path_components[1:])
|
||||
|
||||
|
||||
def get_paths(hierarchy, path_components, full_path_components=None):
|
||||
'''
|
||||
Given a dict capturing a filesystem hierarchy of paths (or a subset thereof), a tuple of path
|
||||
components for a directory path relative to the hierarchy root, and an optional tuple of
|
||||
*absolute* path components for the same path (if different), return a generator of the
|
||||
contained file and directory Archive_path instances from the hierarchy.
|
||||
|
||||
For instance, given the following hierarchy:
|
||||
|
||||
{'foo': {'bar': {'baz.txt': Archive_path('-', 'foo/bar/baz.txt', ''), 'quux': {}}}}
|
||||
|
||||
... and path components of ('foo', 'bar'), return a generator with the following:
|
||||
|
||||
* Archive_path('-', 'foo/bar/baz.txt', '')
|
||||
* Archive_path('d', 'foo/bar/quux', '')
|
||||
|
||||
The given absolute path components are use to construct directory paths like that last archive
|
||||
path.
|
||||
'''
|
||||
if full_path_components is None:
|
||||
full_path_components = path_components
|
||||
|
||||
if len(path_components) == 1:
|
||||
try:
|
||||
return (
|
||||
archive_path
|
||||
if isinstance(archive_path, borgmatic.actions.browse.archive.Archive_path)
|
||||
else borgmatic.actions.browse.archive.Archive_path(
|
||||
'd', os.path.join(*full_path_components, component), ''
|
||||
)
|
||||
for component, archive_path in hierarchy[path_components[0]].items()
|
||||
)
|
||||
except KeyError:
|
||||
raise ValueError(f'Unknown file or directory: {path_components[0]}')
|
||||
|
||||
try:
|
||||
return get_paths(hierarchy[path_components[0]], path_components[1:], full_path_components)
|
||||
except KeyError:
|
||||
raise ValueError(f'Unknown directory: {path_components[0]}')
|
||||
|
||||
|
||||
class Archive_path_loaded(textual.signal.Signal):
|
||||
'''
|
||||
A signal that publishes when each subsequent path is loaded from an archive, intended for
|
||||
consumption in widgets that display paths as they are loaded. This signal also tracks the
|
||||
complete filesystem hierarchy seen thus far, so new widgets that get created after loading has
|
||||
started can "catch up" with existing known paths. Lastly, this signal publishes and tracks when
|
||||
loading is complete.
|
||||
|
||||
Each subscrided callback call includes the loaded path as an Archive_path instance. There is
|
||||
intended to be a separate Archive_path_loaded instance per archive, but that instance should be
|
||||
shared among several different widgets for the same archive for performance reasons.
|
||||
'''
|
||||
|
||||
def __init__(self, owner, name):
|
||||
self.path_hierarchy = {}
|
||||
self.complete = False
|
||||
|
||||
super().__init__(owner, name)
|
||||
|
||||
def publish(self, archive_path):
|
||||
'''
|
||||
Publish the given archive path to subscribers and record its path locally. But if the
|
||||
archive path is actually LOADING_DONE, then record loading as complete.
|
||||
'''
|
||||
super().publish(archive_path)
|
||||
|
||||
if archive_path is LOADING_DONE:
|
||||
self.complete = True
|
||||
else:
|
||||
record_path(
|
||||
archive_path, self.path_hierarchy, archive_path.file_path.split(os.path.sep)
|
||||
)
|
||||
|
||||
|
||||
@textual.work(thread=True)
|
||||
def load_archive_paths(browse_app, path_loaded, config, repository, archive_name):
|
||||
'''
|
||||
Given a running Browse_app instance, an Archive_path_loaded instance, a configuration dict, a
|
||||
repository dict, and an archive name, load the paths in this archive and publish each one via
|
||||
the Archive_path_loaded signal, so interested widgets can subscribe. Also send a "loading done"
|
||||
signal when loading completes.
|
||||
|
||||
This function runs in a separate thread from the main UI. When loading is complete, publish a
|
||||
loading done signal.
|
||||
'''
|
||||
for archive_path in borgmatic.actions.browse.archive.get_archive_paths(
|
||||
config, repository, archive_name
|
||||
):
|
||||
path_loaded.publish(archive_path)
|
||||
|
||||
path_loaded.publish(LOADING_DONE)
|
||||
|
||||
|
||||
class File_preview_loaded(textual.signal.Signal):
|
||||
'''
|
||||
A signal that publishes when file contents are loaded from an archive, intended for consumption
|
||||
in widgets that display loaded files. This signal also publishes when loading is complete.
|
||||
|
||||
Each published callback includes a the file's contents as a string. Given the lack of other
|
||||
identifying information (configuration file, repository, archive), there should be a separate
|
||||
Archive_loaded instance per previewed file.
|
||||
'''
|
||||
|
||||
|
||||
@textual.work(thread=True)
|
||||
def load_file_preview(
|
||||
browse_app,
|
||||
file_preview_loaded,
|
||||
config,
|
||||
repository,
|
||||
archive_name,
|
||||
file_path,
|
||||
):
|
||||
'''
|
||||
Given a running Browse_app instance, a File_preview_loaded instance, a configuration dict, a
|
||||
repository dict, an archive name, and the path of a file in that archive, load the contents of
|
||||
the file and write it into the given file preview widget.
|
||||
|
||||
This function runs in a separate thread from the main UI. When loading is complete, publish a
|
||||
loading done signal.
|
||||
'''
|
||||
file_contents = borgmatic.actions.browse.archive.get_archive_file_content(
|
||||
config, repository, archive_name, file_path
|
||||
)
|
||||
|
||||
file_preview_loaded.publish(file_contents)
|
||||
@@ -788,7 +788,7 @@ def run_check(
|
||||
'''
|
||||
logger.info('Running consistency checks')
|
||||
|
||||
repository_id = borgmatic.borg.check.get_repository_id(
|
||||
repository_id = borgmatic.borg.repo_info.get_repository_id(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
@@ -844,7 +844,9 @@ def run_check(
|
||||
|
||||
if 'spot' in checks:
|
||||
logger.info('Running spot check')
|
||||
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||
with borgmatic.config.paths.Runtime_directory(
|
||||
config, repository_id
|
||||
) as borgmatic_runtime_directory:
|
||||
spot_check(
|
||||
repository,
|
||||
config,
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import os
|
||||
|
||||
import borgmatic.borg.extract
|
||||
import borgmatic.borg.repo_info
|
||||
import borgmatic.borg.repo_list
|
||||
import borgmatic.config.paths
|
||||
|
||||
@@ -109,8 +110,18 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
|
||||
local_path=bootstrap_arguments.local_path,
|
||||
remote_path=bootstrap_arguments.remote_path,
|
||||
)
|
||||
repository_id = borgmatic.borg.repo_info.get_repository_id(
|
||||
bootstrap_arguments.repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=bootstrap_arguments.local_path,
|
||||
remote_path=bootstrap_arguments.remote_path,
|
||||
)
|
||||
|
||||
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||
with borgmatic.config.paths.Runtime_directory(
|
||||
config, repository_id
|
||||
) as borgmatic_runtime_directory:
|
||||
manifest_config_paths = load_config_paths_from_archive(
|
||||
bootstrap_arguments.repository,
|
||||
archive_name,
|
||||
|
||||
@@ -42,8 +42,18 @@ def run_create(
|
||||
|
||||
logger.info(f'Creating archive{dry_run_label}')
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
repository_id = borgmatic.borg.repo_info.get_repository_id(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
|
||||
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||
with borgmatic.config.paths.Runtime_directory(
|
||||
config, repository_id
|
||||
) as borgmatic_runtime_directory:
|
||||
patterns = pattern.process_patterns(
|
||||
pattern.collect_patterns(config, working_directory),
|
||||
config,
|
||||
|
||||
@@ -536,8 +536,18 @@ def run_restore(
|
||||
'''
|
||||
logger.info(f'Restoring data sources from archive {restore_arguments.archive}')
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
repository_id = borgmatic.borg.repo_info.get_repository_id(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
|
||||
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||
with borgmatic.config.paths.Runtime_directory(
|
||||
config, repository_id
|
||||
) as borgmatic_runtime_directory:
|
||||
patterns = borgmatic.actions.pattern.process_patterns(
|
||||
borgmatic.actions.pattern.collect_patterns(config, working_directory),
|
||||
config,
|
||||
|
||||
@@ -29,6 +29,7 @@ def run_arbitrary_borg(
|
||||
command.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
|
||||
try:
|
||||
options = options[1:] if options[0] == '--' else options
|
||||
@@ -52,7 +53,7 @@ def run_arbitrary_borg(
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ flags.make_flags('lock-wait', lock_wait)
|
||||
+ command_options
|
||||
)
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ def break_lock(
|
||||
argparse.Namespace of global arguments, and optional local and remote Borg paths, break any
|
||||
repository and cache locks leftover from Borg aborting.
|
||||
'''
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('break_lock', '')
|
||||
|
||||
full_command = (
|
||||
|
||||
@@ -24,8 +24,8 @@ def change_passphrase(
|
||||
based on an interactive prompt.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('key_change_passphrase', '')
|
||||
|
||||
full_command = (
|
||||
|
||||
+2
-34
@@ -1,10 +1,8 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
|
||||
import borgmatic.config.paths
|
||||
from borgmatic.borg import environment, feature, flags, repo_info
|
||||
from borgmatic.borg import environment, feature, flags
|
||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,7 +18,7 @@ def make_archive_filter_flags(local_borg_version, config, checks, check_argument
|
||||
flag. And if "prefix" is set in configuration and "archives" is in checks, then include a
|
||||
"--match-archives" flag.
|
||||
'''
|
||||
check_last = config.get('check_last')
|
||||
check_last = config.get('check_last', None)
|
||||
prefix = config.get('prefix')
|
||||
|
||||
if 'archives' in checks or 'data' in checks:
|
||||
@@ -82,36 +80,6 @@ def make_check_name_flags(checks, archive_filter_flags):
|
||||
)
|
||||
|
||||
|
||||
def get_repository_id(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, global
|
||||
arguments, and local/remote commands to run, return the corresponding Borg repository ID.
|
||||
|
||||
Raise ValueError if the Borg repository ID cannot be determined.
|
||||
'''
|
||||
try:
|
||||
return json.loads(
|
||||
repo_info.display_repository_info(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(json=True),
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
),
|
||||
)['repository']['id']
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
raise ValueError(f'Cannot determine Borg repository ID for {repository_path}')
|
||||
|
||||
|
||||
def check_archives(
|
||||
repository_path,
|
||||
config,
|
||||
|
||||
@@ -3,7 +3,7 @@ import shlex
|
||||
|
||||
import borgmatic.config.paths
|
||||
from borgmatic.borg import environment, feature, flags
|
||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||
from borgmatic.execute import execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,8 +22,8 @@ def compact_segments(
|
||||
Given dry-run flag, a local or remote repository path, a configuration dict, and the local Borg
|
||||
version, compact the segments in a repository.
|
||||
'''
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('compact', '')
|
||||
threshold = config.get('compact_threshold')
|
||||
|
||||
@@ -35,7 +35,7 @@ def compact_segments(
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--progress',) if config.get('progress') else ())
|
||||
+ (('--cleanup-commits',) if cleanup_commits else ())
|
||||
+ (('--threshold', str(threshold)) if threshold is not None else ())
|
||||
+ (('--threshold', str(threshold)) if threshold else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (
|
||||
@@ -54,7 +54,6 @@ def compact_segments(
|
||||
execute_command(
|
||||
full_command,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=DO_NOT_CAPTURE if config.get('progress') else None,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=borgmatic.config.paths.get_working_directory(config),
|
||||
borg_local_path=local_path,
|
||||
|
||||
@@ -188,14 +188,14 @@ def make_base_create_command( # noqa: PLR0912
|
||||
patterns,
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
checkpoint_interval = config.get('checkpoint_interval')
|
||||
checkpoint_volume = config.get('checkpoint_volume')
|
||||
chunker_params = config.get('chunker_params')
|
||||
compression = config.get('compression')
|
||||
upload_rate_limit = config.get('upload_rate_limit')
|
||||
upload_buffer_size = config.get('upload_buffer_size')
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
checkpoint_interval = config.get('checkpoint_interval', None)
|
||||
checkpoint_volume = config.get('checkpoint_volume', None)
|
||||
chunker_params = config.get('chunker_params', None)
|
||||
compression = config.get('compression', None)
|
||||
upload_rate_limit = config.get('upload_rate_limit', None)
|
||||
upload_buffer_size = config.get('upload_buffer_size', None)
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
list_filter_flags = flags.make_list_filter_flags(local_borg_version, dry_run)
|
||||
files_changed = config.get('files_changed')
|
||||
files_cache = config.get('files_cache')
|
||||
@@ -374,9 +374,7 @@ def create_archive(
|
||||
|
||||
if json:
|
||||
output_log_level = None
|
||||
elif config.get('list_details') or (
|
||||
(config.get('statistics') or config.get('quick_statistics')) and not dry_run
|
||||
):
|
||||
elif config.get('list_details') or (config.get('statistics') and not dry_run):
|
||||
output_log_level = logging.ANSWER
|
||||
else:
|
||||
output_log_level = logging.INFO
|
||||
@@ -388,11 +386,6 @@ def create_archive(
|
||||
create_flags += (
|
||||
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
|
||||
+ (('--stats',) if config.get('statistics') and not json and not dry_run else ())
|
||||
+ (
|
||||
('--quick-stats',)
|
||||
if config.get('quick_statistics') and not json and not dry_run
|
||||
else ()
|
||||
)
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
|
||||
+ (('--progress',) if config.get('progress') else ())
|
||||
+ (('--json',) if json else ())
|
||||
|
||||
@@ -31,7 +31,8 @@ def diff(
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
lock_wait = config.get('lock_wait')
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
exclude_flags = flags.make_exclude_flags(config)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('diff', '')
|
||||
|
||||
if diff_arguments.only_patterns:
|
||||
@@ -62,6 +63,7 @@ def diff(
|
||||
if patterns_file and diff_arguments.only_patterns
|
||||
else ()
|
||||
)
|
||||
+ exclude_flags
|
||||
+ numeric_ids_flags
|
||||
+ (('--same-chunker-params',) if diff_arguments.same_chunker_params else ())
|
||||
+ (('--sort-by', ','.join(diff_arguments.sort_keys)) if diff_arguments.sort_keys else ())
|
||||
|
||||
@@ -4,8 +4,6 @@ import borgmatic.borg.passcommand
|
||||
import borgmatic.hooks.credential.parse
|
||||
|
||||
OPTION_TO_ENVIRONMENT_VARIABLE = {
|
||||
'archive_hostname': 'BORG_HOSTNAME',
|
||||
'archive_username': 'BORG_USERNAME',
|
||||
'borg_base_directory': 'BORG_BASE_DIR',
|
||||
'borg_config_directory': 'BORG_CONFIG_DIR',
|
||||
'borg_cache_directory': 'BORG_CACHE_DIR',
|
||||
|
||||
@@ -29,8 +29,8 @@ def export_key(
|
||||
Raise FileExistsError if a path is given but it already exists on disk.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('key_export', '')
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ def export_tar_archive(
|
||||
If the destination path is "-", then stream the output to stdout instead of to a file.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('export_tar', '')
|
||||
|
||||
full_command = (
|
||||
|
||||
@@ -104,8 +104,8 @@ def extract_archive(
|
||||
If extract to stdout is True, then start the extraction streaming to stdout, and return that
|
||||
extract process as an instance of subprocess.Popen.
|
||||
'''
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('extract', '')
|
||||
|
||||
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
|
||||
|
||||
@@ -27,8 +27,8 @@ def import_key(
|
||||
|
||||
Raise ValueError if the path is given and it does not exist.
|
||||
'''
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('key_import', '')
|
||||
|
||||
|
||||
@@ -115,10 +115,10 @@ def capture_archive_listing(
|
||||
Given a local or remote repository path, an archive name, a configuration dict, the local Borg
|
||||
version, global arguments as an argparse.Namespace, the archive paths (or Borg patterns) in
|
||||
which to list files, the Borg path format indicating keys to include in the output, and local
|
||||
and remote Borg paths, capture the output of listing that archive and return it as a generator
|
||||
of dicts, one per path.
|
||||
and remote Borg paths, capture the output of listing that archive and return it as a sequence of
|
||||
dicts, one per path.
|
||||
'''
|
||||
return (
|
||||
return tuple(
|
||||
json.loads(entry)
|
||||
for entry in execute_command_and_capture_output(
|
||||
make_list_command(
|
||||
|
||||
@@ -24,8 +24,8 @@ def mount_archive(
|
||||
dict, the local Borg version, global arguments as an argparse.Namespace instance, and optional
|
||||
local and remote Borg paths, mount the archive onto the mount point.
|
||||
'''
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('mount', '')
|
||||
|
||||
full_command = (
|
||||
|
||||
+4
-17
@@ -65,8 +65,8 @@ def prune_archives(
|
||||
archives according to the retention policy specified in that configuration.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
umask = config.get('umask')
|
||||
lock_wait = config.get('lock_wait')
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('prune', '')
|
||||
|
||||
full_command = (
|
||||
@@ -83,23 +83,10 @@ def prune_archives(
|
||||
and not feature.available(feature.Feature.NO_PRUNE_STATS, local_borg_version)
|
||||
else ()
|
||||
)
|
||||
+ (
|
||||
('--quick-stats',)
|
||||
if config.get('quick_statistics')
|
||||
and not dry_run
|
||||
and not feature.available(feature.Feature.NO_PRUNE_STATS, local_borg_version)
|
||||
else ()
|
||||
)
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ flags.make_flags_from_arguments(
|
||||
prune_arguments,
|
||||
excludes=(
|
||||
'repository',
|
||||
'match_archives',
|
||||
'statistics',
|
||||
'quick_statistics',
|
||||
'list_details',
|
||||
),
|
||||
excludes=('repository', 'match_archives', 'statistics', 'list_details'),
|
||||
)
|
||||
+ (('--list',) if config.get('list_details') else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
@@ -108,7 +95,7 @@ def prune_archives(
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
if config.get('statistics') or config.get('quick_statistics') or config.get('list_details'):
|
||||
if config.get('statistics') or config.get('list_details'):
|
||||
output_log_level = logging.ANSWER
|
||||
else:
|
||||
output_log_level = logging.INFO
|
||||
|
||||
@@ -28,14 +28,14 @@ def recreate_archive(
|
||||
arguments, optional local and remote Borg paths, executes the recreate command with the given
|
||||
arguments.
|
||||
'''
|
||||
lock_wait = config.get('lock_wait')
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
exclude_flags = flags.make_exclude_flags(config)
|
||||
compression = config.get('compression')
|
||||
chunker_params = config.get('chunker_params')
|
||||
compression = config.get('compression', None)
|
||||
chunker_params = config.get('chunker_params', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('recreate', '')
|
||||
|
||||
# Available recompress MODES: "if-different", "always", "never" (default)
|
||||
recompress = config.get('recompress')
|
||||
recompress = config.get('recompress', None)
|
||||
|
||||
# Write patterns to a temporary file and use that file with --patterns-from.
|
||||
patterns_file = write_patterns_file(
|
||||
|
||||
@@ -5,7 +5,6 @@ import shlex
|
||||
import subprocess
|
||||
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, feature, flags, repo_info
|
||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||
|
||||
@@ -41,21 +40,17 @@ def create_repository(
|
||||
Raise subprocess.CalledProcessError if "borg info" returns an error exit code.
|
||||
'''
|
||||
try:
|
||||
# Suppress Borg's "repository does not exist" error log, so the user isn't confused by
|
||||
# seeing an error during successful repository creation.
|
||||
with borgmatic.logger.Logs_suppressed(msgid='Repository.DoesNotExist'):
|
||||
info_data = json.loads(
|
||||
repo_info.display_repository_info(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(json=True),
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
),
|
||||
)
|
||||
|
||||
info_data = json.loads(
|
||||
repo_info.display_repository_info(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(json=True),
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
),
|
||||
)
|
||||
repository_encryption_mode = info_data.get('encryption', {}).get('mode')
|
||||
|
||||
if repository_encryption_mode != encryption_mode:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
|
||||
@@ -24,7 +26,7 @@ def display_repository_info(
|
||||
information for the Borg repository or return JSON summary information.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
lock_wait = config.get('lock_wait')
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get(
|
||||
'repo_info' if feature.available(feature.Feature.REPO_INFO, local_borg_version) else 'info',
|
||||
'',
|
||||
@@ -37,6 +39,7 @@ def display_repository_info(
|
||||
if feature.available(feature.Feature.REPO_INFO, local_borg_version)
|
||||
else ('info',)
|
||||
)
|
||||
+ (('--critical',) if repo_info_arguments.json else ())
|
||||
+ (
|
||||
('--info',)
|
||||
if logger.getEffectiveLevel() == logging.INFO and not repo_info_arguments.json
|
||||
@@ -80,3 +83,33 @@ def display_repository_info(
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_repository_id(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, global
|
||||
arguments, and local/remote commands to run, return the corresponding Borg repository ID.
|
||||
|
||||
Raise ValueError if the Borg repository ID cannot be determined.
|
||||
'''
|
||||
try:
|
||||
return json.loads(
|
||||
display_repository_info(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(json=True),
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
),
|
||||
)['repository']['id']
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
raise ValueError(f'Cannot determine Borg repository ID for {repository_path}')
|
||||
|
||||
@@ -31,10 +31,9 @@ ACTION_ALIASES = {
|
||||
'transfer': [],
|
||||
'break-lock': [],
|
||||
'key': [],
|
||||
'borg': [],
|
||||
'recreate': [],
|
||||
'diff': [],
|
||||
'browse': [],
|
||||
'borg': [],
|
||||
}
|
||||
|
||||
|
||||
@@ -325,7 +324,7 @@ def make_argument_description(schema, flag_name):
|
||||
' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).',
|
||||
)
|
||||
|
||||
if example and schema_type in ('array', 'object'):
|
||||
if example and schema_type in ('array', 'object'): # noqa: PLR6201
|
||||
example_buffer = io.StringIO()
|
||||
yaml = ruamel.yaml.YAML(typ='safe')
|
||||
yaml.default_flow_style = True
|
||||
@@ -805,13 +804,6 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
||||
action='store_true',
|
||||
help='Display statistics of the pruned archive [Borg 1 only]',
|
||||
)
|
||||
prune_group.add_argument(
|
||||
'--quick-stats',
|
||||
dest='quick_statistics',
|
||||
default=None,
|
||||
action='store_true',
|
||||
help='Display statistics of the pruned archive, skipping repository-wide "All archives" and chunk index statistics [Borg >= 1.4.5 and < 2 only]',
|
||||
)
|
||||
prune_group.add_argument(
|
||||
'--list',
|
||||
dest='list_details',
|
||||
@@ -904,13 +896,6 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
||||
action='store_true',
|
||||
help='Display statistics of archive',
|
||||
)
|
||||
create_group.add_argument(
|
||||
'--quick-stats',
|
||||
dest='quick_statistics',
|
||||
default=None,
|
||||
action='store_true',
|
||||
help='Display statistics of archive, skipping repository-wide "All archives" and chunk index statistics [Borg 1.4.5+ only]',
|
||||
)
|
||||
create_group.add_argument(
|
||||
'--list',
|
||||
'--files',
|
||||
@@ -2012,9 +1997,8 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
||||
diff_parser = action_parsers.add_parser(
|
||||
'diff',
|
||||
aliases=ACTION_ALIASES['diff'],
|
||||
help='Find differences (file contents, user/group/mode) between archives',
|
||||
description='Find differences (file contents, user/group/mode) between archives',
|
||||
add_help=False,
|
||||
help='This command finds differences (file contents, user/group/mode) between archives',
|
||||
description='This command finds differences (file contents, user/group/mode) between archives',
|
||||
)
|
||||
diff_group = diff_parser.add_argument_group('diff arguments')
|
||||
diff_group.add_argument(
|
||||
@@ -2051,18 +2035,6 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
||||
action='store_true',
|
||||
help='Run the diff according to borgmatic configured patterns (ie do not diff entire archives)',
|
||||
)
|
||||
diff_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
browse_parser = action_parsers.add_parser(
|
||||
'browse',
|
||||
aliases=ACTION_ALIASES['browse'],
|
||||
help='Browse repositories, archives, and files in a console UI',
|
||||
description='Browse repositories, archives, and files in a console UI',
|
||||
add_help=False,
|
||||
)
|
||||
browse_group = browse_parser.add_argument_group('browse arguments')
|
||||
browse_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
borg_parser = action_parsers.add_parser(
|
||||
'borg',
|
||||
aliases=ACTION_ALIASES['borg'],
|
||||
@@ -2135,7 +2107,7 @@ def parse_arguments(schema, *unparsed_arguments):
|
||||
)
|
||||
|
||||
if (
|
||||
('list' in arguments and 'repo-info' in arguments and arguments['list'].json)
|
||||
('list' in arguments and 'repo-info' in arguments and arguments['list'].json) # noqa: PLR0916
|
||||
or ('list' in arguments and 'info' in arguments and arguments['list'].json)
|
||||
or ('repo-info' in arguments and 'info' in arguments and arguments['repo-info'].json)
|
||||
):
|
||||
@@ -2153,7 +2125,7 @@ def parse_arguments(schema, *unparsed_arguments):
|
||||
'With the repo-list action, only one of --prefix or --match-archives flags can be used.',
|
||||
)
|
||||
|
||||
if 'info' in arguments and (
|
||||
if 'info' in arguments and ( # noqa: PLR0916
|
||||
(arguments['info'].archive and arguments['info'].prefix)
|
||||
or (arguments['info'].archive and arguments['info'].match_archives)
|
||||
or (arguments['info'].prefix and arguments['info'].match_archives)
|
||||
|
||||
@@ -12,7 +12,6 @@ import ruamel.yaml
|
||||
|
||||
import borgmatic.actions.borg
|
||||
import borgmatic.actions.break_lock
|
||||
import borgmatic.actions.browse.run
|
||||
import borgmatic.actions.change_passphrase
|
||||
import borgmatic.actions.check
|
||||
import borgmatic.actions.compact
|
||||
@@ -212,7 +211,7 @@ def run_configuration(config_filename, config, config_paths, arguments): # noqa
|
||||
f"Skipping {'/'.join(skip_actions)} action{'s' if len(skip_actions) > 1 else ''} due to configured skip_actions",
|
||||
)
|
||||
|
||||
try:
|
||||
try: # noqa: PLR1702
|
||||
with (
|
||||
Monitoring_hooks(config_filename, config, arguments, global_arguments),
|
||||
borgmatic.hooks.command.Before_after_hooks(
|
||||
@@ -826,8 +825,8 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
||||
try:
|
||||
if 'bootstrap' in arguments:
|
||||
try:
|
||||
# No configuration file is needed for bootstrap.
|
||||
local_borg_version = borg_version.local_borg_version(
|
||||
# No configuration file is needed for bootstrap.
|
||||
{},
|
||||
arguments['bootstrap'].local_path,
|
||||
)
|
||||
@@ -898,13 +897,6 @@ def collect_highlander_action_summary_logs(configs, arguments, configuration_par
|
||||
|
||||
return
|
||||
|
||||
if 'browse' in arguments:
|
||||
borgmatic.actions.browse.run.run_browse(
|
||||
arguments['browse'],
|
||||
arguments['global'],
|
||||
configs,
|
||||
)
|
||||
|
||||
except (
|
||||
CalledProcessError,
|
||||
ValueError,
|
||||
|
||||
+39
-20
@@ -1,6 +1,7 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from enum import Enum
|
||||
|
||||
@@ -84,6 +85,25 @@ def replace_temporary_subdirectory_with_glob(
|
||||
)
|
||||
|
||||
|
||||
class Fixed_name_temporary_directory:
|
||||
'''
|
||||
A class whose instances can stand-in for tempfile.TemporaryDirectory's, except the temporary
|
||||
filename path is fixed rather than randomly generated.
|
||||
'''
|
||||
|
||||
def __init__(self, path):
|
||||
'''
|
||||
Given a temporary directory path, save it off for later.
|
||||
'''
|
||||
self.name = path
|
||||
|
||||
def cleanup(self):
|
||||
'''
|
||||
Remove the temporary directory path.
|
||||
'''
|
||||
shutil.rmtree(self.name, ignore_errors=True)
|
||||
|
||||
|
||||
class Runtime_directory:
|
||||
'''
|
||||
A Python context manager for creating and cleaning up the borgmatic runtime directory used for
|
||||
@@ -98,16 +118,17 @@ class Runtime_directory:
|
||||
automatically gets cleaned up as necessary.
|
||||
'''
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, repository_id):
|
||||
'''
|
||||
Given a configuration dict determine the borgmatic runtime directory, creating a secure,
|
||||
temporary directory within it if necessary. Defaults to $XDG_RUNTIME_DIR/./borgmatic or
|
||||
$RUNTIME_DIRECTORY/./borgmatic or $TMPDIR/borgmatic-[random]/./borgmatic or
|
||||
$TEMP/borgmatic-[random]/./borgmatic or /tmp/borgmatic-[random]/./borgmatic where "[random]"
|
||||
is a randomly generated string intended to avoid path collisions.
|
||||
|
||||
If XDG_RUNTIME_DIR or RUNTIME_DIRECTORY is set and already ends in "/borgmatic", then don't
|
||||
tack on a second "/borgmatic" path component.
|
||||
Given a configuration dict and the Borg ID for a repository, determine the borgmatic runtime
|
||||
directory, creating a secure, temporary directory within it if necessary. Defaults to
|
||||
$XDG_RUNTIME_DIR/borgmatic-[repository_id]/./borgmatic or
|
||||
$RUNTIME_DIRECTORY/borgmatic-[repository_id]/./borgmatic or
|
||||
$TMPDIR/borgmatic-[random]/./borgmatic or $TEMP/borgmatic-[random]/./borgmatic or
|
||||
/tmp/borgmatic-[random]/./borgmatic where "[random]" is a randomly generated string and
|
||||
"[repository_id]" is the Borg repository ID being operated on. Both strings are intended to
|
||||
avoid path collisions, and the random string helps avoid predictable temporary path attacks
|
||||
in shared temporary directories.
|
||||
|
||||
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
|
||||
does not get stored in the file path within an archive. That way, the path of the runtime
|
||||
@@ -125,7 +146,8 @@ class Runtime_directory:
|
||||
if not runtime_directory.startswith(os.path.sep):
|
||||
raise ValueError('The runtime directory must be an absolute path')
|
||||
|
||||
self.temporary_directory = None
|
||||
runtime_directory = os.path.join(runtime_directory, f'borgmatic-{repository_id}')
|
||||
self.temporary_directory = Fixed_name_temporary_directory(runtime_directory)
|
||||
else:
|
||||
base_directory = (
|
||||
os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp' # noqa: S108
|
||||
@@ -141,11 +163,9 @@ class Runtime_directory:
|
||||
)
|
||||
runtime_directory = self.temporary_directory.name
|
||||
|
||||
(base_path, final_directory) = os.path.split(runtime_directory.rstrip(os.path.sep))
|
||||
|
||||
self.runtime_path = expand_user_in_path(
|
||||
os.path.join(
|
||||
base_path if final_directory == 'borgmatic' else runtime_directory,
|
||||
runtime_directory,
|
||||
'.', # Borg 1.4+ "slashdot" hack.
|
||||
'borgmatic',
|
||||
),
|
||||
@@ -162,14 +182,13 @@ class Runtime_directory:
|
||||
|
||||
def __exit__(self, exception_type, exception, traceback):
|
||||
'''
|
||||
Delete any temporary directory that was created as part of initialization.
|
||||
Delete the temporary directory that was created as part of initialization.
|
||||
'''
|
||||
if self.temporary_directory:
|
||||
# The cleanup() call errors if, for instance, there's still a
|
||||
# mounted filesystem within the temporary directory. There's
|
||||
# nothing we can do about that here, so swallow the error.
|
||||
with contextlib.suppress(OSError):
|
||||
self.temporary_directory.cleanup()
|
||||
# The cleanup() call errors if, for instance, there's still a
|
||||
# mounted filesystem within the temporary directory. There's
|
||||
# nothing we can do about that here, so swallow the error.
|
||||
with contextlib.suppress(OSError):
|
||||
self.temporary_directory.cleanup()
|
||||
|
||||
|
||||
def make_runtime_directory_glob(borgmatic_runtime_directory):
|
||||
|
||||
@@ -521,21 +521,6 @@ properties:
|
||||
If match_archives is not specified, borgmatic defaults to deriving
|
||||
the match_archives value from archive_name_format.
|
||||
example: "sh:{hostname}-*"
|
||||
archive_hostname:
|
||||
type: string
|
||||
description: |
|
||||
Hostname to use for the "{hostname}" placeholder in
|
||||
"archive_name_format", "match_archives", etc. Defaults to the system
|
||||
hostname. (This option is supported for Borg 1.4.5+ only.)
|
||||
example: example.org
|
||||
archive_username:
|
||||
type: string
|
||||
description: |
|
||||
Username to use for the "{user}" placeholder in
|
||||
"archive_name_format", "match_archives", etc. Defaults to the
|
||||
username of the user running borgmatic. (This option is supported
|
||||
for Borg 1.4.5+ only.)
|
||||
example: backup_user
|
||||
file_list_format:
|
||||
type: string
|
||||
description: |
|
||||
@@ -1106,16 +1091,6 @@ properties:
|
||||
Corresponds to the "--stats" flag on those actions. Defaults to
|
||||
false.
|
||||
example: true
|
||||
quick_statistics:
|
||||
type: boolean
|
||||
description: |
|
||||
Display statistics for an archive when running supported actions,
|
||||
skipping the repository-wide "All archives" and chunk index
|
||||
statistics to save some time. Corresponds to the "--quick-stats"
|
||||
flag on those actions. Defaults to false. (This option is supported
|
||||
for Borg 1.4.5+ only for the "create" action and for Borg >= 1.4.5
|
||||
and < Borg 2 for the "prune" action.)
|
||||
example: true
|
||||
list_details:
|
||||
type: boolean
|
||||
description: |
|
||||
@@ -1156,9 +1131,8 @@ properties:
|
||||
- info
|
||||
- break-lock
|
||||
- key
|
||||
- diff
|
||||
- browse
|
||||
- borg
|
||||
- diff
|
||||
description: |
|
||||
List of one or more actions to skip running for this configuration
|
||||
file, even if specified on the command-line (explicitly or
|
||||
@@ -1374,9 +1348,8 @@ properties:
|
||||
- info
|
||||
- break-lock
|
||||
- key
|
||||
- diff
|
||||
- browse
|
||||
- borg
|
||||
- diff
|
||||
description: |
|
||||
List of actions for which the commands will be
|
||||
run. Defaults to running for all actions.
|
||||
@@ -1441,9 +1414,8 @@ properties:
|
||||
- info
|
||||
- break-lock
|
||||
- key
|
||||
- diff
|
||||
- browse
|
||||
- borg
|
||||
- diff
|
||||
description: |
|
||||
Only trigger the hook when borgmatic is run with
|
||||
particular actions listed here. Defaults to
|
||||
@@ -1857,27 +1829,6 @@ properties:
|
||||
client and restore server. The default varies based on
|
||||
the MariaDB version.
|
||||
example: false
|
||||
events:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to include scheduled events within the dump.
|
||||
Disable if your database user doesn't have the
|
||||
permissions to dump events. Defaults to true.
|
||||
example: false
|
||||
routines:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to include stored routines within the dump.
|
||||
Disable if your user database doesn't have the
|
||||
permissions to dump routines. Defaults to true.
|
||||
example: false
|
||||
tablespaces:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to include tablespaces within the dump. Disable
|
||||
if your database user doesn't have the permissions to
|
||||
dump tablespaces. Defaults to true.
|
||||
example: false
|
||||
mariadb_dump_command:
|
||||
type: string
|
||||
description: |
|
||||
@@ -2086,27 +2037,6 @@ properties:
|
||||
client and restore server. The default varies based on
|
||||
the MySQL installation.
|
||||
example: false
|
||||
events:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to include scheduled events within the dump.
|
||||
Disable if your database user doesn't have the
|
||||
permissions to dump events. Defaults to true.
|
||||
example: false
|
||||
routines:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to include stored routines within the dump.
|
||||
Disable if your database user doesn't have the
|
||||
permissions to dump routines. Defaults to true.
|
||||
example: false
|
||||
tablespaces:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to include tablespaces within the dump. Disable
|
||||
if your database user doesn't have the permissions to
|
||||
dump tablespaces. Defaults to true.
|
||||
example: false
|
||||
mysql_dump_command:
|
||||
type: string
|
||||
description: |
|
||||
@@ -2895,9 +2825,7 @@ properties:
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
description: |
|
||||
URL of this Apprise service. Supports the
|
||||
"{credential ...}" syntax.
|
||||
description: URL of this Apprise service.
|
||||
example: "gotify://hostname/token"
|
||||
label:
|
||||
type: string
|
||||
|
||||
+7
-34
@@ -157,7 +157,6 @@ def borg_json_log_line_to_record(line, log_level):
|
||||
levelno=log_level,
|
||||
created=log_data.get('time'),
|
||||
msg=log_data.get('message'),
|
||||
msgid=log_data.get('msgid'),
|
||||
levelname=logging.getLevelName(log_level),
|
||||
name=log_data.get('name'),
|
||||
)
|
||||
@@ -168,7 +167,6 @@ def borg_json_log_line_to_record(line, log_level):
|
||||
levelno=borg_log_level,
|
||||
created=log_data.get('time'),
|
||||
msg=log_data.get('message'),
|
||||
msgid=log_data.get('msgid'),
|
||||
levelname=log_data.get('levelname'),
|
||||
name=log_data.get('name'),
|
||||
)
|
||||
@@ -180,7 +178,6 @@ def borg_json_log_line_to_record(line, log_level):
|
||||
levelno=log_level,
|
||||
created=time.time(),
|
||||
msg=f'{log_data.get("status")} {log_data.get("path")}',
|
||||
msgid=log_data.get('msgid'),
|
||||
levelname=logging.getLevelName(log_level),
|
||||
name='borg.file_status',
|
||||
)
|
||||
@@ -375,21 +372,13 @@ def log_buffer_lines(
|
||||
yield log_record.getMessage()
|
||||
|
||||
|
||||
def raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level,
|
||||
borg_local_path,
|
||||
borg_exit_codes,
|
||||
capture_stderr=False,
|
||||
):
|
||||
def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path, borg_exit_codes):
|
||||
'''
|
||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
||||
Process_metadata instance, a requested output log level for stdout, Borg's local path, a
|
||||
sequence of exit code configuration dicts, and whether to capture stderr, check the given
|
||||
processes for error or warning exit codes. If found, vent or kill any running processes and
|
||||
drain any remaining buffer lines. In the case of an error exit code, raise. In the case of
|
||||
warning, return Exit_status.WARNING. Otherwise, return None.
|
||||
Process_metadata instance, Borg's local path, a sequence of exit code configuration dicts, check
|
||||
the given processes for error or warning exit codes. If found, vent or kill any running
|
||||
processes. In the case of an error exit code, raise. In the case of warning, return
|
||||
Exit_status.WARNING. Otherwise, return None.
|
||||
'''
|
||||
result_status = None
|
||||
|
||||
@@ -415,13 +404,6 @@ def raise_for_process_errors(
|
||||
result_status = Exit_status.WARNING
|
||||
continue
|
||||
|
||||
# Drain and log remaining buffer lines, so no output gets lost. But swallow captured output,
|
||||
# since we're raising below instead of yielding captured lines.
|
||||
tuple(
|
||||
log_remaining_buffer_lines(
|
||||
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
|
||||
)
|
||||
)
|
||||
last_lines = process_metadatas[process].last_lines
|
||||
|
||||
# If an error occurs, include its output in the raised exception so that we don't
|
||||
@@ -447,9 +429,6 @@ def log_remaining_buffer_lines(
|
||||
whether to capture stderr, drain and log any remaining output lines from the buffers until
|
||||
they're empty. Additionally, for any log records with a log level the same as the output log
|
||||
level, yield those log messages for capture.
|
||||
|
||||
The main difference between this function and log_buffer_lines() is that this one doesn't just
|
||||
do one "turn of the crank" of logging buffer output; it completely drains any remaining output.
|
||||
'''
|
||||
for output_buffer, reader in buffer_readers.items():
|
||||
if not reader.process:
|
||||
@@ -467,7 +446,6 @@ def log_remaining_buffer_lines(
|
||||
borg_local_path=borg_local_path,
|
||||
command=reader.process.args,
|
||||
),
|
||||
last_lines=process_metadatas[reader.process].last_lines,
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -516,7 +494,7 @@ def log_outputs(
|
||||
for buffer in output_buffers_for_process(process, exclude_stdouts)
|
||||
}
|
||||
|
||||
# Log output lines for each process until they all exit or one errors.
|
||||
# Log output lines for each process until they all exit.
|
||||
while True:
|
||||
yield from log_buffer_lines(
|
||||
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
|
||||
@@ -524,12 +502,7 @@ def log_outputs(
|
||||
|
||||
if (
|
||||
raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level,
|
||||
borg_local_path,
|
||||
borg_exit_codes,
|
||||
capture_stderr,
|
||||
buffer_readers, process_metadatas, borg_local_path, borg_exit_codes
|
||||
)
|
||||
== Exit_status.WARNING
|
||||
):
|
||||
|
||||
@@ -241,11 +241,8 @@ def execute_dump_command(
|
||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||
+ (('--ssl',) if database.get('tls') is True else ())
|
||||
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
||||
+ (('--events',) if database.get('events', True) else ())
|
||||
+ (('--routines',) if database.get('routines', True) else ())
|
||||
+ (('--all-tablespaces',) if database.get('tablespaces', True) else ())
|
||||
+ ('--databases', '--events', '--routines', '--all-tablespaces')
|
||||
+ (('--system=users,udfs,servers',) if SYSTEM_DATABASE_NAME in database_names else ())
|
||||
+ ('--databases',)
|
||||
+ tuple(name for name in database_names if name != SYSTEM_DATABASE_NAME)
|
||||
+ ('--result-file', dump_filename)
|
||||
)
|
||||
@@ -425,7 +422,7 @@ def make_data_source_dump_patterns(
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
):
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
and a database name to match, return the corresponding glob patterns to match the database dump
|
||||
@@ -434,54 +431,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
*(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
*(
|
||||
(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'),
|
||||
name,
|
||||
hostname,
|
||||
port=None,
|
||||
container=container,
|
||||
label=label,
|
||||
),
|
||||
)
|
||||
if port == get_default_port(databases, config)
|
||||
else ()
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
*(
|
||||
(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'),
|
||||
name,
|
||||
hostname,
|
||||
port=get_default_port(databases, config),
|
||||
container=container,
|
||||
label=label,
|
||||
),
|
||||
)
|
||||
if port is None
|
||||
else ()
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ def make_data_source_dump_patterns(
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
):
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
and a database name to match, return the corresponding glob patterns to match the database dump
|
||||
@@ -225,54 +225,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
*(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
*(
|
||||
(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'),
|
||||
name,
|
||||
hostname,
|
||||
port=None,
|
||||
container=container,
|
||||
label=label,
|
||||
),
|
||||
)
|
||||
if port == get_default_port(databases, config)
|
||||
else ()
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
*(
|
||||
(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'),
|
||||
name,
|
||||
hostname,
|
||||
port=get_default_port(databases, config),
|
||||
container=container,
|
||||
label=label,
|
||||
),
|
||||
)
|
||||
if port is None
|
||||
else ()
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -165,10 +165,7 @@ def execute_dump_command(
|
||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||
+ (('--ssl',) if database.get('tls') is True else ())
|
||||
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
||||
+ (('--events',) if database.get('events', True) else ())
|
||||
+ (('--routines',) if database.get('routines', True) else ())
|
||||
+ (('--all-tablespaces',) if database.get('tablespaces', True) else ())
|
||||
+ ('--databases',)
|
||||
+ ('--databases', '--events', '--routines', '--all-tablespaces')
|
||||
+ database_names
|
||||
+ ('--result-file', dump_filename)
|
||||
)
|
||||
@@ -346,7 +343,7 @@ def make_data_source_dump_patterns(
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
):
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
and a database name to match, return the corresponding glob patterns to match the database dump
|
||||
@@ -355,54 +352,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
*(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
*(
|
||||
(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'),
|
||||
name,
|
||||
hostname,
|
||||
port=None,
|
||||
container=container,
|
||||
label=label,
|
||||
),
|
||||
)
|
||||
if port == get_default_port(databases, config)
|
||||
else ()
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
*(
|
||||
(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'),
|
||||
name,
|
||||
hostname,
|
||||
port=get_default_port(databases, config),
|
||||
container=container,
|
||||
label=label,
|
||||
),
|
||||
)
|
||||
if port is None
|
||||
else ()
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ def make_data_source_dump_patterns(
|
||||
port=None,
|
||||
container=None,
|
||||
label=None,
|
||||
):
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
|
||||
and a database name to match, return the corresponding glob patterns to match the database dump
|
||||
@@ -319,54 +319,24 @@ def make_data_source_dump_patterns(
|
||||
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
|
||||
|
||||
return (
|
||||
*(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'), name, hostname, port, container, label
|
||||
),
|
||||
*(
|
||||
(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'),
|
||||
name,
|
||||
hostname,
|
||||
port=None,
|
||||
container=container,
|
||||
label=label,
|
||||
),
|
||||
)
|
||||
if port == get_default_port(databases, config)
|
||||
else ()
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_runtime_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
*(
|
||||
(
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path('borgmatic'),
|
||||
name,
|
||||
hostname,
|
||||
port=get_default_port(databases, config),
|
||||
container=container,
|
||||
label=label,
|
||||
),
|
||||
)
|
||||
if port is None
|
||||
else ()
|
||||
dump.make_data_source_dump_filename(
|
||||
make_dump_path(borgmatic_source_directory),
|
||||
name,
|
||||
hostname,
|
||||
port,
|
||||
container,
|
||||
label,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -403,57 +403,41 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
|
||||
snapshot_dataset_names = {
|
||||
full_snapshot_name.split('@')[0] for full_snapshot_name in full_snapshot_names
|
||||
}
|
||||
hash_to_dataset_mount_point = {}
|
||||
|
||||
# Make a map from mount point hash to the corresponding (dataset name, mount point) tuple.
|
||||
for dataset_name, mount_point in dataset_name_to_mount_point.items():
|
||||
mount_point_hash = hashlib.shake_256(mount_point.encode('utf-8')).hexdigest(
|
||||
MOUNT_POINT_HASH_LENGTH
|
||||
)
|
||||
hash_to_dataset_mount_point[mount_point_hash] = (dataset_name, mount_point)
|
||||
|
||||
for snapshots_directory in glob.glob(snapshots_glob):
|
||||
if not os.path.isdir(snapshots_directory):
|
||||
continue
|
||||
|
||||
# Get the dataset and mount point corresponding to the hash found in this snapshot directory
|
||||
# path. If none is found, bail.
|
||||
try:
|
||||
(dataset_name, mount_point) = hash_to_dataset_mount_point[
|
||||
os.path.basename(snapshots_directory)
|
||||
]
|
||||
except KeyError:
|
||||
continue
|
||||
for dataset_name, mount_point in dataset_name_to_mount_point.items():
|
||||
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
|
||||
|
||||
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
|
||||
|
||||
# If this dataset name doesn't correspond to a known snapshot, then this is probably
|
||||
# just a "shadow" of a nested dataset and therefore there's nothing to unmount.
|
||||
if not os.path.isdir(snapshot_mount_path) or dataset_name not in snapshot_dataset_names:
|
||||
continue
|
||||
|
||||
# This might fail if the path is already mounted, but we swallow errors here since we'll
|
||||
# do another recursive delete below. The point of doing it here is that we don't want to
|
||||
# try to unmount a non-mounted directory (which *will* fail), and probing for whether a
|
||||
# directory is mounted is tough to do in a cross-platform way.
|
||||
if not dry_run:
|
||||
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
|
||||
|
||||
# If the delete was successful, that means there's nothing to unmount.
|
||||
if not os.path.isdir(snapshot_mount_path):
|
||||
# If this dataset name does not correspond to a known snapshot, then this is probably
|
||||
# just a "shadow" of a nested dataset and therefore there's nothing to unmount.
|
||||
if not os.path.isdir(snapshot_mount_path) or dataset_name not in snapshot_dataset_names:
|
||||
continue
|
||||
|
||||
logger.debug(f'Unmounting ZFS snapshot at {snapshot_mount_path}{dry_run_label}')
|
||||
# This might fail if the path is already mounted, but we swallow errors here since we'll
|
||||
# do another recursive delete below. The point of doing it here is that we don't want to
|
||||
# try to unmount a non-mounted directory (which *will* fail), and probing for whether a
|
||||
# directory is mounted is tough to do in a cross-platform way.
|
||||
if not dry_run:
|
||||
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
|
||||
|
||||
if not dry_run:
|
||||
try:
|
||||
unmount_snapshot(umount_command, snapshot_mount_path)
|
||||
except FileNotFoundError:
|
||||
logger.debug(f'Could not find "{umount_command}" command')
|
||||
return
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.debug(error)
|
||||
continue
|
||||
# If the delete was successful, that means there's nothing to unmount.
|
||||
if not os.path.isdir(snapshot_mount_path):
|
||||
continue
|
||||
|
||||
logger.debug(f'Unmounting ZFS snapshot at {snapshot_mount_path}{dry_run_label}')
|
||||
|
||||
if not dry_run:
|
||||
try:
|
||||
unmount_snapshot(umount_command, snapshot_mount_path)
|
||||
except FileNotFoundError:
|
||||
logger.debug(f'Could not find "{umount_command}" command')
|
||||
return
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.debug(error)
|
||||
continue
|
||||
|
||||
if not dry_run:
|
||||
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import operator
|
||||
|
||||
import borgmatic.hooks.credential.parse
|
||||
import borgmatic.hooks.monitoring.logs
|
||||
import borgmatic.hooks.monitoring.monitor
|
||||
|
||||
@@ -45,9 +44,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
import apprise # noqa: PLC0415
|
||||
from apprise import NotifyFormat, NotifyType # noqa: PLC0415
|
||||
except ImportError: # pragma: no cover
|
||||
logger.warning(
|
||||
'Unable to import Apprise in its monitoring hook; try installing "borgmatic[Apprise]"'
|
||||
)
|
||||
logger.warning('Unable to import Apprise in monitoring hook')
|
||||
return
|
||||
|
||||
state_to_notify_type = {
|
||||
@@ -79,12 +76,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
logger.info(f'Pinging Apprise services: {labels_string}{dry_run_string}')
|
||||
|
||||
apprise_object = apprise.Apprise()
|
||||
apprise_object.add(
|
||||
[
|
||||
borgmatic.hooks.credential.parse.resolve_credential(service['url'], config)
|
||||
for service in hook_config.get('services')
|
||||
]
|
||||
)
|
||||
apprise_object.add(list(map(operator.itemgetter('url'), hook_config.get('services'))))
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
+1
-89
@@ -154,8 +154,6 @@ def log_record_to_json(record):
|
||||
'''
|
||||
Given a logging.LogRecord, return it as a JSON-encoded string containing relevant attributes.
|
||||
'''
|
||||
message_id = getattr(record, 'msgid', None)
|
||||
|
||||
return json.dumps(
|
||||
dict(
|
||||
type='log_message',
|
||||
@@ -164,7 +162,6 @@ def log_record_to_json(record):
|
||||
levelname=record.levelname,
|
||||
name=record.name,
|
||||
)
|
||||
| ({'msgid': message_id} if message_id is not None else {})
|
||||
)
|
||||
|
||||
|
||||
@@ -172,7 +169,7 @@ class Json_formatter(logging.Formatter):
|
||||
def __init__(self, fmt='{message}', *args, style='{', **kwargs):
|
||||
super().__init__(*args, fmt=fmt, style=style, **kwargs)
|
||||
|
||||
def format(self, record):
|
||||
def format(self, record): # noqa: PLR6301
|
||||
return log_record_to_json(record)
|
||||
|
||||
|
||||
@@ -339,91 +336,6 @@ class Log_prefix:
|
||||
set_log_prefix(self.original_prefix)
|
||||
|
||||
|
||||
class Log_exclude_filter(logging.Filter):
|
||||
'''
|
||||
A Python log filter that omits log records matching given attributes.
|
||||
'''
|
||||
|
||||
def __init__(self, name, filter_attributes):
|
||||
'''
|
||||
Given a unique name for this filter and a dict of attributes to filter on, set the filter
|
||||
name and save the attributes for use below.
|
||||
'''
|
||||
self.filter_attributes = filter_attributes
|
||||
|
||||
super().__init__(name)
|
||||
|
||||
def filter(self, log_record):
|
||||
'''
|
||||
Given a log record, return False (indicating the record should be omitted) if the record's
|
||||
attributes match any of the saved filter attributes. Return True (indicating do not omit)
|
||||
otherwise.
|
||||
'''
|
||||
for attribute_name, value in self.filter_attributes.items():
|
||||
if getattr(log_record, attribute_name, None) == value:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def add_log_exclude_filter(name, filter_attributes):
|
||||
'''
|
||||
Given a unique filter name and a dict of attributes to filter on, create a log exclude filter
|
||||
with them and add the filter to each log handler.
|
||||
'''
|
||||
for handler in logging.getLogger().handlers:
|
||||
handler.addFilter(Log_exclude_filter(name, filter_attributes))
|
||||
|
||||
|
||||
def remove_log_exclude_filter(name):
|
||||
'''
|
||||
Given a unique filter name, remove matching filters from each log handler.
|
||||
'''
|
||||
for handler in logging.getLogger().handlers:
|
||||
for exclude_filter in handler.filters:
|
||||
if getattr(exclude_filter, 'name', None) == name:
|
||||
handler.removeFilter(exclude_filter)
|
||||
|
||||
|
||||
class Logs_suppressed:
|
||||
'''
|
||||
A Python context manager for temporarily adding a log filter that suppresses requested log
|
||||
records for the duration of the context manager.
|
||||
|
||||
Example use:
|
||||
|
||||
|
||||
with borgmatic.logger.Logs_suppressed(msgid='Repository.DoesNotExist'):
|
||||
do_something_that_logs()
|
||||
|
||||
For the scope of that "with" statement, any records logged with the given message ID are
|
||||
filtered out of the log output. "msgid" is just an example; any logging.LogRecord attributes
|
||||
(standard or custom) can be passed in to filter on.
|
||||
|
||||
Multiple instances of this context manager with different filter attributes can be in use at
|
||||
once.
|
||||
'''
|
||||
|
||||
def __init__(self, **filter_attributes):
|
||||
'''
|
||||
Given the desired log record filter attributes as keyword arguments, save them for use below.
|
||||
'''
|
||||
self.filter_attributes = filter_attributes
|
||||
|
||||
def __enter__(self):
|
||||
'''
|
||||
Create a log filter with the saved filter attributes and add the filter to every logging
|
||||
handler, so that they filter out the desired log records.
|
||||
'''
|
||||
add_log_exclude_filter(name=str(id(self)), filter_attributes=self.filter_attributes)
|
||||
|
||||
def __exit__(self, exception_type, exception, traceback):
|
||||
'''
|
||||
Remove the previously added filter from every logging handler.
|
||||
'''
|
||||
remove_log_exclude_filter(name=str(id(self)))
|
||||
|
||||
|
||||
class Delayed_logging_handler(logging.handlers.BufferingHandler):
|
||||
'''
|
||||
A logging handler that buffers logs and doesn't flush them until explicitly flushed (after
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
|
||||
RUN pip install --break-system-packages --no-cache /app && borgmatic config generate && borgmatic config generate --destination /etc/borgmatic --split && chmod +r /etc/borgmatic/*.yaml
|
||||
RUN mkdir /command-line \
|
||||
&& borgmatic --help > /command-line/global.txt \
|
||||
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" "config show" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key import" "key change-passphrase" recreate diff browse borg; do \
|
||||
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" "config show" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key import" "key change-passphrase" recreate diff borg; do \
|
||||
borgmatic $action --help > /command-line/${action/ /-}.txt; done
|
||||
RUN /app/docs/fetch-contributors >> /contributors.html
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Here's the command-line help for this action in the [most recent version of
|
||||
borgmatic](https://projects.torsion.org/borgmatic-collective/borgmatic/releases).
|
||||
If you're using an older version, some of these flags may not work (or the action
|
||||
may be missing entirely). You should instead run the action with `--help` to see
|
||||
the flags and actions specific to your borgmatic version.
|
||||
If you're using an older version, some of these flags may not work, and you
|
||||
should instead run the action with `--help` to see the flags specific to your
|
||||
borgmatic version.
|
||||
|
||||
@@ -31,7 +31,7 @@ changes work:
|
||||
```bash
|
||||
cd borgmatic
|
||||
uv tool update-shell
|
||||
uv tool install --editable .[dev]
|
||||
uv tool install --editable .
|
||||
```
|
||||
|
||||
Or to work on the [Apprise
|
||||
@@ -39,14 +39,7 @@ hook](https://torsion.org/borgmatic/reference/configuration/monitoring/apprise/)
|
||||
change that last line to:
|
||||
|
||||
```bash
|
||||
uv tool install --editable .[dev,Apprise]
|
||||
```
|
||||
|
||||
Or to work on the [browse
|
||||
action](https://torsion.org/borgmatic/reference/command-line/actions/browse/):
|
||||
|
||||
```bash
|
||||
uv tool install --editable .[dev,browse]
|
||||
uv tool install --editable .[Apprise]
|
||||
```
|
||||
|
||||
To get oriented with the borgmatic source code, have a look at the [source
|
||||
|
||||
@@ -135,52 +135,3 @@ is, patterns are _not_ taken into consideration. If you require this, supply the
|
||||
|
||||
See the [Borg](https://borgbackup.readthedocs.io/en/stable/usage/diff.html)
|
||||
documentation for information on output format, what is compared, and more.
|
||||
|
||||
|
||||
## Browsing backups
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 2.1.6</span> <span
|
||||
class="minilink minilink-addedin">Experimental feature</span> borgmatic has an
|
||||
experimental console UI for browsing your repositories, archives, and files.
|
||||
Here's what it looks like:
|
||||
|
||||
<img src="https://torsion.org/borgmatic/static/browse.png" alt="borgmatic browse screenshot" style="width: 100%">
|
||||
|
||||
This feature is not intended to be a general-purpose Borg UI with every
|
||||
borgmatic feature, but rather it's for use cases like quickly looking at the
|
||||
contents of your backups when you're feeling too lazy to type out a full
|
||||
borgmatic command-line.
|
||||
|
||||
Depending on how you installed borgmatic, it may not have come with the
|
||||
necessary Python libraries to support the browse action. (borgmatic's
|
||||
[stand-alone
|
||||
binary](https://projects.torsion.org/borgmatic-collective/borgmatic/releases)
|
||||
does not currently include them.) If you originally [installed borgmatic with
|
||||
uv](https://torsion.org/borgmatic/how-to/install-borgmatic/), run the following
|
||||
to install the libraries needed for the browse action:
|
||||
|
||||
```bash
|
||||
sudo uv tool install borgmatic[browse]
|
||||
```
|
||||
|
||||
Omit `sudo` if borgmatic is installed as a non-root user.
|
||||
|
||||
Once the libraries are installed, run the following to access the browse action:
|
||||
|
||||
```bash
|
||||
borgmatic browse
|
||||
```
|
||||
|
||||
This launches a console UI where you can select a borgmatic configuration file
|
||||
(if there's more than one), select a Borg repository, select an archive in that
|
||||
repository, and even browse the backed up files in that archive.
|
||||
|
||||
Use the keyboard or the mouse to navigate the UI. The footer at the bottom of
|
||||
the screen shows some of the available keys. Logs show up directly in the UI at
|
||||
the [selected
|
||||
verbosity](https://torsion.org/borgmatic/reference/command-line/logging/),
|
||||
although logs are hidden by default.
|
||||
|
||||
Please [provide
|
||||
feedback](https://torsion.org/borgmatic/#support-and-contributing) if you find
|
||||
this feature useful—or even if you don't, but would like it to become useful.
|
||||
|
||||
@@ -98,7 +98,7 @@ installing borgmatic:
|
||||
|
||||
#### Etc.
|
||||
|
||||
* [stand-alone Linux binary](https://projects.torsion.org/borgmatic-collective/borgmatic/releases) (This is a beta feature! The minimal binary omits [Apprise support](https://torsion.org/borgmatic/reference/configuration/monitoring/apprise/).)
|
||||
* [stand-alone Linux binary](https://projects.torsion.org/borgmatic-collective/borgmatic/releases) (This is a beta feature!)
|
||||
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
|
||||
* [pipx](https://pipx.pypa.io/stable/)
|
||||
|
||||
|
||||
@@ -58,19 +58,25 @@ choice](https://torsion.org/borgmatic/how-to/set-up-backups/#autopilot), each
|
||||
entry using borgmatic's `--config` flag instead of relying on
|
||||
`/etc/borgmatic.d`.
|
||||
|
||||
<a id="limitations"></a>
|
||||
|
||||
## Limitations
|
||||
## Parallelism
|
||||
|
||||
borgmatic does not currently support its own parallelism—being run multiple
|
||||
times on the same machine simultaneously. In particular, many of the [data
|
||||
source
|
||||
hooks](https://torsion.org/borgmatic/reference/configuration/data-sources/) rely
|
||||
on global borgmatic runtime files which can't be shared across processes, and
|
||||
therefore multiple borgmatic instances on the same machine would interfere with
|
||||
each other.
|
||||
<span class="minilink minilink-addedin">New in version 2.1.6</span> borgmatic
|
||||
supports multiple borgmatic instances run on the same machine in parallel—as
|
||||
long as they're operating on different Borg repositories.
|
||||
|
||||
A single borgmatic instance also doesn't currently support running multiple Borg
|
||||
instances in parallel on the same machine.
|
||||
However, note that a single borgmatic instance doesn't currently support running
|
||||
multiple Borg instances in parallel on the same machine.
|
||||
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 2.1.6</span> borgmatic
|
||||
did not support parallel borgmatic runs on the same machine simultaneously. In
|
||||
particular, many of the [data source
|
||||
hooks](https://torsion.org/borgmatic/reference/configuration/data-sources/)
|
||||
relied on global borgmatic runtime files which couldn't be shared across
|
||||
processes, and therefore multiple borgmatic instances on the same machine
|
||||
interfered with each other.
|
||||
|
||||
|
||||
<a id="archive-naming"></a>
|
||||
|
||||
@@ -13,15 +13,16 @@ already.
|
||||
## Hosting providers
|
||||
|
||||
Need somewhere to store your encrypted off-site backups? The following hosting
|
||||
provider includes specific support for Borg/borgmatic—and funds borgmatic
|
||||
development and hosting when you use this referral links to sign up:
|
||||
providers include specific support for Borg/borgmatic—and fund borgmatic
|
||||
development and hosting when you use these referral links to sign up:
|
||||
|
||||
<ul>
|
||||
<li class="referral"><a href="https://www.borgbase.com/?utm_source=borgmatic">BorgBase</a>: Borg hosting service with support for monitoring, 2FA, and append-only repos</li>
|
||||
<li class="referral"><a href="https://hetzner.cloud/?ref=v9dOJ98Ic9I8">Hetzner</a>: A "storage box" that includes support for Borg</li>
|
||||
</ul>
|
||||
|
||||
Additionally, Hetzner and rsync\.net have compatible storage offerings, but do
|
||||
not fund borgmatic development or hosting.
|
||||
Additionally, rsync.net has a compatible storage offering, but does not fund
|
||||
borgmatic development or hosting.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: browse
|
||||
eleventyNavigation:
|
||||
key: browse
|
||||
parent: 🎬 Actions
|
||||
---
|
||||
|
||||
<span class="minilink minilink-addedin">Experimental feature</span> {% include snippet/command-line/sample.md %}
|
||||
|
||||
```
|
||||
{% include borgmatic/command-line/browse.txt %}
|
||||
```
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/how-to/inspect-your-backups/)
|
||||
@@ -24,14 +24,9 @@ databases](https://torsion.org/borgmatic/how-to/backup-your-databases/#all-datab
|
||||
borgmatic excludes most data coming from [MariaDB system
|
||||
databases](https://mariadb.com/docs/server/reference/system-tables),
|
||||
because much of it is populated on MariaDB startup and thus not restorable (or
|
||||
just unnecessary to backup).
|
||||
|
||||
The system data that borgmatic does include in these dumps are: users, roles,
|
||||
grants, user-defined functions, and remote servers—all from the `mysql` system
|
||||
database. This omits all other data from the `mysql` database, which includes
|
||||
index and table statistics, time zones, and installed server plugins. It also
|
||||
excludes the separate `information_schema`, `performance_schema`, and `sys`
|
||||
system databases. This is not currently configurable.
|
||||
just unnecessary to backup). The system data that borgmatic does include in
|
||||
these dumps are: users, roles, grants, user-defined functions, and remote
|
||||
servers.
|
||||
|
||||
Within a Borg archive, you can find this data stored in a dump named `mysql`—the
|
||||
name of the system table this data comes from. And if you'd like to dump this
|
||||
|
||||
@@ -5,9 +5,9 @@ eleventyNavigation:
|
||||
parent: 🚨 Monitoring
|
||||
---
|
||||
<span class="minilink minilink-addedin">New in version 1.8.4</span>
|
||||
[Apprise](https://appriseit.com/) is a local notification library
|
||||
[Apprise](https://github.com/caronc/apprise/wiki) is a local notification library
|
||||
that "allows you to send a notification to almost all of the most popular
|
||||
[notification services](https://appriseit.com/services/) available to
|
||||
[notification services](https://github.com/caronc/apprise/wiki) available to
|
||||
us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc."
|
||||
|
||||
Depending on how you installed borgmatic, it may not have come with Apprise.
|
||||
@@ -22,7 +22,7 @@ sudo uv tool install borgmatic[Apprise]
|
||||
Omit `sudo` if borgmatic is installed as a non-root user.
|
||||
|
||||
Once Apprise is installed, configure borgmatic to notify one or more [Apprise
|
||||
services](https://appriseit.com/services/). For example:
|
||||
services](https://github.com/caronc/apprise/wiki). For example:
|
||||
|
||||
```yaml
|
||||
apprise:
|
||||
|
||||
@@ -29,11 +29,18 @@ probes the following values:
|
||||
You can see the runtime directory path that borgmatic selects by running with
|
||||
`--verbosity 2` and looking for `Using runtime directory` in the output.
|
||||
|
||||
Regardless of the runtime directory selected, borgmatic stores its files
|
||||
within a `borgmatic` subdirectory of the runtime directory. Additionally, in
|
||||
the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a
|
||||
randomly named subdirectory in an effort to reduce path collisions in shared
|
||||
system temporary directories.
|
||||
Regardless of the runtime directory selected, borgmatic stores its files within
|
||||
a `borgmatic` subdirectory of the runtime directory. Additionally, in the case
|
||||
of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a randomly
|
||||
named subdirectory in an effort to reduce path collisions and predictable
|
||||
temporary path attacks in shared temporary directories.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 2.1.6</span> When
|
||||
constructing the runtime directory, borgmatic now creates a subdirectory named
|
||||
after the Borg ID of the current repository. This means that borgmatic supports
|
||||
multiple borgmatic instances run [in
|
||||
parallel](https://torsion.org/borgmatic/how-to/make-per-application-backups/#parallelism)—as
|
||||
long as they're operating on different Borg repositories.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
|
||||
borgmatic created temporary streaming database dumps within the `~/.borgmatic`
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
+4
-7
@@ -1,18 +1,17 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "2.1.7.dev0"
|
||||
version = "2.1.6.dev0"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
description = "Simple, configuration-driven backup software for servers and workstations"
|
||||
license = "GPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: Security :: Cryptography",
|
||||
"Topic :: System :: Archiving :: Backup",
|
||||
@@ -31,8 +30,6 @@ validate-borgmatic-config = "borgmatic.commands.validate_config:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
Apprise = ["apprise"]
|
||||
browse = ["textual", "binaryornot"]
|
||||
browse-dev = ["textual-dev"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://torsion.org/borgmatic"
|
||||
@@ -47,7 +44,7 @@ namespaces = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = "tests"
|
||||
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end --timeout=120 --asyncio-mode=auto"
|
||||
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end --timeout=120"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
@@ -57,7 +54,7 @@ exclude = ["*.*/*"]
|
||||
quote-style = "preserve"
|
||||
|
||||
[tool.ruff.lint]
|
||||
preview = false
|
||||
preview = true
|
||||
extend-select = [
|
||||
"A", # flake8-builtins: builtin shadowing
|
||||
"B", # flake8-bugbear: bugs and design problems
|
||||
|
||||
@@ -60,7 +60,7 @@ ProtectSystem=full
|
||||
# includes, for instance, programs to snapshot filesystems (e.g. ZFS, LVM, and
|
||||
# Btrfs). But rather than commenting this out entirely, one workaround may be to
|
||||
# add "CAP_SYS_ADMIN".
|
||||
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_FOWNER CAP_NET_RAW
|
||||
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_NET_RAW
|
||||
|
||||
# Lower CPU and I/O priority.
|
||||
Nice=19
|
||||
|
||||
+3
-12
@@ -43,19 +43,15 @@ twine upload -r pypi --username __token__ "$wheel_path"
|
||||
scripts/export-docs-from-image
|
||||
docs_path=dist/borgmatic-docs.tar.gz
|
||||
|
||||
# Build stand-alone binaries.
|
||||
# Build stand-alone binary.
|
||||
uv venv --python 3.13 --clear binary
|
||||
source binary/bin/activate
|
||||
uv pip install -r binary_requirements.txt nuitka[onefile]
|
||||
nuitka --mode=onefile --enable-plugin=upx --include-package-data=borgmatic --include-data-dir=borgmatic.egg-info=borgmatic.egg-info --include-package=borgmatic.hooks --include-package=apprise --no-deployment-flag=self-execution borgmatic/commands/borgmatic.py
|
||||
rm -fr borgmatic.build borgmatic.dist
|
||||
deactivate
|
||||
rm -fr binary borgmatic.build borgmatic.dist
|
||||
standalone_binary_path="dist/borgmatic-${version}-binary-linux-glibc-x86_64"
|
||||
mv borgmatic.bin "$standalone_binary_path"
|
||||
nuitka --mode=onefile --enable-plugin=upx --include-package-data=borgmatic --include-data-dir=borgmatic.egg-info=borgmatic.egg-info --include-package=borgmatic.hooks --no-deployment-flag=self-execution borgmatic/commands/borgmatic.py
|
||||
rm -fr binary borgmatic.build borgmatic.dist
|
||||
deactivate
|
||||
standalone_minimal_binary_path="dist/borgmatic-${version}-minimal-binary-linux-glibc-x86_64"
|
||||
mv borgmatic.bin "$standalone_minimal_binary_path"
|
||||
|
||||
# Set release changelogs on projects.torsion.org and GitHub.
|
||||
release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')"
|
||||
@@ -87,11 +83,6 @@ curl --silent --request POST \
|
||||
--header "Authorization: token $projects_token" \
|
||||
--header "Accept: application/json" \
|
||||
--form attachment=@"$standalone_binary_path"
|
||||
curl --silent --request POST \
|
||||
"https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/releases/$release_id/assets?name=$(basename $standalone_minimal_binary_path)" \
|
||||
--header "Authorization: token $projects_token" \
|
||||
--header "Accept: application/json" \
|
||||
--form attachment=@"$standalone_minimal_binary_path"
|
||||
|
||||
github-release create --token="$github_token" --owner=witten --repo=borgmatic --tag="$version" --target_commit="main" \
|
||||
--name="borgmatic $version" --body="$release_changelog"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
apprise
|
||||
attrs
|
||||
binaryornot
|
||||
certifi
|
||||
charset-normalizer
|
||||
click>=8.1.8
|
||||
@@ -17,16 +16,13 @@ packaging
|
||||
pluggy
|
||||
pygments
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-cov
|
||||
pytest-timeout
|
||||
pyyaml>5.0.0
|
||||
referencing
|
||||
requests
|
||||
requests-oauthlib
|
||||
rich
|
||||
rpds-py
|
||||
ruamel-yaml>0.15.0
|
||||
textual
|
||||
typing-extensions
|
||||
urllib3
|
||||
|
||||
+8
-18
@@ -2,39 +2,29 @@
|
||||
# uv pip compile --annotation-style line test_requirements.in -o test_requirements.txt
|
||||
apprise==1.10.0 # via -r test_requirements.in
|
||||
attrs==26.1.0 # via jsonschema, referencing, -r test_requirements.in
|
||||
binaryornot==0.6.0 # via -r test_requirements.in
|
||||
certifi==2026.5.20 # via apprise, requests, -r test_requirements.in
|
||||
certifi==2026.4.22 # via apprise, requests, -r test_requirements.in
|
||||
charset-normalizer==3.4.7 # via requests, -r test_requirements.in
|
||||
click>=8.1.8
|
||||
codespell==2.4.2 # via -r test_requirements.in
|
||||
coverage==7.14.0 # via pytest-cov, -r test_requirements.in
|
||||
coverage==7.13.5 # via pytest-cov, -r test_requirements.in
|
||||
flexmock==0.13.0 # via -r test_requirements.in
|
||||
idna==3.16 # via requests, -r test_requirements.in
|
||||
idna==3.13 # via requests, -r test_requirements.in
|
||||
iniconfig==2.3.0 # via pytest, -r test_requirements.in
|
||||
jsonschema==4.26.0 # via -r test_requirements.in
|
||||
jsonschema-specifications==2025.9.1 # via jsonschema, -r test_requirements.in
|
||||
linkify-it-py==2.1.0 # via markdown-it-py
|
||||
markdown==3.10.2 # via apprise, -r test_requirements.in
|
||||
markdown-it-py==4.2.0 # via mdit-py-plugins, rich, textual
|
||||
mdit-py-plugins==0.6.1 # via textual
|
||||
mdurl==0.1.2 # via markdown-it-py
|
||||
oauthlib==3.3.1 # via requests-oauthlib, -r test_requirements.in
|
||||
packaging==26.2 # via pytest, -r test_requirements.in
|
||||
platformdirs==4.9.6 # via textual
|
||||
pluggy==1.6.0 # via pytest, pytest-cov, -r test_requirements.in
|
||||
pygments==2.20.0 # via pytest, rich, textual, -r test_requirements.in
|
||||
pytest==9.0.3 # via pytest-asyncio, pytest-cov, pytest-timeout, -r test_requirements.in
|
||||
pytest-asyncio==1.3.0 # via -r test_requirements.in
|
||||
pygments==2.20.0 # via pytest, -r test_requirements.in
|
||||
pytest==9.0.3 # via pytest-cov, pytest-timeout, -r test_requirements.in
|
||||
pytest-cov==7.1.0 # via -r test_requirements.in
|
||||
pytest-timeout==2.4.0 # via -r test_requirements.in
|
||||
pyyaml>5.0.0
|
||||
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r test_requirements.in
|
||||
requests==2.34.2 # via apprise, requests-oauthlib, -r test_requirements.in
|
||||
requests==2.33.1 # via apprise, requests-oauthlib, -r test_requirements.in
|
||||
requests-oauthlib==2.0.0 # via apprise, -r test_requirements.in
|
||||
rich==15.0.0 # via textual, -r test_requirements.in
|
||||
rpds-py==0.30.0 # via jsonschema, referencing, -r test_requirements.in
|
||||
ruamel-yaml>0.15.0
|
||||
textual==8.2.7 # via -r test_requirements.in
|
||||
typing-extensions==4.15.0 # via textual, -r test_requirements.in
|
||||
uc-micro-py==2.0.0 # via linkify-it-py
|
||||
urllib3==2.7.0 # via requests, -r test_requirements.in
|
||||
typing-extensions==4.15.0 # via -r test_requirements.in
|
||||
urllib3==2.6.3 # via requests, -r test_requirements.in
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.actions.browse.app
|
||||
import borgmatic.actions.browse.configuration_files_list
|
||||
import borgmatic.actions.browse.logs
|
||||
import borgmatic.actions.browse.repositories_list
|
||||
|
||||
|
||||
async def test_browse_app_with_multiple_configs_uses_configuration_files_list():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test():
|
||||
app.query_one(selector='Header')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 1
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].configs == app.configs
|
||||
|
||||
app.query_one(selector='Logs')
|
||||
app.query_one(selector='Footer')
|
||||
|
||||
|
||||
async def test_browse_app_with_one_config_uses_repositories_list():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test():
|
||||
app.query_one(selector='Header')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 1
|
||||
assert isinstance(
|
||||
carousel.panels[0], borgmatic.actions.browse.repositories_list.Repositories_list
|
||||
)
|
||||
assert carousel.panels[0].config == app.configs['test1.yaml']
|
||||
|
||||
app.query_one(selector='Logs')
|
||||
app.query_one(selector='Footer')
|
||||
|
||||
|
||||
async def test_browse_app_key_toggles_logs_panel():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
logs_panel = app.query_one('#logs')
|
||||
|
||||
await pilot.press('v')
|
||||
await pilot.pause()
|
||||
assert logs_panel.styles.display == 'block'
|
||||
@@ -1,86 +0,0 @@
|
||||
import textual.app
|
||||
import textual.widgets.option_list
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import archives_list as module
|
||||
|
||||
|
||||
async def test_archives_list_on_mount_does_not_raise():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.workers).should_receive('add_repository_archives')
|
||||
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
|
||||
flexmock(archives_list.archive_loaded).should_receive('subscribe')
|
||||
|
||||
async with textual.app.App().run_test():
|
||||
archives_list.on_mount()
|
||||
|
||||
|
||||
def test_archives_list_on_archive_loaded_with_loading_done_removes_loading_indicator():
|
||||
loading_timer = flexmock()
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive(
|
||||
'add_inline_loading_indicator'
|
||||
).and_return(loading_timer)
|
||||
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
|
||||
flexmock(loading_timer).should_receive('stop')
|
||||
flexmock(archives_list).should_receive('remove_option').with_args('loading-indicator').once()
|
||||
flexmock(archives_list).should_receive('add_options').never()
|
||||
|
||||
archives_list.on_archive_loaded(module.borgmatic.actions.browse.workers.LOADING_DONE)
|
||||
|
||||
|
||||
def test_archives_list_on_archive_loaded_adds_archive_name():
|
||||
loading_timer = flexmock()
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive(
|
||||
'add_inline_loading_indicator'
|
||||
).and_return(loading_timer)
|
||||
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
|
||||
flexmock(loading_timer).should_receive('stop').never()
|
||||
loading_indicator = flexmock()
|
||||
flexmock(archives_list).should_receive('get_option').and_return(loading_indicator)
|
||||
flexmock(archives_list).should_receive('remove_option').with_args('loading-indicator').once()
|
||||
flexmock(archives_list).should_receive('add_options').once()
|
||||
|
||||
archives_list.on_archive_loaded('archive')
|
||||
|
||||
|
||||
def test_archives_list_on_option_list_option_highlighted_with_highlighted_none_marks_it_unchanged():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
|
||||
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
|
||||
archives_list.highlighted = None
|
||||
archives_list.on_option_list_option_highlighted(event=flexmock())
|
||||
|
||||
assert archives_list.highlighted_option_changed is False
|
||||
|
||||
|
||||
def test_archives_list_on_option_list_option_highlighted_with_highlighted_zero_marks_it_unchanged():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
|
||||
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
|
||||
archives_list.highlighted = 0
|
||||
archives_list.on_option_list_option_highlighted(event=flexmock())
|
||||
|
||||
assert archives_list.highlighted_option_changed is False
|
||||
|
||||
|
||||
def test_archives_list_on_option_list_option_highlighted_with_existing_option_and_highlighted_zero_marks_it_unchanged():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
|
||||
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
|
||||
archives_list.add_option(textual.widgets.option_list.Option('zero', id='zero'))
|
||||
archives_list.highlighted = 0
|
||||
archives_list.on_option_list_option_highlighted(event=flexmock())
|
||||
|
||||
assert archives_list.highlighted_option_changed is False
|
||||
|
||||
|
||||
def test_archives_list_on_option_list_option_highlighted_with_highlighted_non_zero_marks_it_changed():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
|
||||
archives_list = module.Archives_list(config=flexmock(), repository=flexmock())
|
||||
archives_list.add_option(textual.widgets.option_list.Option('zero', id='zero'))
|
||||
archives_list.add_option(textual.widgets.option_list.Option('one', id='one'))
|
||||
archives_list.highlighted = 1
|
||||
archives_list.on_option_list_option_highlighted(event=flexmock())
|
||||
|
||||
assert archives_list.highlighted_option_changed is True
|
||||
@@ -1,522 +0,0 @@
|
||||
import pytest
|
||||
import textual.widgets.option_list
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.actions.browse.app
|
||||
import borgmatic.actions.browse.archive
|
||||
import borgmatic.actions.browse.carousel
|
||||
import borgmatic.actions.browse.loading
|
||||
import borgmatic.actions.browse.logs
|
||||
import borgmatic.actions.browse.workers
|
||||
from borgmatic.actions.browse import carousel as module
|
||||
|
||||
|
||||
def test_make_next_panel_with_configuration_files_list_returns_repositories_list():
|
||||
configs = {'test.yaml': {'repositories': [{'path': 'test.borg'}]}}
|
||||
|
||||
repositories_list = module.make_next_panel(
|
||||
focused_panel=borgmatic.actions.browse.configuration_files_list.Configuration_files_list(
|
||||
configs
|
||||
),
|
||||
option_id='test.yaml',
|
||||
)
|
||||
|
||||
assert isinstance(
|
||||
repositories_list, borgmatic.actions.browse.repositories_list.Repositories_list
|
||||
)
|
||||
assert repositories_list.config == configs['test.yaml']
|
||||
|
||||
|
||||
def test_make_next_panel_with_repositories_list_returns_archives_list():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('add_repository_archives')
|
||||
flexmock(borgmatic.actions.browse.archives_list.Archives_list).should_receive('app').and_return(
|
||||
flexmock()
|
||||
)
|
||||
|
||||
archives_list = module.make_next_panel(
|
||||
focused_panel=borgmatic.actions.browse.repositories_list.Repositories_list(config),
|
||||
option_id=0,
|
||||
)
|
||||
|
||||
assert isinstance(archives_list, borgmatic.actions.browse.archives_list.Archives_list)
|
||||
assert archives_list.config == config
|
||||
assert archives_list.repository == config['repositories'][0]
|
||||
|
||||
|
||||
def test_make_next_panel_with_archives_list_returns_directory_list():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('add_repository_archives')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('load_archive_paths')
|
||||
flexmock(borgmatic.actions.browse.archives_list.Archives_list).should_receive('app').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
|
||||
directory_list = module.make_next_panel(
|
||||
focused_panel=borgmatic.actions.browse.archives_list.Archives_list(
|
||||
config, config['repositories'][0]
|
||||
),
|
||||
option_id='archive',
|
||||
)
|
||||
|
||||
assert isinstance(directory_list, borgmatic.actions.browse.directory_list.Directory_list)
|
||||
assert directory_list.config == config
|
||||
assert directory_list.repository == config['repositories'][0]
|
||||
|
||||
|
||||
def test_make_next_panel_with_root_directory_list_and_selected_directory_option_returns_new_directory_list():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('load_archive_paths')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('Archive_path_loaded').replace_with(
|
||||
flexmock(
|
||||
path_hierarchy={
|
||||
'etc': {},
|
||||
},
|
||||
complete=False,
|
||||
),
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
focused_panel = borgmatic.actions.browse.directory_list.Directory_list(
|
||||
config, config['repositories'][0], 'archive'
|
||||
)
|
||||
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📁 etc'))
|
||||
|
||||
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='etc')
|
||||
|
||||
assert isinstance(directory_list, borgmatic.actions.browse.directory_list.Directory_list)
|
||||
assert directory_list.config == config
|
||||
assert directory_list.repository == config['repositories'][0]
|
||||
assert directory_list.archive_name == 'archive'
|
||||
assert directory_list.path_components == ('etc',)
|
||||
|
||||
|
||||
def test_make_next_panel_with_non_root_directory_list_and_selected_directory_option_returns_new_directory_list():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('load_archive_paths')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('Archive_path_loaded').replace_with(
|
||||
flexmock(
|
||||
path_hierarchy={
|
||||
'etc': {
|
||||
'borgmatic': {},
|
||||
},
|
||||
},
|
||||
complete=False,
|
||||
),
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
focused_panel = borgmatic.actions.browse.directory_list.Directory_list(
|
||||
config,
|
||||
config['repositories'][0],
|
||||
'archive',
|
||||
path_components=('etc',),
|
||||
)
|
||||
flexmock(focused_panel).should_receive('get_option').and_return(flexmock(prompt='📁 borgmatic'))
|
||||
|
||||
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='borgmatic')
|
||||
|
||||
assert isinstance(directory_list, borgmatic.actions.browse.directory_list.Directory_list)
|
||||
assert directory_list.config == config
|
||||
assert directory_list.repository == config['repositories'][0]
|
||||
assert directory_list.archive_name == 'archive'
|
||||
assert directory_list.path_components == ('etc', 'borgmatic')
|
||||
|
||||
|
||||
def test_make_next_panel_with_root_directory_list_and_selected_file_option_returns_new_file_preview():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('load_archive_paths')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('load_file_preview')
|
||||
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
flexmock(borgmatic.actions.browse.file_preview.File_preview).should_receive('app').and_return(
|
||||
flexmock()
|
||||
)
|
||||
focused_panel = borgmatic.actions.browse.directory_list.Directory_list(
|
||||
config, config['repositories'][0], 'archive'
|
||||
)
|
||||
flexmock(focused_panel).should_receive('get_option').and_return(
|
||||
flexmock(prompt='📄 config.yaml')
|
||||
)
|
||||
|
||||
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml')
|
||||
|
||||
assert isinstance(directory_list, borgmatic.actions.browse.file_preview.File_preview)
|
||||
assert directory_list.config == config
|
||||
assert directory_list.repository == config['repositories'][0]
|
||||
assert directory_list.archive_name == 'archive'
|
||||
assert directory_list.file_path == 'config.yaml'
|
||||
|
||||
|
||||
def test_make_next_panel_with_non_root_directory_list_and_selected_file_option_returns_new_file_preview():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('load_archive_paths')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('load_file_preview')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('Archive_path_loaded').replace_with(
|
||||
flexmock(
|
||||
path_hierarchy={
|
||||
'etc': {
|
||||
'borgmatic': {
|
||||
'config.yaml': borgmatic.actions.browse.archive.Archive_path(
|
||||
'-', 'config.yaml', ''
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
complete=False,
|
||||
),
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
flexmock(borgmatic.actions.browse.file_preview.File_preview).should_receive('app').and_return(
|
||||
flexmock()
|
||||
)
|
||||
focused_panel = borgmatic.actions.browse.directory_list.Directory_list(
|
||||
config,
|
||||
config['repositories'][0],
|
||||
'archive',
|
||||
path_components=('etc', 'borgmatic'),
|
||||
)
|
||||
flexmock(focused_panel).should_receive('get_option').and_return(
|
||||
flexmock(prompt='📄 config.yaml')
|
||||
)
|
||||
|
||||
directory_list = module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml')
|
||||
|
||||
assert isinstance(directory_list, borgmatic.actions.browse.file_preview.File_preview)
|
||||
assert directory_list.config == config
|
||||
assert directory_list.repository == config['repositories'][0]
|
||||
assert directory_list.archive_name == 'archive'
|
||||
assert directory_list.file_path == 'etc/borgmatic/config.yaml'
|
||||
|
||||
|
||||
def test_make_next_panel_with_unsupported_focused_panel_returns_none():
|
||||
assert module.make_next_panel(focused_panel=flexmock(), option_id='hmmm') is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('icon', ('🔗', '🚰', '🐙'))
|
||||
def test_make_next_panel_with_directory_list_and_unsupported_selected_option_returns_none(icon):
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('load_archive_paths')
|
||||
flexmock(borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
focused_panel = borgmatic.actions.browse.directory_list.Directory_list(
|
||||
config, config['repositories'][0], 'archive'
|
||||
)
|
||||
flexmock(focused_panel).should_receive('get_option').and_return(
|
||||
flexmock(prompt=f'{icon} config.yaml')
|
||||
)
|
||||
|
||||
assert module.make_next_panel(focused_panel=focused_panel, option_id='config.yaml') is None
|
||||
|
||||
|
||||
async def test_carousel_previous_action_with_multiple_configs_does_not_raise():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('left')
|
||||
|
||||
|
||||
async def test_carousel_previous_action_with_one_config_does_not_raise():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('left')
|
||||
|
||||
|
||||
async def test_carousel_next_action_with_multiple_configs_advances_panel():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('enter')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 2
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'none'
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[1], borgmatic.actions.browse.repositories_list.Repositories_list
|
||||
)
|
||||
assert carousel.panels[1].styles.display == 'block'
|
||||
assert carousel.panels[1].highlighted == 0
|
||||
assert app.focused == carousel.panels[1]
|
||||
|
||||
|
||||
async def test_carousel_next_action_with_one_config_advances_panel():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('add_repository_archives')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('enter')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 2
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0], borgmatic.actions.browse.repositories_list.Repositories_list
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'none'
|
||||
|
||||
assert isinstance(carousel.panels[1], borgmatic.actions.browse.archives_list.Archives_list)
|
||||
assert carousel.panels[1].styles.display == 'block'
|
||||
assert carousel.panels[1].highlighted == 0
|
||||
assert app.focused == carousel.panels[1]
|
||||
|
||||
|
||||
async def test_carousel_next_action_with_no_next_panel_does_not_advance():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
flexmock(borgmatic.actions.browse.workers).should_receive('add_repository_archives')
|
||||
flexmock(module).should_receive('make_next_panel')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('enter')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 1
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0], borgmatic.actions.browse.repositories_list.Repositories_list
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'block'
|
||||
assert app.focused == carousel.panels[0]
|
||||
|
||||
|
||||
async def test_carousel_next_action_and_previous_action_returns_to_original_panel():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('enter')
|
||||
await pilot.press('left')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 2
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'block'
|
||||
assert carousel.panels[0].highlighted == 0
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[1], borgmatic.actions.browse.repositories_list.Repositories_list
|
||||
)
|
||||
assert carousel.panels[1].styles.display == 'none'
|
||||
assert app.focused == carousel.panels[0]
|
||||
|
||||
|
||||
async def test_carousel_next_action_and_previous_action_and_next_action_reuses_next_panel():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
flexmock(borgmatic.actions.browse.carousel).should_call('make_next_panel').once()
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('enter')
|
||||
await pilot.press('left')
|
||||
await pilot.press('enter')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 2
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'none'
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[1], borgmatic.actions.browse.repositories_list.Repositories_list
|
||||
)
|
||||
assert carousel.panels[1].styles.display == 'block'
|
||||
assert carousel.panels[1].highlighted == 0
|
||||
assert app.focused == carousel.panels[1]
|
||||
|
||||
|
||||
async def test_carousel_next_action_with_multiple_configs_and_no_next_panel_does_not_advance():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
flexmock(module).should_receive('make_next_panel').and_return(None)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('enter')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 1
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'block'
|
||||
assert carousel.panels[0].highlighted == 0
|
||||
assert app.focused == carousel.panels[0]
|
||||
|
||||
|
||||
async def test_carousel_next_action_and_previous_action_and_down_truncates_next_panel():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('enter')
|
||||
await pilot.press('left')
|
||||
await pilot.press('down')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 1
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'block'
|
||||
assert carousel.panels[0].highlighted == 1
|
||||
assert app.focused == carousel.panels[0]
|
||||
|
||||
|
||||
async def test_carousel_down_does_not_raise():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('down')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 1
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'block'
|
||||
assert carousel.panels[0].highlighted == 1
|
||||
assert app.focused == carousel.panels[0]
|
||||
|
||||
|
||||
async def test_carousel_up_does_not_raise():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('down')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
assert len(carousel.panels) == 1
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'block'
|
||||
assert carousel.panels[0].highlighted == 1
|
||||
assert app.focused == carousel.panels[0]
|
||||
|
||||
|
||||
async def test_carousel_next_action_and_select_dot_dot_returns_to_original_panel():
|
||||
app = borgmatic.actions.browse.app.Browse_app(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
flexmock(borgmatic.actions.browse.logs).should_receive('log_to_widget')
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press('enter')
|
||||
|
||||
carousel = app.query_one(selector='Carousel')
|
||||
carousel.panels[1].options[0] = textual.widgets.option_list.Option('..', id='..')
|
||||
|
||||
await pilot.press('enter')
|
||||
|
||||
assert len(carousel.panels) == 2
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[0],
|
||||
borgmatic.actions.browse.configuration_files_list.Configuration_files_list,
|
||||
)
|
||||
assert carousel.panels[0].styles.display == 'block'
|
||||
assert carousel.panels[0].highlighted == 0
|
||||
|
||||
assert isinstance(
|
||||
carousel.panels[1], borgmatic.actions.browse.repositories_list.Repositories_list
|
||||
)
|
||||
assert carousel.panels[1].styles.display == 'none'
|
||||
assert app.focused == carousel.panels[0]
|
||||
@@ -1,34 +0,0 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import configuration_files_list as module
|
||||
|
||||
|
||||
def test_configuration_files_list_adds_config_paths_as_options():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('/home/user')
|
||||
|
||||
configuration_files_list = module.Configuration_files_list(
|
||||
configs={
|
||||
'test1.yaml': {'repositories': [{'path': 'test1.borg'}]},
|
||||
'test2.yaml': {'repositories': [{'path': 'test2.borg'}]},
|
||||
}
|
||||
)
|
||||
|
||||
assert len(configuration_files_list.options) == 2
|
||||
assert configuration_files_list.options[0].prompt == 'test1.yaml'
|
||||
assert configuration_files_list.options[0].id == 'test1.yaml'
|
||||
assert configuration_files_list.options[1].prompt == 'test2.yaml'
|
||||
assert configuration_files_list.options[1].id == 'test2.yaml'
|
||||
|
||||
|
||||
def test_configuration_files_list_collapses_home_directory_in_config_path_option():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('/home/user')
|
||||
|
||||
configuration_files_list = module.Configuration_files_list(
|
||||
configs={
|
||||
'/home/user/test.yaml': {'repositories': [{'path': '/home/user/test.borg'}]},
|
||||
}
|
||||
)
|
||||
|
||||
assert len(configuration_files_list.options) == 1
|
||||
assert configuration_files_list.options[0].prompt == '~/test.yaml'
|
||||
assert configuration_files_list.options[0].id == '/home/user/test.yaml'
|
||||
@@ -1,287 +0,0 @@
|
||||
import textual.widgets
|
||||
import textual.widgets.option_list
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import directory_list as module
|
||||
|
||||
|
||||
def test_add_archive_paths_with_only_duplicate_paths_bails():
|
||||
directory_list = textual.widgets.OptionList()
|
||||
directory_list.path_components = ('etc',)
|
||||
directory_list.add_option(textual.widgets.option_list.Option('foo', id='foo'))
|
||||
directory_list.add_option(textual.widgets.option_list.Option('bar', id='bar'))
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(directory_list).should_receive('set_options').never()
|
||||
|
||||
module.add_archive_paths(
|
||||
directory_list=directory_list,
|
||||
config=config,
|
||||
repository=config['repositories'][0],
|
||||
archive_name='archive',
|
||||
archive_paths=(
|
||||
flexmock(path_type='-', file_path='etc/foo/one.txt', link_target=''),
|
||||
flexmock(path_type='-', file_path='etc/foo/two.txt', link_target=''),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_add_archive_paths_adds_ands_sorts_and_filters_and_deduplicates():
|
||||
directory_list = textual.widgets.OptionList()
|
||||
directory_list.path_components = ('etc',)
|
||||
directory_list.add_option(textual.widgets.option_list.Option('📄 foo', id='foo'))
|
||||
directory_list.add_option(textual.widgets.option_list.Option('📄 bar', id='bar'))
|
||||
directory_list.highlighted = 1
|
||||
directory_list.highlighted_option_changed = True
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
|
||||
module.add_archive_paths(
|
||||
directory_list=directory_list,
|
||||
config=config,
|
||||
repository=config['repositories'][0],
|
||||
archive_name='archive',
|
||||
archive_paths=(
|
||||
flexmock(path_type='d', file_path='etc/quux', link_target=''),
|
||||
flexmock(path_type='-', file_path='etc/foo', link_target=''),
|
||||
flexmock(path_type='-', file_path='etc/baz', link_target=''),
|
||||
flexmock(path_type='d', file_path='root/nope', link_target=''),
|
||||
flexmock(path_type='d', file_path='etc/other', link_target=''),
|
||||
),
|
||||
)
|
||||
|
||||
assert len(directory_list.options) == 5
|
||||
assert directory_list.options[0].prompt == '📁 other'
|
||||
assert directory_list.options[0].id == 'other'
|
||||
assert directory_list.options[1].prompt == '📁 quux'
|
||||
assert directory_list.options[1].id == 'quux'
|
||||
assert directory_list.options[2].prompt == '📄 bar'
|
||||
assert directory_list.options[2].id == 'bar'
|
||||
assert directory_list.options[3].prompt == '📄 baz'
|
||||
assert directory_list.options[3].id == 'baz'
|
||||
assert directory_list.options[4].prompt == '📄 foo'
|
||||
assert directory_list.options[4].id == 'foo'
|
||||
assert directory_list.highlighted == 2
|
||||
|
||||
|
||||
def test_add_archive_paths_highlights_first_option_if_highlight_has_not_changed():
|
||||
directory_list = textual.widgets.OptionList()
|
||||
directory_list.path_components = ('etc',)
|
||||
directory_list.add_option(textual.widgets.option_list.Option('📄 foo', id='foo'))
|
||||
directory_list.add_option(textual.widgets.option_list.Option('📄 bar', id='bar'))
|
||||
directory_list.highlighted = None
|
||||
directory_list.highlighted_option_changed = False
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
|
||||
module.add_archive_paths(
|
||||
directory_list=directory_list,
|
||||
config=config,
|
||||
repository=config['repositories'][0],
|
||||
archive_name='archive',
|
||||
archive_paths=(flexmock(path_type='-', file_path='etc/baz', link_target=''),),
|
||||
)
|
||||
|
||||
assert len(directory_list.options) == 3
|
||||
assert directory_list.options[0].prompt == '📄 bar'
|
||||
assert directory_list.options[0].id == 'bar'
|
||||
assert directory_list.options[1].prompt == '📄 baz'
|
||||
assert directory_list.options[1].id == 'baz'
|
||||
assert directory_list.options[2].prompt == '📄 foo'
|
||||
assert directory_list.options[2].id == 'foo'
|
||||
assert directory_list.highlighted == 0
|
||||
|
||||
|
||||
def test_add_archive_paths_retains_loading_indicator_at_bottom():
|
||||
directory_list = textual.widgets.OptionList()
|
||||
directory_list.path_components = ('etc',)
|
||||
directory_list.add_option(textual.widgets.option_list.Option('📄 foo', id='foo'))
|
||||
directory_list.add_option(textual.widgets.option_list.Option('📄 bar', id='bar'))
|
||||
directory_list.add_option(
|
||||
textual.widgets.option_list.Option('loading!!!', id='loading-indicator')
|
||||
)
|
||||
directory_list.highlighted = 0
|
||||
directory_list.highlighted_option_changed = True
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
|
||||
module.add_archive_paths(
|
||||
directory_list=directory_list,
|
||||
config=config,
|
||||
repository=config['repositories'][0],
|
||||
archive_name='archive',
|
||||
archive_paths=(flexmock(path_type='-', file_path='etc/baz', link_target=''),),
|
||||
)
|
||||
|
||||
assert len(directory_list.options) == 4
|
||||
assert directory_list.options[0].prompt == '📄 bar'
|
||||
assert directory_list.options[0].id == 'bar'
|
||||
assert directory_list.options[1].prompt == '📄 baz'
|
||||
assert directory_list.options[1].id == 'baz'
|
||||
assert directory_list.options[2].prompt == '📄 foo'
|
||||
assert directory_list.options[2].id == 'foo'
|
||||
assert directory_list.options[3].prompt == 'loading!!!'
|
||||
assert directory_list.options[3].id == 'loading-indicator'
|
||||
assert directory_list.highlighted == 2
|
||||
|
||||
|
||||
def test_directory_list_with_root_directory_adds_loading_indicator():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive(
|
||||
'add_inline_loading_indicator'
|
||||
).once()
|
||||
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(), repository=flexmock(), archive_name='archive'
|
||||
)
|
||||
assert directory_list.border_title == '📁 archive'
|
||||
assert len(directory_list.options) == 0
|
||||
assert not directory_list.path_loaded.complete
|
||||
|
||||
|
||||
def test_directory_list_with_already_complete_loading_skips_loading_indicator():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive(
|
||||
'add_inline_loading_indicator'
|
||||
).never()
|
||||
flexmock(module.borgmatic.actions.browse.workers).should_receive('load_archive_paths').never()
|
||||
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
path_loaded=flexmock(complete=True),
|
||||
path_components=('etc',),
|
||||
)
|
||||
assert directory_list.border_title == '📁 etc'
|
||||
assert len(directory_list.options) == 1
|
||||
assert directory_list.options[0].prompt == '📁 ..'
|
||||
assert directory_list.options[0].id == '..'
|
||||
|
||||
|
||||
def test_directory_list_on_mount_with_root_directory_loads_archive_paths():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
flexmock(module.borgmatic.actions.browse.workers).should_receive('load_archive_paths').once()
|
||||
flexmock(module).should_receive('add_archive_paths').never()
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(), repository=flexmock(), archive_name='archive'
|
||||
)
|
||||
flexmock(directory_list.path_loaded).should_receive('subscribe')
|
||||
|
||||
directory_list.on_mount()
|
||||
|
||||
|
||||
def test_directory_list_on_mount_with_non_root_directory_adds_archive_paths():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
flexmock(module.borgmatic.actions.browse.workers).should_receive('load_archive_paths').never()
|
||||
flexmock(module).should_receive('add_archive_paths').once()
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
|
||||
path_components=('etc',),
|
||||
)
|
||||
flexmock(directory_list.path_loaded).should_receive('subscribe')
|
||||
|
||||
directory_list.on_mount()
|
||||
|
||||
|
||||
def test_on_archive_path_loaded_with_loading_done_signal_removes_loading_indicator():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
flexmock(module).should_receive('add_archive_paths').never()
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
|
||||
path_components=('etc',),
|
||||
)
|
||||
directory_list.timer = flexmock(stop=lambda: None)
|
||||
flexmock(directory_list).should_receive('remove_option').once()
|
||||
|
||||
directory_list.on_archive_path_loaded(data=module.borgmatic.actions.browse.workers.LOADING_DONE)
|
||||
|
||||
|
||||
def test_on_archive_path_loaded_with_path_loaded_signal_adds_archive_path():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
flexmock(module).should_receive('add_archive_paths').once()
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
|
||||
path_components=('etc',),
|
||||
)
|
||||
flexmock(directory_list).should_receive('remove_option').never()
|
||||
|
||||
directory_list.on_archive_path_loaded(data=flexmock())
|
||||
|
||||
|
||||
def test_directory_list_on_option_list_option_highlighted_with_highlighted_none_marks_it_unchanged():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
|
||||
path_components=('etc',),
|
||||
)
|
||||
directory_list.highlighted = None
|
||||
|
||||
directory_list.on_option_list_option_highlighted(event=flexmock())
|
||||
|
||||
assert directory_list.highlighted_option_changed is False
|
||||
|
||||
|
||||
def test_directory_list_on_option_list_option_highlighted_with_highlighted_zero_marks_it_unchanged():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
|
||||
path_components=('etc',),
|
||||
)
|
||||
directory_list.add_option(textual.widgets.option_list.Option('zero', id='zero'))
|
||||
directory_list.highlighted = 0
|
||||
|
||||
directory_list.on_option_list_option_highlighted(event=flexmock())
|
||||
|
||||
assert directory_list.highlighted_option_changed is False
|
||||
|
||||
|
||||
def test_directory_list_on_option_list_option_highlighted_with_highlighted_non_zero_marks_it_changed():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.directory_list.Directory_list).should_receive(
|
||||
'app'
|
||||
).and_return(flexmock())
|
||||
directory_list = module.Directory_list(
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
path_loaded=flexmock(complete=False, path_hierarchy={'etc': {}}),
|
||||
path_components=('etc',),
|
||||
)
|
||||
directory_list.add_option(textual.widgets.option_list.Option('zero', id='zero'))
|
||||
directory_list.add_option(textual.widgets.option_list.Option('one', id='one'))
|
||||
directory_list.highlighted = 1
|
||||
|
||||
directory_list.on_option_list_option_highlighted(event=flexmock())
|
||||
|
||||
assert directory_list.highlighted_option_changed is True
|
||||
@@ -1,53 +0,0 @@
|
||||
import textual.widgets.option_list
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import file_preview as module
|
||||
|
||||
|
||||
def test_file_preview_does_not_raise():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
|
||||
module.File_preview(
|
||||
config=flexmock(), repository=flexmock(), archive_name='archive', file_path='foo/bar.txt'
|
||||
)
|
||||
|
||||
|
||||
async def test_file_preview_on_mount_does_not_raise():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive('add_inline_loading_indicator')
|
||||
flexmock(module.borgmatic.actions.browse.workers).should_receive('load_file_preview')
|
||||
file_preview = module.File_preview(
|
||||
config=flexmock(), repository=flexmock(), archive_name='archive', file_path='foo.txt'
|
||||
)
|
||||
flexmock(file_preview.file_preview_loaded).should_receive('subscribe')
|
||||
|
||||
async with textual.app.App().run_test():
|
||||
file_preview.on_mount()
|
||||
|
||||
|
||||
def test_file_preview_on_file_preview_loaded_with_none_file_contents_displays_error():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive(
|
||||
'add_inline_loading_indicator'
|
||||
).and_return(flexmock(stop=lambda: None))
|
||||
file_preview = module.File_preview(
|
||||
config=flexmock(), repository=flexmock(), archive_name='archive', file_path='foo.txt'
|
||||
)
|
||||
flexmock(file_preview).should_receive('write').with_args(
|
||||
'Cannot display a preview for this file'
|
||||
).once()
|
||||
|
||||
file_preview.on_file_preview_loaded(None)
|
||||
|
||||
|
||||
def test_file_preview_on_file_preview_loaded_with_file_contents_displays_contents():
|
||||
flexmock(module.borgmatic.actions.browse.loading).should_receive(
|
||||
'add_inline_loading_indicator'
|
||||
).and_return(flexmock(stop=lambda: None))
|
||||
file_preview = module.File_preview(
|
||||
config=flexmock(), repository=flexmock(), archive_name='archive', file_path='foo.txt'
|
||||
)
|
||||
flexmock(file_preview).should_receive('write').with_args(
|
||||
'Cannot display a preview for this file'
|
||||
).never()
|
||||
flexmock(file_preview).should_receive('write').once()
|
||||
|
||||
file_preview.on_file_preview_loaded('hi')
|
||||
@@ -1,97 +0,0 @@
|
||||
import pytest
|
||||
import textual.app
|
||||
import textual.widgets
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import loading as module
|
||||
|
||||
|
||||
def test_update_inline_loading_indicator_with_option_list_adds_a_dot():
|
||||
widget = textual.widgets.OptionList()
|
||||
widget.add_option(textual.widgets.option_list.Option('HOLD.', id='loading-indicator'))
|
||||
|
||||
module.update_inline_loading_indicator(widget)
|
||||
|
||||
assert len(widget.options) == 1
|
||||
assert widget.options[0].prompt == 'HOLD..'
|
||||
|
||||
|
||||
def test_update_inline_loading_indicator_with_option_list_wraps_dots_beyond_three():
|
||||
widget = textual.widgets.OptionList()
|
||||
widget.add_option(textual.widgets.option_list.Option('HOLD...', id='loading-indicator'))
|
||||
|
||||
module.update_inline_loading_indicator(widget)
|
||||
|
||||
assert len(widget.options) == 1
|
||||
assert widget.options[0].prompt == 'HOLD'
|
||||
|
||||
|
||||
def test_update_inline_loading_indicator_with_option_list_and_missing_indicator_does_not_raise():
|
||||
widget = textual.widgets.OptionList()
|
||||
|
||||
module.update_inline_loading_indicator(widget)
|
||||
|
||||
assert len(widget.options) == 0
|
||||
|
||||
|
||||
async def test_update_inline_loading_indicator_with_rich_log_adds_a_dot():
|
||||
async with textual.app.App().run_test():
|
||||
widget = textual.widgets.RichLog()
|
||||
widget._size_known = True
|
||||
widget.write('HOLD.')
|
||||
|
||||
module.update_inline_loading_indicator(widget)
|
||||
|
||||
assert str(widget.lines[0].text) == 'HOLD..'
|
||||
|
||||
|
||||
async def test_update_inline_loading_indicator_with_rich_log_wraps_dots_beyond_three():
|
||||
async with textual.app.App().run_test():
|
||||
widget = textual.widgets.RichLog()
|
||||
widget._size_known = True
|
||||
widget.write('HOLD...')
|
||||
|
||||
module.update_inline_loading_indicator(widget)
|
||||
|
||||
assert str(widget.lines[0].text) == 'HOLD'
|
||||
|
||||
|
||||
def test_update_inline_loading_indicator_with_rich_log_and_missing_indicator_does_not_raise():
|
||||
widget = textual.widgets.RichLog()
|
||||
|
||||
module.update_inline_loading_indicator(widget)
|
||||
|
||||
assert len(widget.lines) == 0
|
||||
|
||||
|
||||
def test_update_inline_loading_indicator_with_unsupported_widget_type_raises():
|
||||
with pytest.raises(ValueError):
|
||||
module.update_inline_loading_indicator(flexmock())
|
||||
|
||||
|
||||
def test_add_inline_loading_indicator_with_option_list_adds_loading_indicator_option():
|
||||
widget = textual.widgets.OptionList()
|
||||
flexmock(widget).should_receive('set_interval')
|
||||
|
||||
module.add_inline_loading_indicator(widget)
|
||||
|
||||
assert len(widget.options) == 1
|
||||
assert widget.options[0].prompt == module.LOADING_MESSAGE
|
||||
assert widget.options[0].id == 'loading-indicator'
|
||||
assert widget.highlighted is None
|
||||
|
||||
|
||||
async def test_add_inline_loading_indicator_with_rich_log_writes_loading_indicator_text():
|
||||
async with textual.app.App().run_test():
|
||||
widget = textual.widgets.RichLog()
|
||||
widget._size_known = True
|
||||
flexmock(widget).should_receive('set_interval')
|
||||
|
||||
module.add_inline_loading_indicator(widget)
|
||||
|
||||
assert str(widget.lines[0].text) == module.LOADING_MESSAGE
|
||||
|
||||
|
||||
def test_add_inline_loading_indicator_with_unsupported_widget_type_raises():
|
||||
with pytest.raises(ValueError):
|
||||
module.add_inline_loading_indicator(flexmock())
|
||||
@@ -1,37 +0,0 @@
|
||||
import contextlib
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import logs as module
|
||||
|
||||
|
||||
def test_log_to_widget_adds_our_handler_and_removes_default_handler():
|
||||
default_handler = module.borgmatic.logger.Multi_stream_handler({})
|
||||
root_logger = module.logging.getLogger()
|
||||
root_logger.addHandler(default_handler)
|
||||
browse_log_handler = None
|
||||
|
||||
try:
|
||||
module.log_to_widget(flexmock())
|
||||
|
||||
with contextlib.suppress(StopIteration):
|
||||
browse_log_handler = next(
|
||||
handler
|
||||
for handler in root_logger.handlers
|
||||
if isinstance(handler, module.Browse_log_handler)
|
||||
)
|
||||
|
||||
assert browse_log_handler
|
||||
finally:
|
||||
if browse_log_handler:
|
||||
root_logger.removeHandler(browse_log_handler)
|
||||
|
||||
assert not any(
|
||||
handler
|
||||
for handler in root_logger.handlers
|
||||
if isinstance(handler, module.borgmatic.logger.Multi_stream_handler)
|
||||
)
|
||||
|
||||
|
||||
def test_logs_does_not_raise():
|
||||
module.Logs()
|
||||
@@ -1,12 +0,0 @@
|
||||
from borgmatic.actions.browse import repositories_list as module
|
||||
|
||||
|
||||
def test_repositories_list_populates_options():
|
||||
config = {'repositories': [{'path': 'test1.borg'}, {'path': 'test2.borg', 'label': 'two'}]}
|
||||
|
||||
repositories_list = module.Repositories_list(config=config)
|
||||
assert len(repositories_list.options) == 2
|
||||
assert repositories_list.options[0].prompt == 'test1.borg'
|
||||
assert repositories_list.options[0].id == 0
|
||||
assert repositories_list.options[1].prompt == 'two'
|
||||
assert repositories_list.options[1].id == 1
|
||||
@@ -1,19 +0,0 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import workers as module
|
||||
|
||||
|
||||
def test_archive_path_loaded_publish_records_complete():
|
||||
signal = module.Archive_path_loaded(owner=flexmock(), name='Bob')
|
||||
signal.publish(module.LOADING_DONE)
|
||||
|
||||
assert signal.complete
|
||||
|
||||
|
||||
def test_archive_path_loaded_publish_records_published_path():
|
||||
archive_path = module.borgmatic.actions.browse.archive.Archive_path('-', 'foo/bar.txt', '')
|
||||
signal = module.Archive_path_loaded(owner=flexmock(), name='Bob')
|
||||
signal.publish(archive_path)
|
||||
|
||||
assert signal.path_hierarchy == {'foo': {'bar.txt': archive_path}}
|
||||
assert not signal.complete
|
||||
@@ -57,8 +57,6 @@ def test_make_rename_command_includes_log_debug():
|
||||
|
||||
|
||||
def test_make_rename_command_includes_dry_run():
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_rename_command(
|
||||
dry_run=True,
|
||||
repository_name='repo',
|
||||
@@ -81,8 +79,6 @@ def test_make_rename_command_includes_dry_run():
|
||||
|
||||
|
||||
def test_make_rename_command_includes_remote_path():
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
@@ -106,8 +102,6 @@ def test_make_rename_command_includes_remote_path():
|
||||
|
||||
|
||||
def test_make_rename_command_includes_umask():
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
@@ -131,8 +125,6 @@ def test_make_rename_command_includes_umask():
|
||||
|
||||
|
||||
def test_make_rename_command_includes_log_json():
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
@@ -148,8 +140,6 @@ def test_make_rename_command_includes_log_json():
|
||||
|
||||
|
||||
def test_make_rename_command_includes_lock_wait():
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
@@ -173,8 +163,6 @@ def test_make_rename_command_includes_lock_wait():
|
||||
|
||||
|
||||
def test_make_rename_command_includes_extra_borg_options():
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_rename_command(
|
||||
dry_run=False,
|
||||
repository_name='repo',
|
||||
|
||||
@@ -557,43 +557,6 @@ def test_log_outputs_with_unfinished_process_re_polls():
|
||||
)
|
||||
|
||||
|
||||
def test_log_outputs_includes_error_output_when_output_spans_multiple_chunks():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
|
||||
flexmock(module).should_receive('command_for_process').and_return('python')
|
||||
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
'-c',
|
||||
(
|
||||
'import os, sys; '
|
||||
f'os.write(sys.stdout.fileno(), b"x" * {module.READ_CHUNK_SIZE + 10}); '
|
||||
'os.write(sys.stdout.fileno(), b"\\nERROR: critical failure"); '
|
||||
'os.close(1); '
|
||||
'os._exit(2)'
|
||||
),
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
flexmock(module).should_receive('output_buffers_for_process').and_return((process.stdout,))
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
tuple(
|
||||
module.log_outputs(
|
||||
(process,),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert error.value.output
|
||||
assert 'ERROR: critical failure' in error.value.output
|
||||
|
||||
|
||||
def test_read_lines_uses_system_locale_when_decoding_output():
|
||||
flexmock(module.locale).should_receive('getpreferredencoding').and_return('ISO-8859-1')
|
||||
process = subprocess.Popen(['echo', b'\xc4pple'], stdout=subprocess.PIPE)
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import archive as module
|
||||
|
||||
|
||||
def test_get_repository_archives_does_not_raise():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('list_repository').and_return('{}')
|
||||
|
||||
assert module.get_repository_archives(config, config['repositories'][0]) == {}
|
||||
|
||||
|
||||
def test_get_archive_paths_returns_each_as_archive_path_with_metadata():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'path': 'foo.txt', 'type': '-', 'linktarget': ''},
|
||||
{'path': 'bar.txt', 'type': 'l', 'linktarget': 'foo.txt'},
|
||||
{'path': 'etc', 'type': 'd', 'linktarget': ''},
|
||||
)
|
||||
|
||||
assert tuple(module.get_archive_paths(config, config['repositories'][0], 'archive')) == (
|
||||
module.Archive_path('-', 'foo.txt', ''),
|
||||
module.Archive_path('l', 'bar.txt', 'foo.txt'),
|
||||
module.Archive_path('d', 'etc', ''),
|
||||
)
|
||||
|
||||
|
||||
def test_get_archive_file_content_with_binary_file_bails():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
|
||||
flexmock(stdout=flexmock(readlines=lambda hint: [b'foo\n', b'bar\n'])),
|
||||
)
|
||||
flexmock(module.binaryornot.helpers).should_receive('is_binary_string').and_return(True)
|
||||
|
||||
assert (
|
||||
module.get_archive_file_content(config, config['repositories'][0], 'archive', 'etc/foo.txt')
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_get_archive_file_content_decodes_content():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
|
||||
flexmock(stdout=flexmock(readlines=lambda hint: [b'foo\n', b'bar\n'])),
|
||||
)
|
||||
flexmock(module.binaryornot.helpers).should_receive('is_binary_string').and_return(False)
|
||||
|
||||
assert (
|
||||
module.get_archive_file_content(config, config['repositories'][0], 'archive', 'etc/foo.txt')
|
||||
== 'foo\nbar\n'
|
||||
)
|
||||
|
||||
|
||||
def test_get_archive_file_content_with_large_file_truncates_content():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
|
||||
flexmock(module).READLINES_HINT_BYTES = 3
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
|
||||
flexmock(stdout=flexmock(readlines=lambda hint: [b'foo\n', b'bar\n'])),
|
||||
)
|
||||
flexmock(module.binaryornot.helpers).should_receive('is_binary_string').and_return(False)
|
||||
|
||||
assert (
|
||||
module.get_archive_file_content(config, config['repositories'][0], 'archive', 'etc/foo.txt')
|
||||
== f'foo\nbar\n\n{module.TRUNCATION_MESSAGE}'
|
||||
)
|
||||
|
||||
|
||||
def test_get_archive_file_content_with_unicode_decode_error_does_not_raise():
|
||||
config = {'repositories': [{'path': 'test.borg'}]}
|
||||
flexmock(module.borgmatic.logger).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.version).should_receive('local_borg_version').and_return('3.0')
|
||||
flexmock(module.borgmatic.borg.extract).should_receive('extract_archive').and_return(
|
||||
flexmock(stdout=flexmock(readlines=lambda hint: [b'foo\n', b'\xc3\n'])),
|
||||
)
|
||||
flexmock(module.binaryornot.helpers).should_receive('is_binary_string').and_return(False)
|
||||
|
||||
assert (
|
||||
module.get_archive_file_content(config, config['repositories'][0], 'archive', 'etc/foo.txt')
|
||||
is None
|
||||
)
|
||||
@@ -1,94 +0,0 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import directory_list as module
|
||||
|
||||
|
||||
def test_get_relative_archive_path_components_strips_off_current_directory():
|
||||
assert module.get_relative_archive_path_components(
|
||||
flexmock(file_path='foo/bar/baz/quux.txt'), ('foo', 'bar')
|
||||
) == ('baz', 'quux.txt')
|
||||
|
||||
|
||||
def test_get_relative_archive_path_components_with_root_current_directory_strips_off_nothing():
|
||||
assert module.get_relative_archive_path_components(
|
||||
flexmock(file_path='foo/bar/baz/quux.txt'), ()
|
||||
) == ('foo', 'bar', 'baz', 'quux.txt')
|
||||
|
||||
|
||||
def test_get_relative_archive_path_components_with_non_matching_paths_returns_none():
|
||||
assert (
|
||||
module.get_relative_archive_path_components(
|
||||
flexmock(file_path='foo/bar/baz/quux.txt'), ('etc',)
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_make_directory_list_option_with_file_path_makes_file_option():
|
||||
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
|
||||
|
||||
option = module.make_directory_list_option(
|
||||
flexmock(path_type='-', file_path='foo/bar/baz.txt', link_target=''), ('baz.txt',)
|
||||
)
|
||||
|
||||
assert option.prompt == '📄 baz.txt'
|
||||
assert option.id == 'baz.txt'
|
||||
|
||||
|
||||
def test_make_directory_list_option_with_directory_path_makes_directory_option():
|
||||
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
|
||||
|
||||
option = module.make_directory_list_option(
|
||||
flexmock(path_type='d', file_path='foo/bar/baz', link_target=''), ('baz',)
|
||||
)
|
||||
|
||||
assert option.prompt == '📁 baz'
|
||||
assert option.id == 'baz'
|
||||
|
||||
|
||||
def test_make_directory_list_option_with_contained_file_path_makes_directory_option():
|
||||
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
|
||||
|
||||
option = module.make_directory_list_option(
|
||||
flexmock(path_type='d', file_path='foo/bar/baz.txt', link_target=''),
|
||||
(
|
||||
'bar',
|
||||
'baz.txt',
|
||||
),
|
||||
)
|
||||
|
||||
assert option.prompt == '📁 bar'
|
||||
assert option.id == 'bar'
|
||||
|
||||
|
||||
def test_make_directory_list_option_with_link_path_makes_link_option():
|
||||
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
|
||||
|
||||
option = module.make_directory_list_option(
|
||||
flexmock(path_type='l', file_path='foo/bar/baz.txt', link_target='quux.txt'), ('baz.txt',)
|
||||
)
|
||||
|
||||
assert option.prompt == '🔗 baz.txt → quux.txt'
|
||||
assert option.id == 'baz.txt'
|
||||
|
||||
|
||||
def test_make_directory_list_option_with_pipe_path_makes_pipe_option():
|
||||
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
|
||||
|
||||
option = module.make_directory_list_option(
|
||||
flexmock(path_type='p', file_path='foo/bar/baz.txt', link_target=''), ('baz.txt',)
|
||||
)
|
||||
|
||||
assert option.prompt == '🚰 baz.txt'
|
||||
assert option.id == 'baz.txt'
|
||||
|
||||
|
||||
def test_make_directory_list_option_with_unknown_path_makes_unknown_option():
|
||||
flexmock(module.textual.widgets.option_list).should_receive('Option').replace_with(flexmock)
|
||||
|
||||
option = module.make_directory_list_option(
|
||||
flexmock(path_type='wtf', file_path='foo/bar/baz.txt', link_target=''), ('baz.txt',)
|
||||
)
|
||||
|
||||
assert option.prompt == '❓ baz.txt'
|
||||
assert option.id == 'baz.txt'
|
||||
@@ -1,87 +0,0 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import logs as module
|
||||
|
||||
|
||||
def test_rich_color_formatter_format_colors_log_record_based_on_level():
|
||||
formatted = module.Rich_color_formatter().format(
|
||||
module.logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=module.logging.ERROR,
|
||||
levelname='ERROR',
|
||||
msg='oh no',
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
assert formatted == '[bright_red]oh no[/bright_red]'
|
||||
|
||||
|
||||
def test_rich_color_formatter_format_includes_prefix():
|
||||
formatter = module.Rich_color_formatter()
|
||||
formatter.prefix = 'sup'
|
||||
formatted = formatter.format(
|
||||
module.logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=module.logging.ERROR,
|
||||
levelname='ERROR',
|
||||
msg='oh no',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assert formatted == '[bright_red]sup: oh no[/bright_red]'
|
||||
|
||||
|
||||
def test_browse_log_handler_emit_from_worker_thread_calls_write_in_main_thread():
|
||||
flexmock(module.textual.worker).should_receive('get_current_worker')
|
||||
logs_widget = flexmock(write=lambda message: None, app=flexmock())
|
||||
flexmock(logs_widget.app).should_receive('call_from_thread').with_args(
|
||||
logs_widget.write, 'hi'
|
||||
).once()
|
||||
|
||||
module.Browse_log_handler(logs_widget).emit(
|
||||
module.logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=module.logging.DEBUG,
|
||||
levelname='DEBUG',
|
||||
msg='hi',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_browse_log_handler_emit_from_main_thread_calls_write_directly():
|
||||
flexmock(module.textual.worker).should_receive('get_current_worker').and_raise(RuntimeError)
|
||||
logs_widget = flexmock(write=lambda message: None, app=flexmock())
|
||||
flexmock(logs_widget.app).should_receive('call_from_thread').never()
|
||||
flexmock(logs_widget).should_receive('write').with_args('hi').once()
|
||||
|
||||
module.Browse_log_handler(logs_widget).emit(
|
||||
module.logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=module.logging.DEBUG,
|
||||
levelname='DEBUG',
|
||||
msg='hi',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_browse_log_handler_emit_from_main_thread_with_no_active_app_does_not_raise():
|
||||
flexmock(module.textual.worker).should_receive('get_current_worker').and_raise(RuntimeError)
|
||||
logs_widget = flexmock(write=lambda message: None, app=flexmock())
|
||||
flexmock(logs_widget.app).should_receive('call_from_thread').never()
|
||||
flexmock(logs_widget).should_receive('write').with_args('hi').and_raise(
|
||||
module.textual._context.NoActiveAppError
|
||||
)
|
||||
|
||||
module.Browse_log_handler(logs_widget).emit(
|
||||
module.logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=module.logging.DEBUG,
|
||||
levelname='DEBUG',
|
||||
msg='hi',
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -1,29 +0,0 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.actions.browse.app
|
||||
from borgmatic.actions.browse import run as module
|
||||
|
||||
|
||||
def test_run_browse_without_configs_bails():
|
||||
app = flexmock()
|
||||
app.should_receive('run').never()
|
||||
|
||||
flexmock(borgmatic.actions.browse.app).should_receive('Browse_app').and_return(app)
|
||||
|
||||
module.run_browse(
|
||||
diff_arguments=flexmock(),
|
||||
global_arguments=flexmock(),
|
||||
configs=(),
|
||||
)
|
||||
|
||||
|
||||
def test_run_browse_with_configs_does_not_raise():
|
||||
flexmock(borgmatic.actions.browse.app).should_receive('Browse_app').and_return(
|
||||
flexmock(run=lambda: None)
|
||||
)
|
||||
|
||||
module.run_browse(
|
||||
diff_arguments=flexmock(),
|
||||
global_arguments=flexmock(),
|
||||
configs=(flexmock(), flexmock()),
|
||||
)
|
||||
@@ -1,126 +0,0 @@
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions.browse import workers as module
|
||||
|
||||
|
||||
def test_add_repository_archives_publishes_for_each_archive():
|
||||
flexmock(module.borgmatic.actions.browse.archive).should_receive(
|
||||
'get_repository_archives'
|
||||
).and_return({'archives': [{'archive': 'foo'}, {'archive': 'bar'}]})
|
||||
archive_loaded = flexmock()
|
||||
archive_loaded.should_receive('publish').with_args('bar').once() # Reversed order.
|
||||
archive_loaded.should_receive('publish').with_args('foo').once()
|
||||
archive_loaded.should_receive('publish').with_args(module.LOADING_DONE).once()
|
||||
|
||||
module.add_repository_archives.__wrapped__(
|
||||
browse_app=flexmock(),
|
||||
archive_loaded=archive_loaded,
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
)
|
||||
|
||||
|
||||
def test_record_path_sets_file_path_in_filesystem_hierarchy():
|
||||
archive_path = flexmock(path_type='-', file_path='foo/bar/baz.txt')
|
||||
quux_path = flexmock()
|
||||
hierarchy = {'foo': {'bar': {'quux.txt': quux_path}, 'empty': {}}}
|
||||
|
||||
module.record_path(
|
||||
archive_path=archive_path, hierarchy=hierarchy, path_components=('foo', 'bar', 'baz.txt')
|
||||
)
|
||||
|
||||
assert hierarchy == {
|
||||
'foo': {'bar': {'quux.txt': quux_path, 'baz.txt': archive_path}, 'empty': {}}
|
||||
}
|
||||
|
||||
|
||||
def test_record_path_sets_directory_path_in_filesystem_hierarchy():
|
||||
archive_path = flexmock(path_type='d', file_path='foo/bar/baz')
|
||||
quux_path = flexmock()
|
||||
hierarchy = {'foo': {'bar': {'quux.txt': quux_path}, 'empty': {}}}
|
||||
|
||||
module.record_path(
|
||||
archive_path=archive_path, hierarchy=hierarchy, path_components=('foo', 'bar', 'baz')
|
||||
)
|
||||
|
||||
assert hierarchy == {'foo': {'bar': {'quux.txt': quux_path, 'baz': {}}, 'empty': {}}}
|
||||
|
||||
|
||||
def test_get_paths_lists_directory_contents():
|
||||
baz_path = module.borgmatic.actions.browse.archive.Archive_path('-', 'foo/bar/baz.txt', '')
|
||||
hierarchy = {'foo': {'bar': {'baz.txt': baz_path, 'other': {}}}, 'nope': {}}
|
||||
|
||||
assert tuple(module.get_paths(hierarchy, ('foo', 'bar'))) == (
|
||||
baz_path,
|
||||
module.borgmatic.actions.browse.archive.Archive_path('d', 'foo/bar/other', ''),
|
||||
)
|
||||
|
||||
|
||||
def test_get_paths_with_full_path_components_lists_directory_contents():
|
||||
baz_path = module.borgmatic.actions.browse.archive.Archive_path('-', 'etc/foo/bar/baz.txt', '')
|
||||
hierarchy = {'foo': {'bar': {'baz.txt': baz_path, 'other': {}}}, 'nope': {}}
|
||||
|
||||
assert tuple(module.get_paths(hierarchy, ('foo', 'bar'), ('etc', 'foo', 'bar'))) == (
|
||||
baz_path,
|
||||
module.borgmatic.actions.browse.archive.Archive_path('d', 'etc/foo/bar/other', ''),
|
||||
)
|
||||
|
||||
|
||||
def test_get_paths_lists_empty_directory_contents():
|
||||
baz_path = module.borgmatic.actions.browse.archive.Archive_path('-', 'etc/foo/bar/baz.txt', '')
|
||||
hierarchy = {'foo': {'bar': {'baz.txt': baz_path, 'other': {}}}, 'nope': {}}
|
||||
|
||||
assert (
|
||||
tuple(module.get_paths(hierarchy, ('foo', 'bar', 'other'), ('etc', 'foo', 'bar', 'other')))
|
||||
== ()
|
||||
)
|
||||
|
||||
|
||||
def test_get_paths_with_unknown_file_raises():
|
||||
hierarchy = {'foo': {'other': {}}}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
tuple(module.get_paths(hierarchy, ('foo', 'bar.txt')))
|
||||
|
||||
|
||||
def test_get_paths_with_unknown_directory_raises():
|
||||
hierarchy = {'foo': {'other': {}}}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
tuple(module.get_paths(hierarchy, ('foo', 'bar', 'baz')))
|
||||
|
||||
|
||||
def test_load_archive_paths_publishes_for_each_archive_path():
|
||||
flexmock(module.borgmatic.actions.browse.archive).should_receive('get_archive_paths').and_yield(
|
||||
'foo.txt', 'bar.txt'
|
||||
)
|
||||
path_loaded = flexmock()
|
||||
path_loaded.should_receive('publish').with_args('foo.txt').once()
|
||||
path_loaded.should_receive('publish').with_args('bar.txt').once()
|
||||
path_loaded.should_receive('publish').with_args(module.LOADING_DONE).once()
|
||||
|
||||
module.load_archive_paths.__wrapped__(
|
||||
browse_app=flexmock(),
|
||||
path_loaded=path_loaded,
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
)
|
||||
|
||||
|
||||
def test_load_file_preview_publishes_file_contents():
|
||||
flexmock(module.borgmatic.actions.browse.archive).should_receive(
|
||||
'get_archive_file_content'
|
||||
).and_return('hi')
|
||||
file_preview_loaded = flexmock()
|
||||
file_preview_loaded.should_receive('publish').with_args('hi').once()
|
||||
|
||||
module.load_file_preview.__wrapped__(
|
||||
browse_app=flexmock(),
|
||||
file_preview_loaded=file_preview_loaded,
|
||||
config=flexmock(),
|
||||
repository=flexmock(),
|
||||
archive_name='archive',
|
||||
file_path='foo/bar/baz.txt',
|
||||
)
|
||||
@@ -171,6 +171,7 @@ def test_run_bootstrap_does_not_raise():
|
||||
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
|
||||
'archive',
|
||||
)
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('repo')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
@@ -222,6 +223,7 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
|
||||
local_path='borg7',
|
||||
remote_path='borg8',
|
||||
).and_return('archive')
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('repo')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
|
||||
@@ -1838,7 +1838,7 @@ def test_spot_check_without_any_source_paths_errors():
|
||||
|
||||
def test_run_check_checks_archives_for_configured_repository():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module).should_receive('upgrade_check_times')
|
||||
flexmock(module).should_receive('parse_checks')
|
||||
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
|
||||
@@ -1873,7 +1873,7 @@ def test_run_check_checks_archives_for_configured_repository():
|
||||
|
||||
def test_run_check_runs_configured_extract_check():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module).should_receive('upgrade_check_times')
|
||||
flexmock(module).should_receive('parse_checks')
|
||||
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
|
||||
@@ -1906,7 +1906,7 @@ def test_run_check_runs_configured_extract_check():
|
||||
|
||||
def test_run_check_runs_configured_spot_check():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module).should_receive('upgrade_check_times')
|
||||
flexmock(module).should_receive('parse_checks')
|
||||
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
|
||||
@@ -1942,7 +1942,7 @@ def test_run_check_runs_configured_spot_check():
|
||||
|
||||
def test_run_check_without_checks_runs_nothing_except_hooks():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module).should_receive('upgrade_check_times')
|
||||
flexmock(module).should_receive('parse_checks')
|
||||
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
|
||||
|
||||
@@ -11,6 +11,7 @@ from borgmatic.actions import create as module
|
||||
|
||||
def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
@@ -49,6 +50,7 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
||||
|
||||
def test_run_create_with_both_list_and_json_errors():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
|
||||
create_arguments = flexmock(
|
||||
@@ -80,6 +82,7 @@ def test_run_create_with_both_list_and_json_errors():
|
||||
|
||||
def test_run_create_with_both_list_and_progress_errors():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
|
||||
create_arguments = flexmock(
|
||||
@@ -111,6 +114,7 @@ def test_run_create_with_both_list_and_progress_errors():
|
||||
|
||||
def test_run_create_produces_json():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
@@ -156,6 +160,7 @@ def test_run_create_with_active_dumps_roundtrips_via_checkpoint_archive():
|
||||
mock_dump_process.should_receive('poll').and_return(None).and_return(0)
|
||||
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
@@ -234,6 +239,7 @@ def test_run_create_with_active_dumps_json_updates_archive_info():
|
||||
}
|
||||
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
@@ -316,6 +322,7 @@ def mock_dump_cleanup(config, borgmatic_runtime_directory, patterns, dry_run):
|
||||
|
||||
def test_run_create_with_active_dumps_removes_data_source_dumps_with_original_patterns():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
flexmock(),
|
||||
)
|
||||
|
||||
@@ -1175,6 +1175,7 @@ def test_run_restore_restores_each_data_source():
|
||||
}
|
||||
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
@@ -1249,6 +1250,7 @@ def test_run_restore_restores_data_source_by_falling_back_to_all_name():
|
||||
}
|
||||
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
@@ -1311,6 +1313,7 @@ def test_run_restore_restores_data_source_configured_with_all_name():
|
||||
}
|
||||
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
@@ -1395,6 +1398,7 @@ def test_run_restore_skips_missing_data_source():
|
||||
}
|
||||
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
@@ -1479,6 +1483,7 @@ def test_run_restore_restores_data_sources_from_different_hooks():
|
||||
}
|
||||
|
||||
borgmatic_runtime_directory = flexmock()
|
||||
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
|
||||
borgmatic_runtime_directory,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@ def test_run_arbitrary_borg_calls_borg_with_flags():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -100,7 +99,6 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -125,7 +123,6 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_flag():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -151,7 +148,6 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
|
||||
borg_local_path='borg1',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -178,7 +174,6 @@ def test_run_arbitrary_borg_with_exit_codes_calls_borg_using_them():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -191,12 +186,9 @@ def test_run_arbitrary_borg_with_exit_codes_calls_borg_using_them():
|
||||
def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args(
|
||||
'remote-path', 'borg1'
|
||||
).and_return(
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(
|
||||
('--remote-path', 'borg1'),
|
||||
)
|
||||
).and_return(())
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
@@ -208,7 +200,6 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags()
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -222,12 +213,9 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags()
|
||||
def test_run_arbitrary_borg_with_remote_path_injection_attack_gets_escaped():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args(
|
||||
'remote-path', 'borg1; naughty-command'
|
||||
).and_return(
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(
|
||||
('--remote-path', 'borg1; naughty-command'),
|
||||
)
|
||||
).and_return(())
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
@@ -239,14 +227,13 @@ def test_run_arbitrary_borg_with_remote_path_injection_attack_gets_escaped():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
options=['break-lock', '::'],
|
||||
remote_path='borg1; naughty-command',
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
@@ -265,7 +252,6 @@ def test_run_arbitrary_borg_passes_borg_specific_flags_to_borg():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -290,7 +276,6 @@ def test_run_arbitrary_borg_omits_dash_dash_in_flags_passed_to_borg():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -315,7 +300,6 @@ def test_run_arbitrary_borg_without_borg_specific_flags_does_not_raise():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
@@ -392,7 +376,6 @@ def test_run_arbitrary_borg_calls_borg_with_working_directory():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.run_arbitrary_borg(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -24,7 +24,6 @@ def insert_execute_command_mock(command, working_directory=None, borg_exit_codes
|
||||
def test_break_lock_calls_borg_with_required_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'break-lock', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.break_lock(
|
||||
repository_path='repo',
|
||||
@@ -37,7 +36,6 @@ def test_break_lock_calls_borg_with_required_flags():
|
||||
def test_break_lock_calls_borg_with_local_path():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg1', 'break-lock', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.break_lock(
|
||||
repository_path='repo',
|
||||
@@ -51,7 +49,6 @@ def test_break_lock_calls_borg_with_local_path():
|
||||
def test_break_lock_calls_borg_using_exit_codes():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg1', 'break-lock', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.break_lock(
|
||||
repository_path='repo',
|
||||
@@ -67,7 +64,6 @@ def test_break_lock_calls_borg_with_remote_path_flags():
|
||||
insert_execute_command_mock(
|
||||
('borg', 'break-lock', '--remote-path', 'borg1', '--log-json', 'repo')
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.break_lock(
|
||||
repository_path='repo',
|
||||
@@ -81,7 +77,6 @@ def test_break_lock_calls_borg_with_remote_path_flags():
|
||||
def test_break_lock_calls_borg_with_umask_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'break-lock', '--umask', '0770', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.break_lock(
|
||||
repository_path='repo',
|
||||
@@ -94,7 +89,6 @@ def test_break_lock_calls_borg_with_umask_flags():
|
||||
def test_break_lock_calls_borg_with_lock_wait_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'break-lock', '--log-json', '--lock-wait', '5', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.break_lock(
|
||||
repository_path='repo',
|
||||
@@ -109,7 +103,6 @@ def test_break_lock_calls_borg_with_extra_borg_options():
|
||||
insert_execute_command_mock(
|
||||
('borg', 'break-lock', '--log-json', '--extra', 'value with space', 'repo')
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.break_lock(
|
||||
repository_path='repo',
|
||||
@@ -152,7 +145,6 @@ def test_break_lock_calls_borg_with_working_directory():
|
||||
insert_execute_command_mock(
|
||||
('borg', 'break-lock', '--log-json', 'repo'), working_directory='/working/dir'
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.break_lock(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -35,7 +35,6 @@ def insert_execute_command_mock(
|
||||
def test_change_passphrase_calls_borg_with_required_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'key', 'change-passphrase', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -49,7 +48,6 @@ def test_change_passphrase_calls_borg_with_required_flags():
|
||||
def test_change_passphrase_calls_borg_with_local_path():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg1', 'key', 'change-passphrase', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -70,7 +68,6 @@ def test_change_passphrase_calls_borg_using_exit_codes():
|
||||
config=config,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -86,7 +83,6 @@ def test_change_passphrase_calls_borg_with_remote_path_flags():
|
||||
insert_execute_command_mock(
|
||||
('borg', 'key', 'change-passphrase', '--remote-path', 'borg1', 'repo'),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -105,7 +101,6 @@ def test_change_passphrase_calls_borg_with_umask_flags():
|
||||
('borg', 'key', 'change-passphrase', '--umask', '0770', 'repo'),
|
||||
config=config,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -123,7 +118,6 @@ def test_change_passphrase_calls_borg_with_lock_wait_flags():
|
||||
('borg', 'key', 'change-passphrase', '--lock-wait', '5', 'repo'),
|
||||
config=config,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -141,7 +135,6 @@ def test_change_passphrase_calls_borg_with_extra_borg_options():
|
||||
('borg', 'key', 'change-passphrase', '--extra', 'value with space', 'repo'),
|
||||
config=config,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -186,7 +179,6 @@ def test_change_passphrase_with_dry_run_skips_borg_call():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -203,7 +195,6 @@ def test_change_passphrase_calls_borg_without_passphrase():
|
||||
('borg', 'key', 'change-passphrase', 'repo'),
|
||||
config={'option': 'foo'},
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
@@ -226,7 +217,6 @@ def test_change_passphrase_calls_borg_with_working_directory():
|
||||
config=config,
|
||||
working_directory='/working/dir',
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.change_passphrase(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -272,54 +272,6 @@ def test_make_check_name_flags_with_extract_omits_extract_flag():
|
||||
assert flags == ()
|
||||
|
||||
|
||||
def test_get_repository_id_with_valid_json_does_not_raise():
|
||||
config = {}
|
||||
flexmock(module.repo_info).should_receive('display_repository_info').and_return(
|
||||
'{"repository": {"id": "repo"}}',
|
||||
)
|
||||
|
||||
assert module.get_repository_id(
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
|
||||
def test_get_repository_id_with_json_error_raises():
|
||||
config = {}
|
||||
flexmock(module.repo_info).should_receive('display_repository_info').and_return(
|
||||
'{"unexpected": {"id": "repo"}}',
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_repository_id(
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
|
||||
def test_get_repository_id_with_missing_json_keys_raises():
|
||||
config = {}
|
||||
flexmock(module.repo_info).should_receive('display_repository_info').and_return('{invalid JSON')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_repository_id(
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_progress_passes_through_to_borg():
|
||||
config = {'progress': True}
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(
|
||||
@@ -337,7 +289,6 @@ def test_check_archives_with_progress_passes_through_to_borg():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -374,7 +325,6 @@ def test_check_archives_with_log_json_and_progress_passes_through_both_to_borg()
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -411,7 +361,6 @@ def test_check_archives_with_repair_passes_through_to_borg():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -448,7 +397,6 @@ def test_check_archives_with_log_json_and_repair_passes_through_both_to_borg():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -485,7 +433,6 @@ def test_check_archives_with_max_duration_flag_passes_through_to_borg():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -522,7 +469,6 @@ def test_check_archives_with_max_duration_option_passes_through_to_borg():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -556,7 +502,6 @@ def test_check_archives_with_max_duration_option_and_archives_check_runs_reposit
|
||||
insert_execute_command_mock(
|
||||
('borg', 'check', '--max-duration', '33', '--repository-only', '--log-json', 'repo'),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -590,7 +535,6 @@ def test_check_archives_with_max_duration_flag_and_archives_check_runs_repositor
|
||||
insert_execute_command_mock(
|
||||
('borg', 'check', '--max-duration', '33', '--repository-only', '--log-json', 'repo'),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -627,7 +571,6 @@ def test_check_archives_with_max_duration_option_and_data_check_runs_repository_
|
||||
insert_execute_command_mock(
|
||||
('borg', 'check', '--max-duration', '33', '--repository-only', '--log-json', 'repo'),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -664,7 +607,6 @@ def test_check_archives_with_max_duration_flag_and_data_check_runs_repository_ch
|
||||
insert_execute_command_mock(
|
||||
('borg', 'check', '--max-duration', '33', '--repository-only', '--log-json', 'repo'),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -701,7 +643,6 @@ def test_check_archives_with_max_duration_flag_overrides_max_duration_option():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -735,7 +676,6 @@ def test_check_archives_calls_borg_with_parameters(checks):
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -763,7 +703,6 @@ def test_check_archives_with_data_check_implies_archives_check_calls_borg_with_p
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -790,8 +729,8 @@ def test_check_archives_with_log_info_passes_through_to_borg():
|
||||
(),
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--log-json', '--info', 'repo'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
insert_execute_command_mock(('borg', 'check', '--log-json', '--info', 'repo'))
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -818,8 +757,8 @@ def test_check_archives_with_log_debug_passes_through_to_borg():
|
||||
(),
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--log-json', '--debug', '--show-rc', 'repo'))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
insert_execute_command_mock(('borg', 'check', '--log-json', '--debug', '--show-rc', 'repo'))
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -845,7 +784,6 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg1', 'check', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -875,7 +813,6 @@ def test_check_archives_with_exit_codes_calls_borg_using_them():
|
||||
insert_execute_command_mock(
|
||||
('borg', 'check', '--log-json', 'repo'), borg_exit_codes=borg_exit_codes
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -901,7 +838,6 @@ def test_check_archives_with_remote_path_passes_through_to_borg():
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--remote-path', 'borg1', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -928,7 +864,6 @@ def test_check_archives_with_umask_passes_through_to_borg():
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--umask', '077', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -954,7 +889,6 @@ def test_check_archives_with_lock_wait_passes_through_to_borg():
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--log-json', '--lock-wait', '5', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -981,7 +915,6 @@ def test_check_archives_with_retention_prefix():
|
||||
flexmock(module).should_receive('make_check_name_flags').with_args(checks, ()).and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg', 'check', '--log-json', 'repo'))
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -1011,7 +944,6 @@ def test_check_archives_with_extra_borg_options_passes_through_to_borg():
|
||||
insert_execute_command_mock(
|
||||
('borg', 'check', '--log-json', '--extra', '--options', 'value with space', 'repo'),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -1048,7 +980,6 @@ def test_check_archives_with_match_archives_passes_through_to_borg():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=[{'code': 1, 'treat_as': 'error'}],
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
@@ -1080,7 +1011,6 @@ def test_check_archives_calls_borg_with_working_directory():
|
||||
insert_execute_command_mock(
|
||||
('borg', 'check', '--log-json', 'repo'), working_directory='/working/dir'
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -10,7 +10,6 @@ from ..test_verbosity import insert_logging_mock
|
||||
def insert_execute_command_mock(
|
||||
compact_command,
|
||||
output_log_level,
|
||||
output_file=None,
|
||||
working_directory=None,
|
||||
borg_exit_codes=None,
|
||||
):
|
||||
@@ -21,7 +20,6 @@ def insert_execute_command_mock(
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
compact_command,
|
||||
output_log_level=output_log_level,
|
||||
output_file=output_file,
|
||||
environment=None,
|
||||
working_directory=working_directory,
|
||||
borg_local_path=compact_command[0],
|
||||
@@ -35,7 +33,6 @@ COMPACT_COMMAND = ('borg', 'compact')
|
||||
def test_compact_segments_calls_borg_with_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock((*COMPACT_COMMAND, '--log-json', 'repo'), logging.INFO)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -86,7 +83,6 @@ def test_compact_segments_with_dry_run_skips_borg_call_when_feature_unavailable(
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
flexmock(module.logger).should_receive('info').with_args('Skipping compact (dry run)').once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
repository_path='repo',
|
||||
@@ -106,7 +102,6 @@ def test_compact_segments_with_dry_run_executes_borg_call_when_feature_available
|
||||
flexmock(module.environment).should_receive('make_environment').once()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').once()
|
||||
flexmock(module).should_receive('execute_command').once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
repository_path='repo',
|
||||
@@ -120,7 +115,6 @@ def test_compact_segments_with_dry_run_executes_borg_call_when_feature_available
|
||||
def test_compact_segments_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg1', *COMPACT_COMMAND[1:], '--log-json', 'repo'), logging.INFO)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -140,7 +134,6 @@ def test_compact_segments_with_exit_codes_calls_borg_using_them():
|
||||
logging.INFO,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -151,12 +144,25 @@ def test_compact_segments_with_exit_codes_calls_borg_using_them():
|
||||
)
|
||||
|
||||
|
||||
def test_compact_segments_with_progress_calls_borg_with_progress_flag():
|
||||
def test_compact_segments_with_remote_path_calls_borg_with_remote_path_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(
|
||||
(*COMPACT_COMMAND, '--progress', 'repo'), logging.INFO, module.DO_NOT_CAPTURE
|
||||
(*COMPACT_COMMAND, '--remote-path', 'borg1', '--log-json', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_compact_segments_with_progress_calls_borg_with_progress_flag():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock((*COMPACT_COMMAND, '--progress', 'repo'), logging.INFO)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -170,11 +176,8 @@ def test_compact_segments_with_progress_calls_borg_with_progress_flag():
|
||||
def test_compact_segments_with_log_json_and_progress_calls_borg_with_both_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(
|
||||
(*COMPACT_COMMAND, '--log-json', '--progress', 'repo'),
|
||||
logging.INFO,
|
||||
module.DO_NOT_CAPTURE,
|
||||
(*COMPACT_COMMAND, '--log-json', '--progress', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -190,7 +193,6 @@ def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_f
|
||||
insert_execute_command_mock(
|
||||
(*COMPACT_COMMAND, '--log-json', '--cleanup-commits', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -207,7 +209,6 @@ def test_compact_segments_with_threshold_calls_borg_with_threshold_flag():
|
||||
insert_execute_command_mock(
|
||||
(*COMPACT_COMMAND, '--log-json', '--threshold', '20', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -218,29 +219,12 @@ def test_compact_segments_with_threshold_calls_borg_with_threshold_flag():
|
||||
)
|
||||
|
||||
|
||||
def test_compact_segments_with_zero_threshold_calls_borg_with_threshold_flag():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(
|
||||
(*COMPACT_COMMAND, '--log-json', '--threshold', '0', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={'compact_threshold': 0},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
)
|
||||
|
||||
|
||||
def test_compact_segments_with_umask_calls_borg_with_umask_flags():
|
||||
config = {'umask': '077'}
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(
|
||||
(*COMPACT_COMMAND, '--umask', '077', '--log-json', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -257,7 +241,6 @@ def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
insert_execute_command_mock(
|
||||
(*COMPACT_COMMAND, '--log-json', '--lock-wait', '5', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -274,7 +257,6 @@ def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options(
|
||||
(*COMPACT_COMMAND, '--log-json', '--extra', '--options', 'value with space', 'repo'),
|
||||
logging.INFO,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
@@ -292,7 +274,6 @@ def test_compact_segments_calls_borg_with_working_directory():
|
||||
logging.INFO,
|
||||
working_directory='/working/dir',
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.compact_segments(
|
||||
dry_run=False,
|
||||
|
||||
@@ -1265,7 +1265,6 @@ def test_create_archive_calls_borg_with_flags():
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1300,7 +1299,6 @@ def test_create_archive_calls_borg_with_environment():
|
||||
working_directory=None,
|
||||
environment=environment,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1479,44 +1477,6 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
'statistics': True,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
borgmatic_runtime_directory='/borgmatic/run',
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_quick_stats_and_dry_run_calls_borg_without_quick_stats():
|
||||
# --dry-run and --quick-stats are mutually exclusive, see:
|
||||
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_base_create_command').and_return(
|
||||
(('borg', 'create', '--dry-run'), REPO_ARCHIVE, flexmock()),
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--dry-run', '--info', *REPO_ARCHIVE),
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
'quick_statistics': True,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
@@ -1544,7 +1504,6 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
|
||||
working_directory='/working/dir',
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1580,7 +1539,6 @@ def test_create_archive_with_exit_codes_calls_borg_using_them():
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1615,7 +1573,6 @@ def test_create_archive_with_stats_calls_borg_with_stats_flag_and_answer_output_
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1654,7 +1611,6 @@ def test_create_archive_with_files_calls_borg_with_answer_output_log_level():
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1724,7 +1680,6 @@ def test_create_archive_with_progress_calls_borg_with_progress_flag():
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1776,7 +1731,6 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
).and_yield()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1810,7 +1764,6 @@ def test_create_archive_with_json_calls_borg_with_json_flag():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_yield('[]')
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
json_output = module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1845,7 +1798,6 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_yield('[]')
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
json_output = module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1854,42 +1806,6 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
|
||||
'source_directories': ['foo*'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
'statistics': True,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
borgmatic_runtime_directory='/borgmatic/run',
|
||||
json=True,
|
||||
)
|
||||
|
||||
assert json_output == '[]'
|
||||
|
||||
|
||||
def test_create_archive_with_quick_stats_and_json_calls_borg_without_quick_stats_flag():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_base_create_command').and_return(
|
||||
(('borg', 'create'), REPO_ARCHIVE, flexmock()),
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'create', '--json', *REPO_ARCHIVE),
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_yield('[]')
|
||||
|
||||
json_output = module.create_archive(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo*'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
'quick_statistics': True,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
@@ -1918,7 +1834,6 @@ def test_create_archive_with_comment_calls_borg_with_comment_flag():
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
@@ -1956,7 +1871,6 @@ def test_create_archive_calls_borg_with_working_directory():
|
||||
working_directory='/working/dir',
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
|
||||
@@ -9,13 +9,13 @@ from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def test_make_delete_command_includes_log_info():
|
||||
insert_logging_mock(logging.INFO)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -31,13 +31,13 @@ def test_make_delete_command_includes_log_info():
|
||||
|
||||
|
||||
def test_make_delete_command_includes_log_debug():
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -63,7 +63,6 @@ def test_make_delete_command_includes_dry_run():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -89,7 +88,6 @@ def test_make_delete_command_includes_remote_path():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -113,7 +111,6 @@ def test_make_delete_command_includes_umask():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -139,7 +136,6 @@ def test_make_delete_command_includes_lock_wait():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -161,7 +157,6 @@ def test_make_delete_command_includes_extra_borg_options():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -187,7 +182,6 @@ def test_make_delete_command_with_list_config_calls_borg_with_list_flag():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -209,7 +203,6 @@ def test_make_delete_command_includes_force():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -231,7 +224,6 @@ def test_make_delete_command_includes_force_twice():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -255,7 +247,6 @@ def test_make_delete_command_includes_archive():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -284,7 +275,6 @@ def test_make_delete_command_includes_match_archives():
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('repo',),
|
||||
)
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
command = module.make_delete_command(
|
||||
repository={'path': 'repo'},
|
||||
@@ -317,7 +307,6 @@ def test_delete_archives_with_archive_calls_borg_delete():
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.delete_archives(
|
||||
repository={'path': 'repo'},
|
||||
@@ -338,7 +327,6 @@ def test_delete_archives_with_match_archives_calls_borg_delete():
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.delete_archives(
|
||||
repository={'path': 'repo'},
|
||||
@@ -360,7 +348,6 @@ def test_delete_archives_with_archive_related_argument_calls_borg_delete(argumen
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.delete_archives(
|
||||
repository={'path': 'repo'},
|
||||
@@ -380,7 +367,6 @@ def test_delete_archives_without_archive_related_argument_calls_borg_repo_delete
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.delete_archives(
|
||||
repository={'path': 'repo'},
|
||||
@@ -417,7 +403,6 @@ def test_delete_archives_calls_borg_delete_with_working_directory():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.delete_archives(
|
||||
repository={'path': 'repo'},
|
||||
|
||||
@@ -4,14 +4,13 @@ from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import diff as module
|
||||
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
LOGGING_ANSWER = flexmock()
|
||||
|
||||
|
||||
def test_diff_calls_borg_with_archives():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -41,7 +40,6 @@ def test_diff_calls_borg_with_archives():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -66,6 +64,7 @@ def test_diff_calls_borg_with_archives():
|
||||
def test_diff_with_local_path_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -95,7 +94,6 @@ def test_diff_with_local_path_calls_borg_with_it():
|
||||
borg_local_path='borg6',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -120,6 +118,7 @@ def test_diff_with_local_path_calls_borg_with_it():
|
||||
def test_diff_with_remote_path_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -151,7 +150,6 @@ def test_diff_with_remote_path_calls_borg_with_it():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -176,6 +174,7 @@ def test_diff_with_remote_path_calls_borg_with_it():
|
||||
def test_diff_with_lock_wait_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -207,7 +206,6 @@ def test_diff_with_lock_wait_calls_borg_with_it():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -232,6 +230,7 @@ def test_diff_with_lock_wait_calls_borg_with_it():
|
||||
def test_diff_with_log_level_info_calls_borg_with_info_flag():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -263,7 +262,6 @@ def test_diff_with_log_level_info_calls_borg_with_info_flag():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -288,6 +286,7 @@ def test_diff_with_log_level_info_calls_borg_with_info_flag():
|
||||
def test_diff_with_log_level_debug_calls_borg_with_debug_flags():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -320,7 +319,6 @@ def test_diff_with_log_level_debug_calls_borg_with_debug_flags():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -345,6 +343,7 @@ def test_diff_with_log_level_debug_calls_borg_with_debug_flags():
|
||||
def test_diff_with_only_patterns_calls_borg_with_configured_pattern_paths():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
@@ -377,7 +376,6 @@ def test_diff_with_only_patterns_calls_borg_with_configured_pattern_paths():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -399,9 +397,68 @@ def test_diff_with_only_patterns_calls_borg_with_configured_pattern_paths():
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_exclude_config_calls_borg_with_exclude_flags():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(
|
||||
('--exclude', 'stuff')
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
|
||||
('--repo', 'repo')
|
||||
)
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
|
||||
environment
|
||||
)
|
||||
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
|
||||
full_command=(
|
||||
'borg',
|
||||
'diff',
|
||||
'--log-json',
|
||||
'--exclude',
|
||||
'stuff',
|
||||
'--repo',
|
||||
'repo',
|
||||
'archive',
|
||||
'archive2',
|
||||
),
|
||||
output_log_level=LOGGING_ANSWER,
|
||||
environment=environment,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
second_archive='archive2',
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
diff_arguments=flexmock(
|
||||
same_chunker_params=False,
|
||||
sort_keys=[],
|
||||
content_only=False,
|
||||
second_archive='archive2',
|
||||
only_patterns=False,
|
||||
),
|
||||
global_arguments=flexmock(),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
patterns=[],
|
||||
)
|
||||
|
||||
|
||||
def test_diff_with_numeric_ids_calls_borg_with_numeric_ids_flag():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -432,7 +489,6 @@ def test_diff_with_numeric_ids_calls_borg_with_numeric_ids_flag():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -457,6 +513,7 @@ def test_diff_with_numeric_ids_calls_borg_with_numeric_ids_flag():
|
||||
def test_diff_with_numeric_ids_and_feature_not_available_calls_borg_with_numeric_owner_flag():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -492,7 +549,6 @@ def test_diff_with_numeric_ids_and_feature_not_available_calls_borg_with_numeric
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -517,6 +573,7 @@ def test_diff_with_numeric_ids_and_feature_not_available_calls_borg_with_numeric
|
||||
def test_diff_with_same_chunker_params_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -547,7 +604,6 @@ def test_diff_with_same_chunker_params_calls_borg_with_it():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -572,6 +628,7 @@ def test_diff_with_same_chunker_params_calls_borg_with_it():
|
||||
def test_diff_with_sort_keys_calls_borg_with_formatted_sort_by_flags():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -603,7 +660,6 @@ def test_diff_with_sort_keys_calls_borg_with_formatted_sort_by_flags():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -628,6 +684,7 @@ def test_diff_with_sort_keys_calls_borg_with_formatted_sort_by_flags():
|
||||
def test_diff_with_content_only_calls_borg_with_it():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -658,7 +715,6 @@ def test_diff_with_content_only_calls_borg_with_it():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -683,6 +739,7 @@ def test_diff_with_content_only_calls_borg_with_it():
|
||||
def test_diff_with_extra_borg_options_calls_borg_with_them():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -714,7 +771,6 @@ def test_diff_with_extra_borg_options_calls_borg_with_them():
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
@@ -739,6 +795,7 @@ def test_diff_with_extra_borg_options_calls_borg_with_them():
|
||||
def test_diff_without_separate_repository_archive_feature_available_calls_borg_joined_repository_archive():
|
||||
flexmock(module.logging).ANSWER = LOGGING_ANSWER
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
|
||||
flexmock(name='test')
|
||||
@@ -771,7 +828,6 @@ def test_diff_without_separate_repository_archive_feature_available_calls_borg_j
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).once()
|
||||
insert_logging_mock(logging.WARNING)
|
||||
|
||||
module.borgmatic.borg.diff.diff(
|
||||
repository='repo',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user