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.
This commit is contained in:
Maze 2022-01-14 19:27:59 +00:00
parent 63a56d92fd
commit 2839f83dde
9 changed files with 268 additions and 29 deletions

2
.gitignore vendored
View File

@ -16,6 +16,8 @@ dist
Makefile Makefile
mirage mirage
mirage.pro.user mirage.pro.user
moment
moment.pro.user
*.AppImage *.AppImage
tags tags

View File

@ -27,7 +27,7 @@ QRC_FILE = $$BUILD_DIR/resources.qrc
RESOURCES += $$QRC_FILE RESOURCES += $$QRC_FILE
HEADERS += $$glob_filenames(*.h) submodules/hsluv-c/src/hsluv.h HEADERS += $$glob_filenames(*.h) submodules/hsluv-c/src/hsluv.h
SOURCES += $$glob_filenames(*.cpp) submodules/hsluv-c/src/hsluv.c SOURCES += $$glob_filenames(*.cpp) submodules/hsluv-c/src/hsluv.c
TARGET = mirage TARGET = moment
unix:!macx { unix:!macx {
LIBS += -lX11 -lXss LIBS += -lX11 -lXss
@ -61,12 +61,12 @@ no-x11 {
executables.files = $$TARGET executables.files = $$TARGET
shortcuts.path = $$PREFIX/share/applications shortcuts.path = $$PREFIX/share/applications
shortcuts.files = packaging/mirage.desktop shortcuts.files = packaging/moment.desktop
icons256.path = $$PREFIX/share/icons/hicolor/256x256/apps 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 examples.files = src/config/settings.py
INSTALLS += executables shortcuts icons256 examples 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 *= $$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 *= $$glob_filenames(*.pyc, *.qmlc, *.jsc, *.egg-info)
QMAKE_CLEAN *= packaging/flatpak/flatpak-env QMAKE_CLEAN *= packaging/flatpak/flatpak-env
QMAKE_CLEAN *= packaging/flatpak/flatpak-pip-generator QMAKE_CLEAN *= packaging/flatpak/flatpak-pip-generator

View File

@ -1,7 +1,7 @@
# Copyright Mirage authors & contributors <https://github.com/mirukana/mirage> # Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
# SPDX-License-Identifier: LGPL-3.0-or-later # 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 To learn more about how this package works, you might want to check the
documentation in the following modules first: documentation in the following modules first:
@ -12,7 +12,7 @@ documentation in the following modules first:
- `nio_callbacks` - `nio_callbacks`
""" """
__app_name__ = "mirage" __app_name__ = "moment"
__display_name__ = "Mirage" __display_name__ = "Moment"
__reverse_dns__ = "io.github.mirukana.mirage" __reverse_dns__ = "xyz.mx-moment"
__version__ = "0.7.2" __version__ = "0.7.2"

View File

@ -104,7 +104,7 @@ class Backend:
media_cache: A matrix media cache for downloaded files. media_cache: A matrix media cache for downloaded files.
presences: A `{user_id: Presence}` dict for storing presence info about 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 mxc_events: A dict storing media `Event` model items for any account
that have the same mxc URI that have the same mxc URI
@ -137,7 +137,7 @@ class Backend:
DefaultDict(asyncio.Lock) # {room_id: lock} DefaultDict(asyncio.Lock) # {room_id: lock}
cache_dir = Path( 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) self.media_cache: MediaCache = MediaCache(self, cache_dir)

View File

@ -95,7 +95,7 @@ class Media:
<base download folder>/<homeserver domain>/ <base download folder>/<homeserver domain>/
<file title>_<mxc id>.<file extension>` <file title>_<mxc id>.<file extension>`
``` ```
e.g. `~/.cache/mirage/downloads/matrix.org/foo_Hm24ar11i768b0el.png`. e.g. `~/.cache/moment/downloads/matrix.org/foo_Hm24ar11i768b0el.png`.
""" """
parsed = urlparse(self.mxc) parsed = urlparse(self.mxc)
@ -304,7 +304,7 @@ class Thumbnail(Media):
<file title>_<mxc id>.<file extension>` <file title>_<mxc id>.<file extension>`
``` ```
e.g. 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) size = self.normalize_size(self.server_size or self.wanted_size)

View File

@ -197,7 +197,7 @@ class ConfigFile(UserFile):
@property @property
def path(self) -> Path: def path(self) -> Path:
return Path( return Path(
os.environ.get("MIRAGE_CONFIG_DIR") or os.environ.get("MOMENT_CONFIG_DIR") or
self.backend.appdirs.user_config_dir, self.backend.appdirs.user_config_dir,
) / self.filename ) / self.filename
@ -209,7 +209,7 @@ class UserDataFile(UserFile):
@property @property
def path(self) -> Path: def path(self) -> Path:
return Path( return Path(
os.environ.get("MIRAGE_DATA_DIR") or os.environ.get("MOMENT_DATA_DIR") or
self.backend.appdirs.user_data_dir, self.backend.appdirs.user_data_dir,
) / self.filename ) / self.filename
@ -460,7 +460,7 @@ class NewTheme(UserDataFile, PCNFile):
@property @property
def path(self) -> Path: def path(self) -> Path:
data_dir = Path( data_dir = Path(
os.environ.get("MIRAGE_DATA_DIR") or os.environ.get("MOMENT_DATA_DIR") or
self.backend.appdirs.user_data_dir, self.backend.appdirs.user_data_dir,
) )
return data_dir / "themes" / self.filename return data_dir / "themes" / self.filename
@ -515,7 +515,7 @@ class Theme(UserDataFile):
@property @property
def path(self) -> Path: def path(self) -> Path:
data_dir = Path( data_dir = Path(
os.environ.get("MIRAGE_DATA_DIR") or os.environ.get("MOMENT_DATA_DIR") or
self.backend.appdirs.user_data_dir, self.backend.appdirs.user_data_dir,
) )
return data_dir / "themes" / self.filename return data_dir / "themes" / self.filename

View File

@ -29,7 +29,7 @@ class General:
# Can be the name of a built-in theme (Mirage.qpl or Glass.qpl), or # 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 # 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, # 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 # For Flatpak, it is
# "~/.var/app/io.github.mirukana.mirage/data/mirage/themes". # "~/.var/app/io.github.mirukana.mirage/data/mirage/themes".
theme: str = "Midnight.qpl" theme: str = "Midnight.qpl"
@ -204,7 +204,7 @@ class Chat:
auto_play_gif: bool = True auto_play_gif: bool = True
# When clicking on a file in the timeline, open it in an external # 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. # On Linux, the xdg-open command is called.
click_opens_externally: bool = False click_opens_externally: bool = False
@ -234,7 +234,7 @@ class Keys:
# would need to be pressed. # would need to be pressed.
# #
# A list of default bindings can be found at: # 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 # Helper functions
@ -280,7 +280,7 @@ class Keys:
qml_console = ["F1"] qml_console = ["F1"]
# Start the Python backend debugger. # 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"] python_debugger = ["Shift+F1"]
# Start the Python backend debugger in remote access mode. # Start the Python backend debugger in remote access mode.

View File

@ -18,9 +18,9 @@ QtObject {
-h, --help Show this help and exit -h, --help Show this help and exit
Environment variables: Environment variables:
MIRAGE_CONFIG_DIR Override the default configuration folder MOMENT_CONFIG_DIR Override the default configuration folder
MIRAGE_DATA_DIR Override the default application data folder MOMENT_DATA_DIR Override the default application data folder
MIRAGE_CACHE_DIR Override the default cache and downloads folder MOMENT_CACHE_DIR Override the default cache and downloads folder
http_proxy Override the General.proxy setting, see settings.py http_proxy Override the General.proxy setting, see settings.py
` `

View File

@ -17,6 +17,7 @@
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QLockFile> #include <QLockFile>
#include <QMessageBox>
#include <signal.h> #include <signal.h>
#ifdef Q_OS_UNIX #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) { bool setLockFile(QString configPath) {
QDir settingsFolder(configPath); QDir settingsFolder(configPath);
@ -130,13 +361,19 @@ int main(int argc, char *argv[]) {
qInstallMessageHandler(loggingHandler); qInstallMessageHandler(loggingHandler);
// Define some basic info about the app before creating the QApplication // Define some basic info about the app before creating the QApplication
QApplication::setOrganizationName("mirage"); QApplication::setOrganizationName("moment");
QApplication::setApplicationName("mirage"); QApplication::setApplicationName("moment");
QApplication::setApplicationDisplayName("Mirage"); QApplication::setApplicationDisplayName("Moment");
QApplication::setApplicationVersion("0.7.2"); QApplication::setApplicationVersion("0.7.2");
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 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( QString settingsFolder(
customConfigDir.isEmpty() ? customConfigDir.isEmpty() ?
QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)
@ -144,8 +381,8 @@ int main(int argc, char *argv[]) {
customConfigDir customConfigDir
); );
// Attempt to create a lockfile in the settings folder
if (! setLockFile(settingsFolder)) return EXIT_SUCCESS; if (! setLockFile(settingsFolder)) return EXIT_SUCCESS;
QApplication app(argc, argv);
// Register handlers for quit signals, e.g. SIGINT/Ctrl-C in unix terminals // Register handlers for quit signals, e.g. SIGINT/Ctrl-C in unix terminals
signal(SIGINT, onExitSignal); signal(SIGINT, onExitSignal);