From b77b1747f148cf1bb69d2fc9aee4068865bcc480 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Sun, 12 Feb 2023 03:22:21 +0300 Subject: [PATCH] feat: add migration to move old files to xdg directory + add db column for migration version + handle pre-init migrations + handle post-init migration --- app/db/sqlite/migrations.py | 64 +++++++++++++++++++ app/db/sqlite/queries.py | 10 +++ app/migrations/__init__.py | 28 +++++++- app/migrations/_preinit/__init__.py | 38 +++++++++++ app/migrations/_preinit/move_to_xdg_folder.py | 49 ++++++++++++++ app/migrations/main/__init__.py | 10 ++- app/migrations/main/sample.py | 6 -- app/migrations/userdata/__init__.py | 10 ++- app/migrations/userdata/sample.py | 6 -- app/setup/__init__.py | 13 +++- 10 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 app/db/sqlite/migrations.py create mode 100644 app/migrations/_preinit/__init__.py create mode 100644 app/migrations/_preinit/move_to_xdg_folder.py delete mode 100644 app/migrations/main/sample.py delete mode 100644 app/migrations/userdata/sample.py diff --git a/app/db/sqlite/migrations.py b/app/db/sqlite/migrations.py new file mode 100644 index 00000000..93763f59 --- /dev/null +++ b/app/db/sqlite/migrations.py @@ -0,0 +1,64 @@ +""" +Reads and saves the latest database migrations version. +""" + + +from app.db.sqlite.utils import SQLiteManager + + +class MigrationManager: + all_get_sql = "SELECT * FROM migrations" + pre_init_set_sql = "UPDATE migrations SET pre_init_version = ? WHERE id = 1" + post_init_set_sql = "UPDATE migrations SET post_init_version = ? WHERE id = 1" + + @classmethod + def get_preinit_version(cls) -> int: + """ + Returns the latest userdata pre-init database version. + """ + with SQLiteManager() as cur: + cur.execute(cls.all_get_sql) + return int(cur.fetchone()[1]) + + @classmethod + def get_maindb_postinit_version(cls) -> int: + """ + Returns the latest maindb post-init database version. + """ + with SQLiteManager() as cur: + cur.execute(cls.all_get_sql) + return int(cur.fetchone()[2]) + + @classmethod + def get_userdatadb_postinit_version(cls) -> int: + """ + Returns the latest userdata post-init database version. + """ + with SQLiteManager(userdata_db=True) as cur: + cur.execute(cls.all_get_sql) + return cur.fetchone()[2] + + # 👇 Setters 👇 + @classmethod + def set_preinit_version(cls, version: int): + """ + Sets the userdata pre-init database version. + """ + with SQLiteManager() as cur: + cur.execute(cls.pre_init_set_sql, (version,)) + + @classmethod + def set_maindb_postinit_version(cls, version: int): + """ + Sets the maindb post-init database version. + """ + with SQLiteManager() as cur: + cur.execute(cls.post_init_set_sql, (version,)) + + @classmethod + def set_userdatadb_postinit_version(cls, version: int): + """ + Sets the userdata post-init database version. + """ + with SQLiteManager(userdata_db=True) as cur: + cur.execute(cls.post_init_set_sql, (version,)) diff --git a/app/db/sqlite/queries.py b/app/db/sqlite/queries.py index ac95a3be..a12fe123 100644 --- a/app/db/sqlite/queries.py +++ b/app/db/sqlite/queries.py @@ -69,3 +69,13 @@ CREATE TABLE IF NOT EXISTS folders ( trackcount integer NOT NULL ); """ + +CREATE_MIGRATIONS_TABLE = """ +CREATE TABLE IF NOT EXISTS migrations ( + id integer PRIMARY KEY, + pre_init_version integer NOT NULL DEFAULT 0, + post_init_version integer NOT NULL DEFAULT 0 +); + +INSERT INTO migrations (pre_init_version, post_init_version) VALUES (0, 0); +""" diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py index cf470dde..d3a1ab32 100644 --- a/app/migrations/__init__.py +++ b/app/migrations/__init__.py @@ -2,19 +2,43 @@ Migrations module. Reads and applies the latest database migrations. + +PLEASE NOTE: OLDER MIGRATIONS CAN NEVER BE DELETED. +ONLY MODIFY OLD MIGRATIONS FOR BUG FIXES OR ENHANCEMENTS ONLY +[TRY NOT TO MODIFY BEHAVIOR, UNLESS YOU KNOW WHAT YOU'RE DOING]. """ + + +from app.db.sqlite.migrations import MigrationManager +from app.logger import log + from .main import main_db_migrations from .userdata import userdata_db_migrations def apply_migrations(): - userdb_version = 0 - maindb_version = 0 + """ + Applies the latest database migrations. + """ + + userdb_version = MigrationManager.get_userdatadb_postinit_version() + maindb_version = MigrationManager.get_maindb_postinit_version() for migration in main_db_migrations: if migration.version > maindb_version: + log.info("Running new MAIN-DB post-init migration: %s", migration.name) migration.migrate() for migration in userdata_db_migrations: if migration.version > userdb_version: + log.info("Running new USERDATA-DB post-init migration: %s", migration.name) migration.migrate() + + +def set_postinit_migration_versions(): + """ + Sets the post-init migration versions. + """ + # TODO: Don't forget to remove the zeros below when you add a valid migration 👇. + MigrationManager.set_maindb_postinit_version(0) + MigrationManager.set_userdatadb_postinit_version(0) diff --git a/app/migrations/_preinit/__init__.py b/app/migrations/_preinit/__init__.py new file mode 100644 index 00000000..6b379386 --- /dev/null +++ b/app/migrations/_preinit/__init__.py @@ -0,0 +1,38 @@ +""" +Pre-init migrations are executed before the database is created. +Useful when you need to move files or folders before the database is created. + +PLEASE NOTE: OLDER MIGRATIONS CAN NEVER BE DELETED. +ONLY MODIFY OLD MIGRATIONS FOR BUG FIXES OR ENHANCEMENTS ONLY. +[TRY NOT TO MODIFY BEHAVIOR, UNLESS YOU KNOW WHAT YOU'RE DOING]. +""" +from sqlite3 import OperationalError + +from app.db.sqlite.migrations import MigrationManager +from app.logger import log + +from .move_to_xdg_folder import MoveToXdgFolder + +all_preinits = [MoveToXdgFolder] + + +def run_preinit_migrations(): + """ + Runs all pre-init migrations. + """ + try: + userdb_version = MigrationManager.get_preinit_version() + except (OperationalError): + userdb_version = 0 + + for migration in all_preinits: + if migration.version > userdb_version: + log.warn("Running new pre-init migration: %s", migration.name) + migration.migrate() + + +def set_preinit_migration_versions(): + """ + Sets the migration versions. + """ + MigrationManager.set_preinit_version(all_preinits[-1].version) diff --git a/app/migrations/_preinit/move_to_xdg_folder.py b/app/migrations/_preinit/move_to_xdg_folder.py new file mode 100644 index 00000000..78c5f0e7 --- /dev/null +++ b/app/migrations/_preinit/move_to_xdg_folder.py @@ -0,0 +1,49 @@ +""" +This migration handles moving the config folder to the XDG standard location. +It also handles moving the userdata and the downloaded artist images to the new location. +""" + + +import os +import shutil +from app.settings import APP_DIR, USER_HOME_DIR +from app.logger import log + + +class MoveToXdgFolder: + version = 1 + name = "MoveToXdgFolder" + + @staticmethod + def migrate(): + old_config_dir = os.path.join(USER_HOME_DIR, ".swing") + new_config_dir = APP_DIR + + if not os.path.exists(old_config_dir): + log.info("No old config folder found. Skipping migration.") + return + + log.info("Found old config folder: %s", old_config_dir) + old_imgs_dir = os.path.join(old_config_dir, "images") + + # move images to new location + if os.path.exists(old_imgs_dir): + shutil.copytree( + old_imgs_dir, + os.path.join(new_config_dir, "images"), + copy_function=shutil.copy2, + dirs_exist_ok=True, + ) + + log.warn("Moved artist images to: %s", new_config_dir) + + # move userdata.db to new location + userdata_db = os.path.join(old_config_dir, "userdata.db") + if os.path.exists(userdata_db): + shutil.copy2(userdata_db, new_config_dir) + + log.warn("Moved userdata.db to: %s", new_config_dir) + log.warn("Migration complete. ✅") + + # swing.db is not moved because the new code fixes bugs which require + # the whole database to be recreated anyway. (ie. the bug which caused duplicate album and artist color entries) diff --git a/app/migrations/main/__init__.py b/app/migrations/main/__init__.py index 2bbd1c0b..d1cbfb57 100644 --- a/app/migrations/main/__init__.py +++ b/app/migrations/main/__init__.py @@ -1,4 +1,10 @@ -from .sample import SampleMigrationModel +""" +Migrations for the main database. -main_db_migrations = [SampleMigrationModel] +PLEASE NOTE: OLDER MIGRATIONS CAN NEVER BE DELETED. +ONLY MODIFY OLD MIGRATIONS FOR BUG FIXES OR ENHANCEMENTS ONLY +[TRY NOT TO MODIFY BEHAVIOR, UNLESS YOU KNOW WHAT YOU'RE DOING]. +""" + +main_db_migrations = [] diff --git a/app/migrations/main/sample.py b/app/migrations/main/sample.py deleted file mode 100644 index ec24e3fa..00000000 --- a/app/migrations/main/sample.py +++ /dev/null @@ -1,6 +0,0 @@ -class SampleMigrationModel: - version = 1 - - @staticmethod - def migrate(): - print("executing sample main db migration") diff --git a/app/migrations/userdata/__init__.py b/app/migrations/userdata/__init__.py index 980d343c..d7ad1765 100644 --- a/app/migrations/userdata/__init__.py +++ b/app/migrations/userdata/__init__.py @@ -1,4 +1,10 @@ -from .sample import SampleMigrationModel +""" +Migrations for the userdata database. -userdata_db_migrations = [SampleMigrationModel] +PLEASE NOTE: OLDER MIGRATIONS CAN NEVER BE DELETED. +ONLY MODIFY OLD MIGRATIONS FOR BUG FIXES OR ENHANCEMENTS ONLY +[TRY NOT TO MODIFY BEHAVIOR, UNLESS YOU KNOW WHAT YOU'RE DOING]. +""" + +userdata_db_migrations = [] diff --git a/app/migrations/userdata/sample.py b/app/migrations/userdata/sample.py deleted file mode 100644 index 5205a662..00000000 --- a/app/migrations/userdata/sample.py +++ /dev/null @@ -1,6 +0,0 @@ -class SampleMigrationModel: - version = 1 - - @staticmethod - def migrate(): - print("executing sample userdata db migration") diff --git a/app/setup/__init__.py b/app/setup/__init__.py index f0c6fad2..1a6eb7de 100644 --- a/app/setup/__init__.py +++ b/app/setup/__init__.py @@ -3,14 +3,19 @@ Contains the functions to prepare the server for use. """ import os import shutil +import time from configparser import ConfigParser from app import settings from app.db.sqlite import create_connection, create_tables, queries from app.db.store import Store +from app.migrations import apply_migrations, set_postinit_migration_versions +from app.migrations._preinit import ( + run_preinit_migrations, + set_preinit_migration_versions, +) from app.settings import APP_DB_PATH, USERDATA_DB_PATH from app.utils import get_home_res_path -from app.migrations import apply_migrations config = ConfigParser() @@ -102,6 +107,7 @@ def setup_sqlite(): """ # if os.path.exists(DB_PATH): # os.remove(DB_PATH) + run_preinit_migrations() app_db_conn = create_connection(APP_DB_PATH) playlist_db_conn = create_connection(USERDATA_DB_PATH) @@ -109,10 +115,15 @@ def setup_sqlite(): create_tables(app_db_conn, queries.CREATE_APPDB_TABLES) create_tables(playlist_db_conn, queries.CREATE_USERDATA_TABLES) + create_tables(app_db_conn, queries.CREATE_MIGRATIONS_TABLE) + create_tables(playlist_db_conn, queries.CREATE_MIGRATIONS_TABLE) + app_db_conn.close() playlist_db_conn.close() apply_migrations() + set_preinit_migration_versions() + set_postinit_migration_versions() Store.load_all_tracks() Store.process_folders()