Added a mechanism to detect a corrupt/broken config database file. Fixes #6460

This commit is contained in:
Nikhil Mohite 2021-06-08 19:41:47 +05:30 committed by Akshay Joshi
parent 93ddc4a5ba
commit 7c88ee7cff
7 changed files with 137 additions and 9 deletions

View File

@ -21,6 +21,7 @@ Housekeeping
************ ************
| `Issue #6225 <https://redmine.postgresql.org/issues/6225>`_ - Updated Flask-Security-Too to the latest v4. | `Issue #6225 <https://redmine.postgresql.org/issues/6225>`_ - Updated Flask-Security-Too to the latest v4.
| `Issue #6460 <https://redmine.postgresql.org/issues/6460>`_ - Added a mechanism to detect a corrupt/broken config database file.
Bug fixes Bug fixes
********* *********

View File

@ -14,6 +14,8 @@ import os
import sys import sys
import re import re
import ipaddress import ipaddress
import traceback
from types import MethodType from types import MethodType
from collections import defaultdict from collections import defaultdict
from importlib import import_module from importlib import import_module
@ -38,8 +40,8 @@ from pgadmin.utils import PgAdminModule, driver, KeyManager
from pgadmin.utils.preferences import Preferences from pgadmin.utils.preferences import Preferences
from pgadmin.utils.session import create_session_interface, pga_unauthorised from pgadmin.utils.session import create_session_interface, pga_unauthorised
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
from datetime import timedelta from datetime import timedelta, datetime
from pgadmin.setup import get_version, set_version from pgadmin.setup import get_version, set_version, check_db_tables
from pgadmin.utils.ajax import internal_server_error, make_json_response from pgadmin.utils.ajax import internal_server_error, make_json_response
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin import authenticate from pgadmin import authenticate
@ -349,6 +351,44 @@ def create_app(app_name=None):
########################################################################## ##########################################################################
# Upgrade the schema (if required) # Upgrade the schema (if required)
########################################################################## ##########################################################################
def backup_db_file():
"""
Create a backup of the current database file
and create new database file with default settings.
"""
backup_file_name = "{0}.{1}".format(
SQLITE_PATH, datetime.now().strftime('%Y%m%d%H%M%S'))
os.rename(SQLITE_PATH, backup_file_name)
app.logger.error('Exception in database migration.')
app.logger.info('Creating new database file.')
try:
db_upgrade(app)
os.environ[
'CORRUPTED_DB_BACKUP_FILE'] = backup_file_name
app.logger.info('Database migration completed.')
except Exception as e:
app.logger.error('Database migration failed')
app.logger.error(traceback.format_exc())
raise RuntimeError('Migration failed')
def upgrade_db():
"""
Execute the migrations.
"""
try:
db_upgrade(app)
os.environ['CORRUPTED_DB_BACKUP_FILE'] = ''
except Exception as e:
backup_db_file()
# check all tables are present in the db.
is_db_error, invalid_tb_names = check_db_tables()
if is_db_error:
app.logger.error(
'Table(s) {0} are missing in the'
' database'.format(invalid_tb_names))
backup_db_file()
with app.app_context(): with app.app_context():
# Run migration for the first time i.e. create database # Run migration for the first time i.e. create database
from config import SQLITE_PATH from config import SQLITE_PATH
@ -356,25 +396,34 @@ def create_app(app_name=None):
# If version not available, user must have aborted. Tables are not # If version not available, user must have aborted. Tables are not
# created and so its an empty db # created and so its an empty db
if not os.path.exists(SQLITE_PATH) or get_version() == -1: version = get_version()
if not os.path.exists(SQLITE_PATH) or version == -1:
# If running in cli mode then don't try to upgrade, just raise # If running in cli mode then don't try to upgrade, just raise
# the exception # the exception
if not cli_mode: if not cli_mode:
db_upgrade(app) upgrade_db()
else: else:
if not os.path.exists(SQLITE_PATH): if not os.path.exists(SQLITE_PATH):
raise FileNotFoundError( raise FileNotFoundError(
'SQLite database file "' + SQLITE_PATH + 'SQLite database file "' + SQLITE_PATH +
'" does not exists.') '" does not exists.')
raise RuntimeError('Specified SQLite database file ' raise RuntimeError(
'is not valid.') 'The configuration database file is not valid.')
else: else:
schema_version = get_version() schema_version = get_version()
# Run migration if current schema version is greater than the # Run migration if current schema version is greater than the
# schema version stored in version table # schema version stored in version table
if CURRENT_SCHEMA_VERSION >= schema_version: if CURRENT_SCHEMA_VERSION >= schema_version:
db_upgrade(app) upgrade_db()
else:
# check all tables are present in the db.
is_db_error, invalid_tb_names = check_db_tables()
if is_db_error:
app.logger.error(
'Table(s) {0} are missing in the'
' database'.format(invalid_tb_names))
backup_db_file()
# Update schema version to the latest # Update schema version to the latest
if CURRENT_SCHEMA_VERSION > schema_version: if CURRENT_SCHEMA_VERSION > schema_version:

