From be10b462fc27d5b7dad77bc69fd7a162af052c66 Mon Sep 17 00:00:00 2001 From: Andrew Senetar Date: Tue, 17 Aug 2021 21:04:09 -0500 Subject: [PATCH] Add portable mode If settings.ini is present next to the executable, will run in portable mode. This results in settings, data, and cache all being in same folder as dupeGuru. --- core/app.py | 4 ++-- core/util.py | 6 ++++++ hscommon/desktop.py | 22 ++++++++++++++-------- qt/app.py | 2 +- qt/preferences.py | 2 ++ qtlib/preferences.py | 28 ++++++++++++++++++---------- qtlib/util.py | 8 ++++++-- run.py | 4 ++-- 8 files changed, 51 insertions(+), 25 deletions(-) diff --git a/core/app.py b/core/app.py index 0e7656e9..b6259236 100644 --- a/core/app.py +++ b/core/app.py @@ -126,13 +126,13 @@ class DupeGuru(Broadcaster): PICTURE_CACHE_TYPE = "sqlite" # set to 'shelve' for a ShelveCache - def __init__(self, view): + def __init__(self, view, portable=False): if view.get_default(DEBUG_MODE_PREFERENCE): logging.getLogger().setLevel(logging.DEBUG) logging.debug("Debug mode enabled") Broadcaster.__init__(self) self.view = view - self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME) + self.appdata = desktop.special_folder_path(desktop.SpecialFolder.AppData, appname=self.NAME, portable=portable) if not op.exists(self.appdata): os.makedirs(self.appdata) self.app_mode = AppMode.Standard diff --git a/core/util.py b/core/util.py index 7cfa3090..7ae750ba 100644 --- a/core/util.py +++ b/core/util.py @@ -5,6 +5,8 @@ # http://www.gnu.org/licenses/gpl-3.0.html import time +import sys +import os from hscommon.util import format_time_decimal @@ -58,3 +60,7 @@ def fix_surrogate_encoding(s, encoding="utf-8"): return s.encode(encoding, "replace").decode(encoding) else: return s + + +def executable_folder(): + return os.path.dirname(os.path.abspath(sys.argv[0])) diff --git a/hscommon/desktop.py b/hscommon/desktop.py index 0defad28..8ce3b72a 100644 --- a/hscommon/desktop.py +++ b/hscommon/desktop.py @@ -30,7 +30,7 @@ def reveal_path(path): _reveal_path(str(path)) -def special_folder_path(special_folder, appname=None): +def special_folder_path(special_folder, appname=None, portable=False): """Returns the path of ``special_folder``. ``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current @@ -38,7 +38,7 @@ def special_folder_path(special_folder, appname=None): You can override the application name with ``appname``. This argument is ingored under Qt. """ - return _special_folder_path(special_folder, appname) + return _special_folder_path(special_folder, appname, portable=portable) try: @@ -54,7 +54,7 @@ try: _open_path = proxy.openPath_ _reveal_path = proxy.revealPath_ - def _special_folder_path(special_folder, appname=None): + def _special_folder_path(special_folder, appname=None, portable=False): if special_folder == SpecialFolder.Cache: base = proxy.getCachePath() else: @@ -68,6 +68,9 @@ except ImportError: try: from PyQt5.QtCore import QUrl, QStandardPaths from PyQt5.QtGui import QDesktopServices + from qtlib.util import getAppData + from core.util import executable_folder + from hscommon.plat import ISWINDOWS def _open_url(url): QDesktopServices.openUrl(QUrl(url)) @@ -79,12 +82,15 @@ except ImportError: def _reveal_path(path): _open_path(op.dirname(str(path))) - def _special_folder_path(special_folder, appname=None): + def _special_folder_path(special_folder, appname=None, portable=False): if special_folder == SpecialFolder.Cache: - qtfolder = QStandardPaths.CacheLocation + if ISWINDOWS and portable: + folder = op.join(executable_folder(), "cache") + else: + folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0] else: - qtfolder = QStandardPaths.DataLocation - return QStandardPaths.standardLocations(qtfolder)[0] + folder = getAppData(portable) + return folder except ImportError: # We're either running tests, and these functions don't matter much or we're in a really @@ -97,5 +103,5 @@ except ImportError: def _reveal_path(path): pass - def _special_folder_path(special_folder, appname=None): + def _special_folder_path(special_folder, appname=None, portable=False): return "/tmp" diff --git a/qt/app.py b/qt/app.py index a68f5be0..d9988c8a 100644 --- a/qt/app.py +++ b/qt/app.py @@ -52,7 +52,7 @@ class DupeGuru(QObject): # Enable tabs instead of separate floating windows for each dialog # Could be passed as an argument to this class if we wanted self.use_tabs = True - self.model = DupeGuruModel(view=self) + self.model = DupeGuruModel(view=self, portable=self.prefs.portable) self._setup() # --- Private diff --git a/qt/preferences.py b/qt/preferences.py index 51a4be99..dc9f1a82 100644 --- a/qt/preferences.py +++ b/qt/preferences.py @@ -29,6 +29,7 @@ class Preferences(PreferencesBase): self.language = get("Language", self.language) if not self.language and trans.installed_lang: self.language = trans.installed_lang + self.portable = get("Portable", False) self.tableFontSize = get("TableFontSize", self.tableFontSize) self.reference_bold_font = get("ReferenceBoldFont", self.reference_bold_font) @@ -138,6 +139,7 @@ class Preferences(PreferencesBase): set_("DestinationType", self.destination_type) set_("CustomCommand", self.custom_command) set_("Language", self.language) + set_("Portable", self.portable) set_("TableFontSize", self.tableFontSize) set_("ReferenceBoldFont", self.reference_bold_font) diff --git a/qtlib/preferences.py b/qtlib/preferences.py index 371e681e..5bfd1d25 100644 --- a/qtlib/preferences.py +++ b/qtlib/preferences.py @@ -12,6 +12,7 @@ from PyQt5.QtWidgets import QDockWidget from hscommon.trans import trget from hscommon.util import tryint from hscommon.plat import ISWINDOWS +from core.util import executable_folder from os import path as op @@ -68,18 +69,25 @@ def adjust_after_deserialization(v): return v -def createQSettings(): +def create_qsettings(): # Create a QSettings instance with the correct arguments. - # On windows use an ini file in the AppDataLocation instead of registry if possible as it - # makes it easier for a user to clear it out when there are issues. - if ISWINDOWS: - Locations = QStandardPaths.standardLocations(QStandardPaths.AppDataLocation) - if Locations: - return QSettings(op.join(Locations[0], "settings.ini"), QSettings.IniFormat) + config_location = op.join(executable_folder(), "settings.ini") + if op.isfile(config_location): + settings = QSettings(config_location, QSettings.IniFormat) + settings.setValue("Portable", True) + elif ISWINDOWS: + # On windows use an ini file in the AppDataLocation instead of registry if possible as it + # makes it easier for a user to clear it out when there are issues. + locations = QStandardPaths.standardLocations(QStandardPaths.AppDataLocation) + if locations: + settings = QSettings(op.join(locations[0], "settings.ini"), QSettings.IniFormat) else: - return QSettings() + settings = QSettings() + settings.setValue("Portable", False) else: - return QSettings() + settings = QSettings() + settings.setValue("Portable", False) + return settings # About QRect conversion: @@ -93,7 +101,7 @@ class Preferences(QObject): def __init__(self): QObject.__init__(self) self.reset() - self._settings = createQSettings() + self._settings = create_qsettings() def _load_values(self, settings, get): pass diff --git a/qtlib/util.py b/qtlib/util.py index 8ac8d268..3bb6dc82 100644 --- a/qtlib/util.py +++ b/qtlib/util.py @@ -12,6 +12,7 @@ import os.path as op import os import logging +from core.util import executable_folder from hscommon.util import first from PyQt5.QtCore import QStandardPaths @@ -88,8 +89,11 @@ def setAccelKeys(menu): action.setText(newtext) -def getAppData(): - return QStandardPaths.standardLocations(QStandardPaths.DataLocation)[0] +def getAppData(portable=False): + if portable: + return op.join(executable_folder(), "data") + else: + return QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)[0] class SysWrapper(io.IOBase): diff --git a/run.py b/run.py index e6f779e6..0254b1d8 100644 --- a/run.py +++ b/run.py @@ -16,7 +16,7 @@ from PyQt5.QtWidgets import QApplication from hscommon.trans import install_gettext_trans_under_qt from qtlib.error_report_dialog import install_excepthook from qtlib.util import setupQtLogging -from qtlib.preferences import createQSettings +from qtlib.preferences import create_qsettings from qt import dg_rc # noqa: F401 from qt.platform import BASE_PATH from core import __version__, __appname__ @@ -53,7 +53,7 @@ def main(): QCoreApplication.setApplicationName(__appname__) QCoreApplication.setApplicationVersion(__version__) setupQtLogging() - settings = createQSettings() + settings = create_qsettings() lang = settings.value("Language") locale_folder = op.join(BASE_PATH, "locale") install_gettext_trans_under_qt(locale_folder, lang)