From 2cb9afd4d551dfa5f251d6edafb29de020a4234e Mon Sep 17 00:00:00 2001 From: ratchek Date: Tue, 30 May 2023 10:43:20 +0000 Subject: [PATCH] Add a dev mode that allows for local storing of config files and logs (#1682) Allows vorta to be called with the command-line flag `--development` or `-D` that will make it use a directory in the project tree to store all the settings, logs, and cache. This default directory will be called `.dev_config` and placed in the projects root. Also allows for a custom directory path allowing for multiple "configuration" folders at once. This can be used to prevent the vorta instance that a developer is working on from accessing the configuration files that they have set up for their personal backups. * .gitignore : Add `.dev_config`. * src/vorta/utils.py (parse_args): Add `--development` flag. The default will be `DEFAULT_DIR_FLAG`. * src/vorta/utils.py : Add `DEFAULT_DIR_FLAG`. * src/vorta/config.py : Add methods for populating the config directories exposed by this module. * src/vorta/__main__.py (main): Handle `--development` flag and update config directories if its specified. * Access config constants through the `config` module instead of importing them directly with `from .config import`. --------- Co-authored-by: yfprojects <62463991+real-yfprojects@users.noreply.github.com> --- .gitignore | 2 ++ src/vorta/__main__.py | 18 +++++++++-- src/vorta/application.py | 12 +++++--- src/vorta/borg/check.py | 4 +-- src/vorta/borg/compact.py | 4 +-- src/vorta/borg/create.py | 4 +-- src/vorta/config.py | 61 +++++++++++++++++++++++++++++++------ src/vorta/log.py | 4 +-- src/vorta/utils.py | 20 +++++++++++- src/vorta/views/misc_tab.py | 5 +-- 10 files changed, 107 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 7ebab259..7d83381b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ env venv .env .venv +# dirs created by the --development option +.dev_config/ # Avoid adding translations of source language # Files are still used by Transifex src/vorta/i18n/ts/vorta.en.ts diff --git a/src/vorta/__main__.py b/src/vorta/__main__.py index 8d1e07cd..29d24beb 100644 --- a/src/vorta/__main__.py +++ b/src/vorta/__main__.py @@ -4,13 +4,15 @@ import sys from peewee import SqliteDatabase +# Need to import config as a whole module instead of individual variables +# because we will be overriding the modules variables +from vorta import config from vorta._version import __version__ -from vorta.config import SETTINGS_DIR from vorta.i18n import trans_late, translate from vorta.log import init_logger, logger from vorta.store.connection import init_db from vorta.updater import get_updater -from vorta.utils import parse_args +from vorta.utils import DEFAULT_DIR_FLAG, parse_args def main(): @@ -48,6 +50,7 @@ def main(): want_version = getattr(args, 'version', False) want_background = getattr(args, 'daemonize', False) + want_development = getattr(args, 'development', False) if want_version: print(f"Vorta {__version__}") # noqa: T201 @@ -57,11 +60,20 @@ def main(): if os.fork(): sys.exit() + if want_development: + # if we're using the default dev dir + if want_development is DEFAULT_DIR_FLAG: + config.init_dev_mode(config.default_dev_dir()) + else: + # if we're not using the default dev dir and + # instead we're using whatever dir is passed as an argument + config.init_dev_mode(want_development) + init_logger(background=want_background) # Init database sqlite_db = SqliteDatabase( - SETTINGS_DIR / 'settings.db', + config.SETTINGS_DIR / 'settings.db', pragmas={ 'journal_mode': 'wal', }, diff --git a/src/vorta/application.py b/src/vorta/application.py index 4885b304..fac38440 100644 --- a/src/vorta/application.py +++ b/src/vorta/application.py @@ -7,11 +7,11 @@ from typing import Any, Dict, List, Tuple from PyQt6 import QtCore from PyQt6.QtWidgets import QMessageBox +from vorta import config from vorta.borg.break_lock import BorgBreakJob from vorta.borg.create import BorgCreateJob from vorta.borg.jobs_manager import JobsManager from vorta.borg.version import BorgVersionJob -from vorta.config import LOG_DIR, PROFILE_BOOTSTRAP_FILE, TEMP_DIR from vorta.i18n import init_translations, translate from vorta.notifications import VortaNotifications from vorta.profile_export import ProfileExport @@ -25,7 +25,7 @@ from vorta.views.main_window import MainWindow logger = logging.getLogger(__name__) -APP_ID = TEMP_DIR / "socket" +APP_ID = config.TEMP_DIR / "socket" class VortaApp(QtSingleApplication): @@ -261,7 +261,11 @@ class VortaApp(QtSingleApplication): job = BorgBreakJob(params['cmd'], params) self.jobs_manager.add_job(job) - def bootstrap_profile(self, bootstrap_file=PROFILE_BOOTSTRAP_FILE): + def bootstrap_profile(self, bootstrap_file=None): + # Necessary to dynamically load the variable from config during runtime + # Check out pull request for #1682 for context + bootstrap_file = bootstrap_file or config.PROFILE_BOOTSTRAP_FILE + """ Make sure there is at least one profile when first starting Vorta. Will either import a profile placed in ~/.vorta-init.json @@ -334,7 +338,7 @@ class VortaApp(QtSingleApplication): msg.setIcon(QMessageBox.Icon.Warning) text = translate( 'VortaApp', 'Borg exited with warning status (rc 1). See the logs for details.' - ).format(LOG_DIR.as_uri()) + ).format(config.LOG_DIR.as_uri()) infotext = error_message elif returncode > 128: # 128+N - killed by signal N (e.g. 137 == kill -9) diff --git a/src/vorta/borg/check.py b/src/vorta/borg/check.py index ae4d7f1f..e25443fd 100644 --- a/src/vorta/borg/check.py +++ b/src/vorta/borg/check.py @@ -1,6 +1,6 @@ from typing import Any, Dict -from vorta.config import LOG_DIR +from vorta import config from vorta.i18n import translate from vorta.utils import borg_compat @@ -27,7 +27,7 @@ class BorgCheckJob(BorgJob): self.app.backup_progress_event.emit( f"[{self.params['profile_name']}] " + translate('RepoCheckJob', 'Repo check failed. See the logs for details.').format( - LOG_DIR.as_uri() + config.LOG_DIR.as_uri() ) ) self.app.check_failed_event.emit(result) diff --git a/src/vorta/borg/compact.py b/src/vorta/borg/compact.py index acf495d5..4f110a92 100644 --- a/src/vorta/borg/compact.py +++ b/src/vorta/borg/compact.py @@ -1,6 +1,6 @@ from typing import Any, Dict -from vorta.config import LOG_DIR +from vorta import config from vorta.i18n import trans_late, translate from vorta.utils import borg_compat @@ -30,7 +30,7 @@ class BorgCompactJob(BorgJob): f"[{self.params['profile_name']}] " + translate( 'BorgCompactJob', 'Errors during compaction. See the logs for details.' - ).format(LOG_DIR.as_uri()) + ).format(config.LOG_DIR.as_uri()) ) else: self.app.backup_progress_event.emit(f"[{self.params['profile_name']}] {self.tr('Compaction completed.')}") diff --git a/src/vorta/borg/create.py b/src/vorta/borg/create.py index 0eea27cc..2e7b9196 100644 --- a/src/vorta/borg/create.py +++ b/src/vorta/borg/create.py @@ -3,7 +3,7 @@ import subprocess import tempfile from datetime import datetime as dt -from vorta.config import LOG_DIR +from vorta import config from vorta.i18n import trans_late, translate from vorta.store.models import ( ArchiveModel, @@ -46,7 +46,7 @@ class BorgCreateJob(BorgJob): + translate( 'BorgCreateJob', 'Backup finished with warnings. See the logs for details.', - ).format(LOG_DIR.as_uri()) + ).format(config.LOG_DIR.as_uri()) ) else: self.app.backup_log_event.emit('', {}) diff --git a/src/vorta/config.py b/src/vorta/config.py index b496a94d..2bf14add 100644 --- a/src/vorta/config.py +++ b/src/vorta/config.py @@ -5,14 +5,57 @@ import platformdirs APP_NAME = 'Vorta' APP_AUTHOR = 'BorgBase' APP_ID_DARWIN = 'com.borgbase.client.macos' -dirs = platformdirs.PlatformDirs(APP_NAME, APP_AUTHOR) -SETTINGS_DIR = dirs.user_data_path -LOG_DIR = dirs.user_log_path -CACHE_DIR = dirs.user_cache_path -TEMP_DIR = CACHE_DIR / "tmp" -PROFILE_BOOTSTRAP_FILE = Path.home() / '.vorta-init.json' +SETTINGS_DIR = None +LOG_DIR = None +CACHE_DIR = None +TEMP_DIR = None +PROFILE_BOOTSTRAP_FILE = None -# ensure directories exist -for dir in (SETTINGS_DIR, LOG_DIR, CACHE_DIR, TEMP_DIR): - dir.mkdir(parents=True, exist_ok=True) +def default_dev_dir() -> Path: + """Returns a default dir for config files in the project's main folder""" + return Path(__file__).parent.parent.parent / '.dev_config' + + +def init_from_platformdirs(): + """Initializes config dirs for system-wide use""" + dirs = platformdirs.PlatformDirs(APP_NAME, APP_AUTHOR) + init(dirs.user_data_path, dirs.user_log_path, dirs.user_cache_path, dirs.user_cache_path / 'tmp', Path.home()) + + +def init_dev_mode(dir: Path): + """Initializes config dirs for local use inside provided dir""" + dir_full_path = Path(dir).resolve() + init( + dir_full_path / 'settings', + dir_full_path / 'logs', + dir_full_path / 'cache', + dir_full_path / 'tmp', + dir_full_path, + ) + + +def init(settings: Path, logs: Path, cache: Path, tmp: Path, bootstrap: Path): + """Initializes config directories with provided paths""" + global SETTINGS_DIR + global LOG_DIR + global CACHE_DIR + global TEMP_DIR + global PROFILE_BOOTSTRAP_FILE + SETTINGS_DIR = settings + LOG_DIR = logs + CACHE_DIR = cache + TEMP_DIR = tmp + PROFILE_BOOTSTRAP_FILE = bootstrap / '.vorta-init.json' + ensure_dirs() + + +def ensure_dirs(): + """Creates config dirs and parent dirs if they don't exist""" + # ensure directories exist + for dir in (SETTINGS_DIR, LOG_DIR, CACHE_DIR, TEMP_DIR): + dir.mkdir(parents=True, exist_ok=True) + + +# Make sure that the config values are valid +init_from_platformdirs() diff --git a/src/vorta/log.py b/src/vorta/log.py index b1034e3b..7251c335 100644 --- a/src/vorta/log.py +++ b/src/vorta/log.py @@ -9,7 +9,7 @@ Set up logging to user log dir. Uses the platform's default location: import logging from logging.handlers import TimedRotatingFileHandler -from .config import LOG_DIR +from vorta import config logger = logging.getLogger() @@ -23,7 +23,7 @@ def init_logger(background=False): formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # create handlers - fh = TimedRotatingFileHandler(LOG_DIR / 'vorta.log', when='d', interval=1, backupCount=5) + fh = TimedRotatingFileHandler(config.LOG_DIR / 'vorta.log', when='d', interval=1, backupCount=5) fh.setLevel(logging.DEBUG) fh.setFormatter(formatter) logger.addHandler(fh) diff --git a/src/vorta/utils.py b/src/vorta/utils.py index 7c296073..89510e80 100644 --- a/src/vorta/utils.py +++ b/src/vorta/utils.py @@ -22,6 +22,10 @@ from vorta.i18n import trans_late from vorta.log import logger from vorta.network_status.abc import NetworkStatusMonitor +# Used to store whether a user wanted to override the +# default directory for the --development flag +DEFAULT_DIR_FLAG = object() + borg_compat = BorgCompatibility() _network_status_monitor = None @@ -353,7 +357,21 @@ def parse_args(): help='Create a backup in the background using the given profile. ' 'Vorta must already be running for this to work.', ) - + # the "development" attribute will be None if the flag is not called + # if the flag is called without an extra argument, the "development" attribute + # will be set to the value of DEFAULT_DIR_FLAG. + # if the flag is called with an extra argument, the "development" attribute + # will be set to that argument + parser.add_argument( + '--development', + '-D', + nargs='?', + const=DEFAULT_DIR_FLAG, + metavar="CONFIG_DIRECTORY", + help='Start vorta in a local development environment. ' + 'All log, config, cache, and temp files will be stored within the project tree. ' + 'You can follow this flag with an optional path and it will store the files in the provided location.', + ) return parser.parse_known_args()[0] diff --git a/src/vorta/views/misc_tab.py b/src/vorta/views/misc_tab.py index 087e30ef..2d52880f 100644 --- a/src/vorta/views/misc_tab.py +++ b/src/vorta/views/misc_tab.py @@ -12,8 +12,8 @@ from PyQt6.QtWidgets import ( QSpacerItem, ) +from vorta import config from vorta._version import __version__ -from vorta.config import LOG_DIR from vorta.i18n import translate from vorta.store.models import BackupProfileMixin, SettingsModel from vorta.store.settings import get_misc_settings @@ -34,7 +34,8 @@ class MiscTab(MiscTabBase, MiscTabUI, BackupProfileMixin): self.setupUi(parent) self.versionLabel.setText(__version__) self.logLink.setText( - f'Log' + f'Log' ) self.checkboxLayout = QFormLayout(self.frameSettings)