From 2839f83ddedb537da24b939c46309ab18f320483 Mon Sep 17 00:00:00 2001 From: Maze Date: Fri, 14 Jan 2022 19:27:59 +0000 Subject: [PATCH] Change config dir, offer to migrate Moment has different default directories and is controlled by different environment variables. On first startup, Moment detects Mirage directories and offers to copy them. It doesn't make sense to migrate logins without migrating encryption keys, so this is now included in migration. Themes are migrated as well. Only offer migration is only if both config and data directory are able to be migrated. --- .gitignore | 2 + mirage.pro => moment.pro | 10 +- src/backend/__init__.py | 8 +- src/backend/backend.py | 4 +- src/backend/media_cache.py | 4 +- src/backend/user_files.py | 8 +- src/config/settings.py | 8 +- src/gui/ArgumentParser.qml | 6 +- src/main.cpp | 247 ++++++++++++++++++++++++++++++++++++- 9 files changed, 268 insertions(+), 29 deletions(-) rename mirage.pro => moment.pro (93%) diff --git a/.gitignore b/.gitignore index 7292d91f..32769594 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ dist Makefile mirage mirage.pro.user +moment +moment.pro.user *.AppImage tags diff --git a/mirage.pro b/moment.pro similarity index 93% rename from mirage.pro rename to moment.pro index ae91489f..38131ac8 100644 --- a/mirage.pro +++ b/moment.pro @@ -27,7 +27,7 @@ QRC_FILE = $$BUILD_DIR/resources.qrc RESOURCES += $$QRC_FILE HEADERS += $$glob_filenames(*.h) submodules/hsluv-c/src/hsluv.h SOURCES += $$glob_filenames(*.cpp) submodules/hsluv-c/src/hsluv.c -TARGET = mirage +TARGET = moment unix:!macx { LIBS += -lX11 -lXss @@ -61,12 +61,12 @@ no-x11 { executables.files = $$TARGET shortcuts.path = $$PREFIX/share/applications - shortcuts.files = packaging/mirage.desktop + shortcuts.files = packaging/moment.desktop icons256.path = $$PREFIX/share/icons/hicolor/256x256/apps - icons256.files = packaging/mirage.png + icons256.files = packaging/moment.png - examples.path = $$PREFIX/share/examples/mirage + examples.path = $$PREFIX/share/examples/moment examples.files = src/config/settings.py INSTALLS += executables shortcuts icons256 examples @@ -101,7 +101,7 @@ for(file, $$list($$glob_filenames(*.py))) { } QMAKE_CLEAN *= $$MOC_DIR $$OBJECTS_DIR $$RCC_DIR $$PYCACHE_DIRS $$QRC_FILE -QMAKE_CLEAN *= $$BUILD_DIR $$TARGET Makefile mirage.pro.user .qmake.stash +QMAKE_CLEAN *= $$BUILD_DIR $$TARGET Makefile moment.pro.user .qmake.stash QMAKE_CLEAN *= $$glob_filenames(*.pyc, *.qmlc, *.jsc, *.egg-info) QMAKE_CLEAN *= packaging/flatpak/flatpak-env QMAKE_CLEAN *= packaging/flatpak/flatpak-pip-generator diff --git a/src/backend/__init__.py b/src/backend/__init__.py index fb26c07c..a6c23b27 100644 --- a/src/backend/__init__.py +++ b/src/backend/__init__.py @@ -1,7 +1,7 @@ # Copyright Mirage authors & contributors # SPDX-License-Identifier: LGPL-3.0-or-later -"""This package provides Mirage's backend side that can interact with the UI. +"""This package provides Moment's backend side that can interact with the UI. To learn more about how this package works, you might want to check the documentation in the following modules first: @@ -12,7 +12,7 @@ documentation in the following modules first: - `nio_callbacks` """ -__app_name__ = "mirage" -__display_name__ = "Mirage" -__reverse_dns__ = "io.github.mirukana.mirage" +__app_name__ = "moment" +__display_name__ = "Moment" +__reverse_dns__ = "xyz.mx-moment" __version__ = "0.7.2" diff --git a/src/backend/backend.py b/src/backend/backend.py index 89a96a24..cf8b359f 100644 --- a/src/backend/backend.py +++ b/src/backend/backend.py @@ -104,7 +104,7 @@ class Backend: media_cache: A matrix media cache for downloaded files. presences: A `{user_id: Presence}` dict for storing presence info about - matrix users registered on Mirage. + matrix users registered on Moment. mxc_events: A dict storing media `Event` model items for any account that have the same mxc URI @@ -137,7 +137,7 @@ class Backend: DefaultDict(asyncio.Lock) # {room_id: lock} cache_dir = Path( - os.environ.get("MIRAGE_CACHE_DIR") or self.appdirs.user_cache_dir, + os.environ.get("MOMENT_CACHE_DIR") or self.appdirs.user_cache_dir, ) self.media_cache: MediaCache = MediaCache(self, cache_dir) diff --git a/src/backend/media_cache.py b/src/backend/media_cache.py index 3e9a58fc..2dee6218 100644 --- a/src/backend/media_cache.py +++ b/src/backend/media_cache.py @@ -95,7 +95,7 @@ class Media: // _.` ``` - e.g. `~/.cache/mirage/downloads/matrix.org/foo_Hm24ar11i768b0el.png`. + e.g. `~/.cache/moment/downloads/matrix.org/foo_Hm24ar11i768b0el.png`. """ parsed = urlparse(self.mxc) @@ -304,7 +304,7 @@ class Thumbnail(Media): _.` ``` e.g. - `~/.cache/mirage/thumbnails/matrix.org/32x32/foo_Hm24ar11i768b0el.png`. + `~/.cache/moment/thumbnails/matrix.org/32x32/foo_Hm24ar11i768b0el.png`. """ size = self.normalize_size(self.server_size or self.wanted_size) diff --git a/src/backend/user_files.py b/src/backend/user_files.py index 4a43fca1..28b51cbe 100644 --- a/src/backend/user_files.py +++ b/src/backend/user_files.py @@ -197,7 +197,7 @@ class ConfigFile(UserFile): @property def path(self) -> Path: return Path( - os.environ.get("MIRAGE_CONFIG_DIR") or + os.environ.get("MOMENT_CONFIG_DIR") or self.backend.appdirs.user_config_dir, ) / self.filename @@ -209,7 +209,7 @@ class UserDataFile(UserFile): @property def path(self) -> Path: return Path( - os.environ.get("MIRAGE_DATA_DIR") or + os.environ.get("MOMENT_DATA_DIR") or self.backend.appdirs.user_data_dir, ) / self.filename @@ -460,7 +460,7 @@ class NewTheme(UserDataFile, PCNFile): @property def path(self) -> Path: data_dir = Path( - os.environ.get("MIRAGE_DATA_DIR") or + os.environ.get("MOMENT_DATA_DIR") or self.backend.appdirs.user_data_dir, ) return data_dir / "themes" / self.filename @@ -515,7 +515,7 @@ class Theme(UserDataFile): @property def path(self) -> Path: data_dir = Path( - os.environ.get("MIRAGE_DATA_DIR") or + os.environ.get("MOMENT_DATA_DIR") or self.backend.appdirs.user_data_dir, ) return data_dir / "themes" / self.filename diff --git a/src/config/settings.py b/src/config/settings.py index 16c37d1a..ce1abd46 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -29,7 +29,7 @@ class General: # Can be the name of a built-in theme (Mirage.qpl or Glass.qpl), or # the name (including extension) of a file in the user theme folder, which # is "$XDG_DATA_HOME/mirage/themes" if that environment variable is set, - # else "~/.local/share/mirage/themes". + # else "~/.local/share/moment/themes". # For Flatpak, it is # "~/.var/app/io.github.mirukana.mirage/data/mirage/themes". theme: str = "Midnight.qpl" @@ -204,7 +204,7 @@ class Chat: auto_play_gif: bool = True # When clicking on a file in the timeline, open it in an external - # program instead of displaying it using Mirage's interface. + # program instead of displaying it using Moment's interface. # On Linux, the xdg-open command is called. click_opens_externally: bool = False @@ -234,7 +234,7 @@ class Keys: # would need to be pressed. # # A list of default bindings can be found at: - # https://github.com/mirukana/mirage/blob/master/docs/KEYBINDINGS.md + # https://gitlab.com/mx-moment/moment/-/blob/main/docs/KEYBINDINGS.md # Helper functions @@ -280,7 +280,7 @@ class Keys: qml_console = ["F1"] # Start the Python backend debugger. - # Mirage must be connected to a terminal for this to work. + # Moment must be connected to a terminal for this to work. python_debugger = ["Shift+F1"] # Start the Python backend debugger in remote access mode. diff --git a/src/gui/ArgumentParser.qml b/src/gui/ArgumentParser.qml index 8a2e2ae8..17224427 100644 --- a/src/gui/ArgumentParser.qml +++ b/src/gui/ArgumentParser.qml @@ -18,9 +18,9 @@ QtObject { -h, --help Show this help and exit Environment variables: - MIRAGE_CONFIG_DIR Override the default configuration folder - MIRAGE_DATA_DIR Override the default application data folder - MIRAGE_CACHE_DIR Override the default cache and downloads folder + MOMENT_CONFIG_DIR Override the default configuration folder + MOMENT_DATA_DIR Override the default application data folder + MOMENT_CACHE_DIR Override the default cache and downloads folder http_proxy Override the General.proxy setting, see settings.py ` diff --git a/src/main.cpp b/src/main.cpp index 197a6f4e..2d671108 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #ifdef Q_OS_UNIX @@ -97,6 +98,236 @@ void onExitSignal(int signum) { } +void migrateFile(QDir source, QDir destination, QString fname) { + if (! QFile::copy(source.filePath(fname), destination.filePath(fname))) + qWarning() << "Could not migrate" << fname; +} + + +void migrateShallowDirectory( + QDir sourceParent, QDir destinationParent, QString dname + ) { + if (sourceParent.exists(dname)) { + if (! destinationParent.mkpath(dname)) { + qWarning() << "Could not create directory" + << destinationParent.filePath(dname); + exit(EXIT_FAILURE); + } + QDir source(sourceParent.filePath(dname)); + QDir destination(destinationParent.filePath(dname)); + QFileInfoList files = source.entryInfoList(); + for (QFileInfo file : files) { + if(file.fileName() == "." || file.fileName() == "..") + continue; + migrateFile(source, destination, file.fileName()); + } + } +} + + +void offerMigrateFromMirage( + QDir configDirMoment, QDir configDirMirage, + QDir dataDirMoment, QDir dataDirMirage + ) { + QMessageBox dialog; + dialog.setText("Would you like Moment to re-use your logins, " + "encryption keys, configuration and themes from Mirage?"); + dialog.setInformativeText( + "Will copy "+configDirMirage.path()+" → "+configDirMoment.path() + +"\nWill copy "+dataDirMirage.path()+" → "+dataDirMoment.path()); + dialog.addButton(QMessageBox::Yes); + dialog.addButton(QMessageBox::No); + dialog.addButton(QMessageBox::Cancel); + int result = dialog.exec(); + if (result == QMessageBox::Yes) { + qWarning("Migrating config and data from Mirage"); + if (! configDirMoment.mkpath(".")) { + qFatal("Could not create config directory"); + exit(EXIT_FAILURE); + } + if (! dataDirMoment.mkpath(".")) { + qFatal("Could not create data directory"); + exit(EXIT_FAILURE); + } + migrateFile(configDirMirage, configDirMoment, "settings.py"); + migrateFile(configDirMirage, configDirMoment, "settings.gui.json"); + migrateFile(configDirMirage, configDirMoment, "accounts.json"); + migrateFile(dataDirMirage, dataDirMoment, "history.json"); + migrateFile(dataDirMirage, dataDirMoment, "state.json"); + migrateShallowDirectory(dataDirMirage, dataDirMoment, "themes"); + migrateShallowDirectory(dataDirMirage, dataDirMoment, "encryption"); + } else if (result == QMessageBox::No) { + // Nothing to do. Proceed with starting the app + qWarning("Not migrating"); + return; + } else { + // Neither "Yes" nor "No" was chosen. + // We can't know what the user wants. Just quit. + qWarning("Quitting. You can decide about migration next time."); + exit(EXIT_SUCCESS); + } +} + + +bool shouldMigrateFromMirage() { + QString genericConfig = QStandardPaths::writableLocation( + QStandardPaths::GenericConfigLocation); + QString genericData = QStandardPaths::writableLocation( + QStandardPaths::GenericDataLocation); + + // Check whether Moment config already exists + { + QString customConfigDirMoment( + qEnvironmentVariable("MOMENT_CONFIG_DIR") + ); + if (! customConfigDirMoment.isEmpty()) { + // MOMENT_CONFIG_DIR is set. + // Moment would definitely use this as the config directory. + if (QDir(customConfigDirMoment).exists()) + // But it already exists. + // So this is not the first time Moment was started. + return false; + } else { + // No MOMENT_CONFIG_DIR, so check the default config directory. + if (QDir(genericConfig + "/moment").exists()) + // The default config folder exists. + // So this is not the first time Moment was started. + return false; + } + } + + // Check whether Moment data already exists + { + QString customDataDirMoment(qEnvironmentVariable("MOMENT_DATA_DIR")); + if (! customDataDirMoment.isEmpty()) { + // MOMENT_DATA_DIR is set. + // Moment would definitely use this as the data directory. + if (QDir(customDataDirMoment).exists()) + // But it already exists. + // So this is not the first time Moment was started. + return false; + } else { + // No MOMENT_DATA_DIR, so check the default data directory. + if (QDir(genericData + "/moment").exists()) + // The default data folder exists. + // So this is not the first time Moment was started. + return false; + } + } + + // Check whether Mirage config exists + { + QString customConfigDirMirage( + qEnvironmentVariable("MIRAGE_CONFIG_DIR") + ); + if (! customConfigDirMirage.isEmpty()) { + // MIRAGE_CONFIG_DIR is set. + // Mirage would definitely use this as the config directory. + if (! QDir(customConfigDirMirage).exists()) + // But this directory does not exist. + // So there is nowhere to migrate from. + return false; + } else { + // No MIRAGE_CONFIG_DIR, so check the default config directory. + // Check /matrix-mirage (Debian) first, since it is more specific + if (! QDir(genericConfig + "/matrix-mirage").exists()) + // Default Debian config folder does not exist, + // so check whether the normal /mirage exists + if (! QDir(genericConfig + "/mirage").exists()) + // No, neither /matrix-mirage nor /mirage exist. + // So there is nowhere to migrate from. + return false; + } + } + + // We found out that there is no Moment config dir nor Moment data dir, + // but there is a Mirage config dir which can be migrated from. + // We could also check for Mirage data dir but it doesn't really matter. + // User should definitely be prompted for migration. + return true; +} + + +void tryMigrateFromMirage() { + QString genericConfig = QStandardPaths::writableLocation( + QStandardPaths::GenericConfigLocation); + QString genericData = QStandardPaths::writableLocation( + QStandardPaths::GenericDataLocation); + + QDir configDirMoment, configDirMirage, dataDirMoment, dataDirMirage; + + QString customConfigDirMoment(qEnvironmentVariable("MOMENT_CONFIG_DIR")); + configDirMoment = QDir(customConfigDirMoment.isEmpty() + ? genericConfig + "/moment" : customConfigDirMoment); + + QString customDataDirMoment(qEnvironmentVariable("MOMENT_DATA_DIR")); + dataDirMoment = QDir(customDataDirMoment.isEmpty() + ? genericData + "/moment" : customDataDirMoment); + + QString customConfigDirMirage(qEnvironmentVariable("MIRAGE_CONFIG_DIR")); + if (! customConfigDirMirage.isEmpty()) { + // MIRAGE_CONFIG_DIR is set. + // Mirage would definitely use this as the config directory. + // So this is where we should migrate from. + configDirMirage = QDir(customConfigDirMirage); + } else { + // No MIRAGE_CONFIG_DIR. + // Check if Mirage default config directory exists + // Check /matrix-mirage (Debian) first + QDir dirDeb(genericConfig + "/matrix-mirage"); + if (dirDeb.exists()) { + // Default Debian config dir exists. + // So this is where we should migrate from. + configDirMirage = dirDeb; + } else { + // No /matrix-mirage found, so check /mirage + QDir dir(genericConfig + "/mirage"); + if (dir.exists()) + // Default config dir exists. + // So this is where we should migrate from. + configDirMirage = dir; + else + // No Mirage config dir found. + // Do not migrate. + return; + } + } + + QString customDataDirMirage(qEnvironmentVariable("MIRAGE_DATA_DIR")); + if (! customDataDirMirage.isEmpty()) { + // MIRAGE_DATA_DIR is set. + // Mirage would definitely use this as the data directory. + // So this is where we should migrate from. + dataDirMirage = QDir(customDataDirMirage); + } else { + // No MIRAGE_DATA_DIR. + // Check if Mirage default data directory exists + // Check /matrix-mirage (Debian) first + QDir dirDeb(genericData + "/matrix-mirage"); + if (dirDeb.exists()) { + // Default Debian data dir exists. + // So this is where we should migrate from. + dataDirMirage = dirDeb; + } else { + // No /matrix-mirage found, so check /mirage + QDir dir(genericData + "/mirage"); + if (dir.exists()) + // Default data dir exists. + // So this is where we should migrate from. + dataDirMirage = dir; + else + // No Mirage data dir found. + // Do not migrate. + return; + } + } + + offerMigrateFromMirage( + configDirMoment, configDirMirage, dataDirMoment, dataDirMirage + ); +} + + bool setLockFile(QString configPath) { QDir settingsFolder(configPath); @@ -130,13 +361,19 @@ int main(int argc, char *argv[]) { qInstallMessageHandler(loggingHandler); // Define some basic info about the app before creating the QApplication - QApplication::setOrganizationName("mirage"); - QApplication::setApplicationName("mirage"); - QApplication::setApplicationDisplayName("Mirage"); + QApplication::setOrganizationName("moment"); + QApplication::setApplicationName("moment"); + QApplication::setApplicationDisplayName("Moment"); QApplication::setApplicationVersion("0.7.2"); QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - QString customConfigDir(qEnvironmentVariable("MIRAGE_CONFIG_DIR")); + // app needs to be constructed before attempting to migrate + // because migrate displays a popup dialog + QApplication app(argc, argv); + + if (shouldMigrateFromMirage()) tryMigrateFromMirage(); + + QString customConfigDir(qEnvironmentVariable("MOMENT_CONFIG_DIR")); QString settingsFolder( customConfigDir.isEmpty() ? QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) @@ -144,8 +381,8 @@ int main(int argc, char *argv[]) { customConfigDir ); + // Attempt to create a lockfile in the settings folder if (! setLockFile(settingsFolder)) return EXIT_SUCCESS; - QApplication app(argc, argv); // Register handlers for quit signals, e.g. SIGINT/Ctrl-C in unix terminals signal(SIGINT, onExitSignal);