View File

@ -342,6 +342,7 @@ class BrowserModule(PgAdminModule):
list: a list of url endpoints exposed to the client. list: a list of url endpoints exposed to the client.
""" """
return [BROWSER_INDEX, 'browser.nodes', return [BROWSER_INDEX, 'browser.nodes',
'browser.check_corrupted_db_file',
'browser.check_master_password', 'browser.check_master_password',
'browser.set_master_password', 'browser.set_master_password',
'browser.reset_master_password', 'browser.reset_master_password',
@ -951,6 +952,19 @@ def form_master_password_response(existing=True, present=False, errmsg=None):
}) })
@blueprint.route("/check_corrupted_db_file",
endpoint="check_corrupted_db_file", methods=["GET"])
def check_corrupted_db_file():
"""
Get the corrupted database file path.
"""
file_location = os.environ['CORRUPTED_DB_BACKUP_FILE'] \
if 'CORRUPTED_DB_BACKUP_FILE' in os.environ else ''
# reset the corrupted db file path in env.
os.environ['CORRUPTED_DB_BACKUP_FILE'] = ''
return make_json_response(data=file_location)
@blueprint.route("/master_password", endpoint="check_master_password", @blueprint.route("/master_password", endpoint="check_master_password",
methods=["GET"]) methods=["GET"])
def check_master_password(): def check_master_password():

View File

@ -594,7 +594,7 @@ define('pgadmin.browser', [
}, 300000); }, 300000);
obj.set_master_password(''); obj.set_master_password('');
obj.check_corrupted_db_file();
obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode, obj); obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode, obj);
obj.Events.on('pgadmin:browser:tree:update', obj.onUpdateTreeNode, obj); obj.Events.on('pgadmin:browser:tree:update', obj.onUpdateTreeNode, obj);
obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode, obj); obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode, obj);
@ -607,7 +607,35 @@ define('pgadmin.browser', [
obj.register_to_activity_listener(document); obj.register_to_activity_listener(document);
obj.start_inactivity_timeout_daemon(); obj.start_inactivity_timeout_daemon();
}, },
check_corrupted_db_file: function() {
$.ajax({
url: url_for('browser.check_corrupted_db_file'),
type: 'GET',
dataType: 'json',
contentType: 'application/json',
}).done((res)=> {
if(res.data.length > 0) {
Alertify.alert(
'Warning',
'pgAdmin detected unrecoverable corruption in it\'s SQLite configuration database. ' +
'The database has been backed up and recreated with default settings. '+
'It may be possible to recover data such as query history manually from '+
'the original/corrupt file using a tool such as DB Browser for SQLite if desired.'+
'<br><br>Original file: ' + res.data + '<br>Replacement file: ' +
res.data.substring(0, res.data.length - 14)
)
.set({'closable': true,
'onok': function() {
},
});
}
}).fail(function(xhr, status, error) {
Alertify.alert(error);
});
},
init_master_password: function() { init_master_password: function() {
let self = this; let self = this;
// Master password dialog // Master password dialog

View File

@ -11,3 +11,4 @@ from .user_info import user_info
from .db_version import get_version, set_version from .db_version import get_version, set_version
from .db_upgrade import db_upgrade from .db_upgrade import db_upgrade
from .data_directory import create_app_data_directory from .data_directory import create_app_data_directory
from .db_table_check import check_db_tables

View File

@ -0,0 +1,32 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2021, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from pgadmin.model import Version
from pgadmin.model import db
def get_db_table_names():
db_table_names = db.metadata.tables.keys() if db.metadata.tables else 0
return db_table_names
def check_db_tables():
is_error = False
invalid_tb_names = list()
db_table_names = get_db_table_names()
# check table is actually present in the db.
for table_name in db_table_names:
if not db.engine.dialect.has_table(db.engine, table_name):
invalid_tb_names.append(table_name)
is_error = True
if is_error:
return True, invalid_tb_names
else:
return False, invalid_tb_names

View File

@ -16,7 +16,10 @@ def get_version():
except Exception: except Exception:
return -1 return -1
if version:
return version.value return version.value
else:
return -1
def set_version(new_version): def set_version(new_version):