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 #6460 <https://redmine.postgresql.org/issues/6460>`_ - Added a mechanism to detect a corrupt/broken config database file.
Bug fixes
*********

View File

@ -14,6 +14,8 @@ import os
import sys
import re
import ipaddress
import traceback
from types import MethodType
from collections import defaultdict
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.session import create_session_interface, pga_unauthorised
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
from datetime import timedelta
from pgadmin.setup import get_version, set_version
from datetime import timedelta, datetime
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.csrf import pgCSRFProtect
from pgadmin import authenticate
@ -349,6 +351,44 @@ def create_app(app_name=None):
##########################################################################
# 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():
# Run migration for the first time i.e. create database
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
# 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
# the exception
if not cli_mode:
db_upgrade(app)
upgrade_db()
else:
if not os.path.exists(SQLITE_PATH):
raise FileNotFoundError(
'SQLite database file "' + SQLITE_PATH +
'" does not exists.')
raise RuntimeError('Specified SQLite database file '
'is not valid.')
raise RuntimeError(
'The configuration database file is not valid.')
else:
schema_version = get_version()
# Run migration if current schema version is greater than the
# schema version stored in version table
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
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.
"""
return [BROWSER_INDEX, 'browser.nodes',
'browser.check_corrupted_db_file',
'browser.check_master_password',
'browser.set_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",
methods=["GET"])
def check_master_password():

View File

@ -594,7 +594,7 @@ define('pgadmin.browser', [
}, 300000);
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:update', obj.onUpdateTreeNode, 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.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() {
let self = this;
// 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_upgrade import db_upgrade
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:
return -1
return version.value
if version:
return version.value
else:
return -1
def set_version(new_version):