From b02f5dac5b19e1b0e5e2cfe9eb54738e8818cabc Mon Sep 17 00:00:00 2001 From: azivner Date: Mon, 9 Oct 2017 16:50:36 -0400 Subject: [PATCH] basic implementation of DB upgrades --- src/app.py | 12 ++++++ src/backup.py | 28 ++++++++------ src/migration_api.py | 72 ++++++++++++++++++++++++++++++++++++ src/sql.py | 5 +++ src/templates/app.html | 4 +- src/templates/migration.html | 57 ++++++++++++++++++++++++++++ src/tree_api.py | 2 +- static/js/migration.js | 43 +++++++++++++++++++++ 8 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 src/migration_api.py create mode 100644 src/templates/migration.html create mode 100644 static/js/migration.js diff --git a/src/app.py b/src/app.py index b3d430990..391a0231e 100644 --- a/src/app.py +++ b/src/app.py @@ -14,6 +14,7 @@ from password_api import password_api from settings_api import settings_api from notes_history_api import notes_history_api from audit_api import audit_api +from migration_api import migration_api, APP_DB_VERSION import config_provider import my_scrypt @@ -37,6 +38,7 @@ app.register_blueprint(password_api) app.register_blueprint(settings_api) app.register_blueprint(notes_history_api) app.register_blueprint(audit_api) +app.register_blueprint(migration_api) class User(UserMixin): pass @@ -48,8 +50,18 @@ def login_form(): @app.route('/app', methods=['GET']) @login_required def show_app(): + db_version = int(getOption('db_version')) + + if db_version != APP_DB_VERSION: + return redirect('migration') + return render_template('app.html') +@app.route('/migration', methods=['GET']) +@login_required +def show_migration(): + return render_template('migration.html') + @app.route('/logout', methods=['POST']) @login_required def logout(): diff --git a/src/backup.py b/src/backup.py index 4d7c8baab..14431e1b8 100644 --- a/src/backup.py +++ b/src/backup.py @@ -7,25 +7,29 @@ from shutil import copyfile import os import re -def backup(): +def regular_backup(): now = utils.nowTimestamp() last_backup_date = int(getOption('last_backup_date')) if now - last_backup_date > 43200: - config = config_provider.getConfig() - - document_path = config['Document']['documentPath'] - backup_directory = config['Backup']['backupDirectory'] - - date_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M") - - copyfile(document_path, backup_directory + "/" + "backup-" + date_str + ".db") - - setOption('last_backup_date', now) - commit() + backup_now() cleanup_old_backups() +def backup_now(): + now = utils.nowTimestamp() + + config = config_provider.getConfig() + + document_path = config['Document']['documentPath'] + backup_directory = config['Backup']['backupDirectory'] + + date_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M") + + copyfile(document_path, backup_directory + "/" + "backup-" + date_str + ".db") + + setOption('last_backup_date', now) + commit() def cleanup_old_backups(): now = datetime.utcnow() diff --git a/src/migration_api.py b/src/migration_api.py new file mode 100644 index 000000000..ae2a789c4 --- /dev/null +++ b/src/migration_api.py @@ -0,0 +1,72 @@ +import os +import re + +import traceback + +from flask import Blueprint, jsonify +from flask_login import login_required + +from sql import getOption, setOption, commit, execute_script + +import backup + +APP_DB_VERSION = 0 + +MIGRATIONS_DIR = "src/migrations" + +migration_api = Blueprint('migration_api', __name__) + +@migration_api.route('/api/migration', methods = ['GET']) +@login_required +def getMigrationInfo(): + return jsonify({ + 'db_version': int(getOption('db_version')), + 'app_db_version': APP_DB_VERSION + }) + +@migration_api.route('/api/migration', methods = ['POST']) +@login_required +def runMigration(): + migrations = [] + + backup.backup_now() + + current_db_version = int(getOption('db_version')) + + for file in os.listdir(MIGRATIONS_DIR): + match = re.search(r"([0-9]{4})__([a-zA-Z0-9_ ]+)\.sql", file) + + if match: + db_version = int(match.group(1)) + + if db_version > current_db_version: + name = match.group(2) + + migration_record = { + 'db_version': db_version, + 'name': name + } + + migrations.append(migration_record) + + with open(MIGRATIONS_DIR + "/" + file, 'r') as sql_file: + sql = sql_file.read() + + try: + execute_script(sql) + + setOption('db_version', db_version) + commit() + + migration_record['success'] = True + except: + migration_record['success'] = False + migration_record['error'] = traceback.format_exc() + + break + + migrations.sort(key=lambda x: x['db_version']) + + return jsonify({ + 'migrations': migrations + }) diff --git a/src/sql.py b/src/sql.py index c067f5e35..5ae80b96f 100644 --- a/src/sql.py +++ b/src/sql.py @@ -64,6 +64,11 @@ def execute(sql, params=[]): cursor.execute(sql, params) return cursor +def execute_script(sql): + cursor = conn.cursor() + cursor.executescript(sql) + return cursor + def getResults(sql, params=[]): cursor = conn.cursor() query = cursor.execute(sql, params) diff --git a/src/templates/app.html b/src/templates/app.html index aaaa4ce65..5e4b863a2 100644 --- a/src/templates/app.html +++ b/src/templates/app.html @@ -98,9 +98,9 @@

- +   - +

diff --git a/src/templates/migration.html b/src/templates/migration.html new file mode 100644 index 000000000..74e5e887c --- /dev/null +++ b/src/templates/migration.html @@ -0,0 +1,57 @@ + + + + + Migration + + +
+

Migration

+ + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/src/tree_api.py b/src/tree_api.py index 6368b9e2b..ff85b19d8 100644 --- a/src/tree_api.py +++ b/src/tree_api.py @@ -13,7 +13,7 @@ tree_api = Blueprint('tree_api', __name__) @tree_api.route('/api/tree', methods = ['GET']) @login_required def getTree(): - backup.backup() + backup.regular_backup() notes = getResults("select " "notes_tree.*, " diff --git a/static/js/migration.js b/static/js/migration.js new file mode 100644 index 000000000..8acf17910 --- /dev/null +++ b/static/js/migration.js @@ -0,0 +1,43 @@ +$(document).ready(() => { + $.get(baseApiUrl + 'migration').then(result => { + const appDbVersion = result.app_db_version; + const dbVersion = result.db_version; + + if (appDbVersion === dbVersion) { + $("#up-to-date").show(); + } + else { + $("#need-to-migrate").show(); + + $("#app-db-version").html(appDbVersion); + $("#db-version").html(dbVersion); + } + }); +}); + +$("#run-migration").click(() => { + $("#run-migration").prop("disabled", true); + + $("#migration-result").show(); + + $.ajax({ + url: baseApiUrl + 'migration', + type: 'POST', + success: result => { + for (const migration of result.migrations) { + const row = $('') + .append($('').html(migration.db_version)) + .append($('').html(migration.name)) + .append($('').html(migration.success ? 'Yes' : 'No')) + .append($('').html(migration.success ? 'N/A' : migration.error)); + + if (!migration.success) { + row.addClass("danger"); + } + + $("#migration-table").append(row); + } + }, + error: () => alert("Migration failed with unknown error") + }); +}); \ No newline at end of file