Add support for restoring plain SQL database dumps. #5871
parent
d7faa85338
commit
7a25da9b06
|
|
@ -27,6 +27,8 @@ restore process:
|
|||
|
||||
* Select *Custom or tar* to restore from a custom archive file to create a
|
||||
copy of the backed-up object.
|
||||
* Select *Plain* to restore a plain SQL backup. When selecting this option
|
||||
all the other options will not be applicable.
|
||||
* Select *Directory* to restore from a compressed directory-format archive.
|
||||
|
||||
* Enter the complete path to the backup file in the *Filename* field.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from './hooks';
|
||||
import { registerView, View } from './registry';
|
||||
import { createFieldControls, listenDepChanges } from './utils';
|
||||
import FormViewTab from './FormViewTab';
|
||||
|
||||
const ErrorMessageBox = () => {
|
||||
const [key, setKey] = useState(0);
|
||||
|
|
@ -181,14 +182,8 @@ export default function FormView({
|
|||
action={(ref) => ref?.updateIndicator()}
|
||||
>{
|
||||
finalGroups.map((tabGroup, idx) =>
|
||||
<Tab
|
||||
key={tabGroup.id}
|
||||
label={tabGroup.label}
|
||||
data-test={tabGroup.id}
|
||||
className={
|
||||
tabGroup.hasError &&
|
||||
tabValue != idx ? 'tab-with-error' : ''
|
||||
}
|
||||
<FormViewTab
|
||||
key={tabGroup.id} tabGroup={tabGroup} idx={idx} tabValue={tabValue}
|
||||
/>
|
||||
)
|
||||
}{hasSQLTab &&
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useFieldOptions, useSchemaStateSubscriber } from './hooks';
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function FormViewTab({tabGroup, idx, tabValue, ...props}) {
|
||||
const accessPath = [tabGroup.id];
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const subscriberManager = useSchemaStateSubscriber(setRefreshKey);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const options = useFieldOptions(accessPath, schemaState, subscriberManager);
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={refreshKey}
|
||||
label={tabGroup.label}
|
||||
data-test={tabGroup.id}
|
||||
iconPosition='start'
|
||||
disabled={options.disabled}
|
||||
className={
|
||||
tabGroup.hasError &&
|
||||
tabValue != idx ? 'tab-with-error' : ''
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
FormViewTab.muiName = Tab.muiName;
|
||||
|
||||
FormViewTab.propTypes = {
|
||||
tabGroup: PropTypes.object,
|
||||
idx: PropTypes.number,
|
||||
tabValue: PropTypes.number,
|
||||
};
|
||||
|
|
@ -788,13 +788,17 @@ function getFinalTheme(baseTheme) {
|
|||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'&.MuiTab-textColorPrimary':{
|
||||
'&:not(.Mui-disabled).MuiTab-textColorPrimary':{
|
||||
color: baseTheme.palette.text.primary,
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
color: baseTheme.otherVars.activeColor,
|
||||
},
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
fontSize: '1rem',
|
||||
marginRight: '2px',
|
||||
},
|
||||
}
|
||||
},
|
||||
MuiBackdrop: {
|
||||
|
|
|
|||
|
|
@ -141,18 +141,18 @@ def _get_create_req_data():
|
|||
data = json.loads(request.data)
|
||||
|
||||
try:
|
||||
_file = filename_with_file_manager_path(data['file'])
|
||||
filepath = filename_with_file_manager_path(data['file'])
|
||||
except Exception as e:
|
||||
return True, internal_server_error(errormsg=str(e)), data, None
|
||||
|
||||
if _file is None:
|
||||
if filepath is None:
|
||||
return True, make_json_response(
|
||||
status=410,
|
||||
success=0,
|
||||
errormsg=_("File could not be found.")
|
||||
), data, _file
|
||||
), data, filepath
|
||||
|
||||
return False, '', data, _file
|
||||
return False, '', data, filepath
|
||||
|
||||
|
||||
def _connect_server(sid):
|
||||
|
|
@ -263,15 +263,15 @@ def set_multiple(key, param, data, args, driver, conn, with_schema=True):
|
|||
return False
|
||||
|
||||
|
||||
def _set_args_param_values(data, manager, server, driver, conn, _file):
|
||||
def get_restore_util_args(data, manager, server, driver, conn, filepath):
|
||||
"""
|
||||
add args to the list.
|
||||
return the args for the command
|
||||
:param data: Data.
|
||||
:param manager: Manager.
|
||||
:param server: Server.
|
||||
:param driver: Driver.
|
||||
:param conn: Connection.
|
||||
:param _file: File.
|
||||
:param filepath: File.
|
||||
:return: args list.
|
||||
"""
|
||||
args = []
|
||||
|
|
@ -347,11 +347,56 @@ def _set_args_param_values(data, manager, server, driver, conn, _file):
|
|||
False)
|
||||
set_multiple('indexes', '--index', data, args, driver, conn, False)
|
||||
|
||||
args.append(fs_short_path(_file))
|
||||
args.append(fs_short_path(filepath))
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def get_sql_util_args(data, manager, server, filepath):
|
||||
"""
|
||||
return the args for the command
|
||||
:param data: Data.
|
||||
:param manager: Manager.
|
||||
:param server: Server.
|
||||
:param filepath: File.
|
||||
:return: args list.
|
||||
"""
|
||||
args = [
|
||||
'--host',
|
||||
manager.local_bind_host if manager.use_ssh_tunnel else server.host,
|
||||
'--port',
|
||||
str(manager.local_bind_port) if manager.use_ssh_tunnel
|
||||
else str(server.port),
|
||||
'--username', server.username, '--dbname',
|
||||
data['database'],
|
||||
'--file', fs_short_path(filepath)
|
||||
]
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def use_restore_utility(data, manager, server, driver, conn, filepath):
|
||||
utility = manager.utility('restore')
|
||||
ret_val = does_utility_exist(utility)
|
||||
if ret_val:
|
||||
return ret_val, None, None
|
||||
|
||||
args = get_restore_util_args(data, manager, server, driver, conn, filepath)
|
||||
|
||||
return None, utility, args
|
||||
|
||||
|
||||
def use_sql_utility(data, manager, server, filepath):
|
||||
utility = manager.utility('sql')
|
||||
ret_val = does_utility_exist(utility)
|
||||
if ret_val:
|
||||
return ret_val, None, None
|
||||
|
||||
args = get_sql_util_args(data, manager, server, filepath)
|
||||
|
||||
return None, utility, args
|
||||
|
||||
|
||||
@blueprint.route('/job/<int:sid>', methods=['POST'], endpoint='create_job')
|
||||
@pga_login_required
|
||||
def create_restore_job(sid):
|
||||
|
|
@ -364,7 +409,7 @@ def create_restore_job(sid):
|
|||
Returns:
|
||||
None
|
||||
"""
|
||||
is_error, errmsg, data, _file = _get_create_req_data()
|
||||
is_error, errmsg, data, filepath = _get_create_req_data()
|
||||
if is_error:
|
||||
return errmsg
|
||||
|
||||
|
|
@ -372,16 +417,19 @@ def create_restore_job(sid):
|
|||
if is_error:
|
||||
return errmsg
|
||||
|
||||
utility = manager.utility('restore')
|
||||
ret_val = does_utility_exist(utility)
|
||||
if ret_val:
|
||||
if data['format'] == 'plain':
|
||||
error_msg, utility, args = use_sql_utility(
|
||||
data, manager, server, filepath)
|
||||
else:
|
||||
error_msg, utility, args = use_restore_utility(
|
||||
data, manager, server, driver, conn, filepath)
|
||||
|
||||
if error_msg is not None:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
errormsg=ret_val
|
||||
errormsg=error_msg
|
||||
)
|
||||
|
||||
args = _set_args_param_values(data, manager, server, driver, conn, _file)
|
||||
|
||||
try:
|
||||
p = BatchProcess(
|
||||
desc=RestoreMessage(
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ define('tools.restore', [
|
|||
()=>getRestoreDisableOptionSchema({nodeInfo: treeNodeInfo}),
|
||||
()=>getRestoreMiscellaneousSchema({nodeInfo: treeNodeInfo}),
|
||||
{
|
||||
role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData)
|
||||
role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData),
|
||||
nodeType: itemNodeData._type,
|
||||
},
|
||||
treeNodeInfo,
|
||||
pgBrowser
|
||||
|
|
|
|||
|
|
@ -343,12 +343,37 @@ export default class RestoreSchema extends BaseUISchema {
|
|||
this.getRestoreMiscellaneousSchema = restoreMiscellaneousSchema;
|
||||
this.treeNodeInfo = treeNodeInfo;
|
||||
this.pgBrowser = pgBrowser;
|
||||
|
||||
this.formatOptions = this.getFormatOptions();
|
||||
}
|
||||
|
||||
get idAttribute() {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
isPlainFormat(state) {
|
||||
return state.format == 'plain';
|
||||
}
|
||||
|
||||
getFormatOptions() {
|
||||
const options = [{
|
||||
label: gettext('Custom or tar'),
|
||||
value: 'custom',
|
||||
}, {
|
||||
label: gettext('Directory'),
|
||||
value: 'directory',
|
||||
}];
|
||||
|
||||
if(this.fieldOptions.nodeType == 'database') {
|
||||
options.splice(1, 0, {
|
||||
label: gettext('Plain'),
|
||||
value: 'plain',
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let obj = this;
|
||||
return [{
|
||||
|
|
@ -356,14 +381,15 @@ export default class RestoreSchema extends BaseUISchema {
|
|||
label: gettext('Format'),
|
||||
disabled: false,type: 'select',
|
||||
controlProps: { allowClear: false, width: '100%' },
|
||||
options: [{
|
||||
label: gettext('Custom or tar'),
|
||||
value: 'custom',
|
||||
},
|
||||
{
|
||||
label: gettext('Directory'),
|
||||
value: 'directory',
|
||||
}],
|
||||
options: this.formatOptions,
|
||||
depChange: (state) => {
|
||||
if(state.format == 'plain') {
|
||||
return {
|
||||
no_of_jobs: undefined,
|
||||
role: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
}, {
|
||||
id: 'file',
|
||||
label: gettext('Filename'),
|
||||
|
|
@ -378,11 +404,12 @@ export default class RestoreSchema extends BaseUISchema {
|
|||
};
|
||||
},
|
||||
disabled: false,
|
||||
deps: ['format']
|
||||
deps: ['format'],
|
||||
}, {
|
||||
id: 'no_of_jobs',
|
||||
label: gettext('Number of jobs'),
|
||||
type: 'int',
|
||||
disabled: obj.isPlainFormat,
|
||||
}, {
|
||||
id: 'role',
|
||||
label: gettext('Role name'),
|
||||
|
|
@ -392,35 +419,45 @@ export default class RestoreSchema extends BaseUISchema {
|
|||
controlProps: {
|
||||
allowClear: false,
|
||||
},
|
||||
disabled: obj.isPlainFormat,
|
||||
}, {
|
||||
id: 'data-options', type: 'group', label: gettext('Data Options'),
|
||||
disabled: function(state) {
|
||||
return obj.isPlainFormat(state);
|
||||
}, deps: ['format']
|
||||
}, {
|
||||
type: 'nested-fieldset',
|
||||
label: gettext('Sections'),
|
||||
group: gettext('Data Options'),
|
||||
group: 'data-options',
|
||||
schema:obj.getSectionSchema(),
|
||||
visible: true
|
||||
visible: true,
|
||||
}, {
|
||||
type: 'nested-fieldset',
|
||||
label: gettext('Type of objects'),
|
||||
group: gettext('Data Options'),
|
||||
group: 'data-options',
|
||||
schema:obj.getRestoreTypeObjSchema(),
|
||||
visible: true
|
||||
visible: true,
|
||||
}, {
|
||||
type: 'nested-fieldset',
|
||||
label: gettext('Do not save'),
|
||||
group: gettext('Data Options'),
|
||||
group: 'data-options',
|
||||
schema:obj.getRestoreSaveOptSchema(),
|
||||
visible: true
|
||||
visible: true,
|
||||
}, {
|
||||
id: 'query-options', type: 'group', label: gettext('Query Options'),
|
||||
disabled: function(state) {
|
||||
return obj.isPlainFormat(state);
|
||||
}, deps: ['format']
|
||||
}, {
|
||||
id: 'include_create_database',
|
||||
label: gettext('Include CREATE DATABASE statement'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Query Options')
|
||||
group: 'query-options'
|
||||
}, {
|
||||
id: 'clean',
|
||||
label: gettext('Clean before restore'),
|
||||
type: 'switch',
|
||||
group: gettext('Query Options'),
|
||||
group: 'query-options',
|
||||
inlineGroup: 'clean',
|
||||
disabled: function(state) {
|
||||
if(obj.selectedNodeType === 'function' || obj.selectedNodeType === 'trigger_function') {
|
||||
|
|
@ -432,7 +469,7 @@ export default class RestoreSchema extends BaseUISchema {
|
|||
id: 'if_exists',
|
||||
label: gettext('Include IF EXISTS clause'),
|
||||
type: 'switch',
|
||||
group: gettext('Query Options'),
|
||||
group: 'query-options',
|
||||
inlineGroup: 'clean',
|
||||
deps: ['clean'],
|
||||
disabled: function(state) {
|
||||
|
|
@ -447,29 +484,39 @@ export default class RestoreSchema extends BaseUISchema {
|
|||
label: gettext('Single transaction'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Query Options'),
|
||||
group: 'query-options',
|
||||
}, {
|
||||
id: 'table-options', type: 'group', label: gettext('Table Options'),
|
||||
disabled: function(state) {
|
||||
return obj.isPlainFormat(state);
|
||||
}, deps: ['format']
|
||||
}, {
|
||||
id: 'enable_row_security',
|
||||
label: gettext('Enable row security'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Table Options'),
|
||||
group: 'table-options',
|
||||
}, {
|
||||
id: 'no_data_fail_table',
|
||||
label: gettext('No data for failed tables'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Table Options'),
|
||||
group: 'table-options',
|
||||
}, {
|
||||
id: 'options', type: 'group', label: gettext('Options'),
|
||||
disabled: function(state) {
|
||||
return obj.isPlainFormat(state);
|
||||
}, deps: ['format']
|
||||
}, {
|
||||
type: 'nested-fieldset',
|
||||
label: gettext('Disable'),
|
||||
group: gettext('Options'),
|
||||
group: 'options',
|
||||
schema:obj.getRestoreDisableOptionSchema(),
|
||||
visible: true
|
||||
}, {
|
||||
type: 'nested-fieldset',
|
||||
label: gettext('Miscellaneous / Behavior'),
|
||||
group: gettext('Options'),
|
||||
group: 'options',
|
||||
schema:obj.getRestoreMiscellaneousSchema(),
|
||||
visible: true
|
||||
}];
|
||||
|
|
|
|||
|
|
@ -24,6 +24,28 @@ RESTORE_JOB_URL = '/restore/job/{0}'
|
|||
class RestoreCreateJobTest(BaseTestGenerator):
|
||||
"""Test the RestoreCreateJob class"""
|
||||
scenarios = [
|
||||
('When restore object with plain format',
|
||||
dict(
|
||||
class_params=dict(
|
||||
sid=1,
|
||||
name='test_restore_server',
|
||||
port=5444,
|
||||
host='localhost',
|
||||
database='postgres',
|
||||
bfile='test_restore',
|
||||
username='postgres'
|
||||
),
|
||||
params=dict(
|
||||
file='test_restore_file',
|
||||
format='plain',
|
||||
database='postgres'
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='psql',
|
||||
expected_cmd_opts=['--file'],
|
||||
not_expected_cmd_opts=[],
|
||||
expected_exit_code=[0, None]
|
||||
)),
|
||||
('When restore object with default options',
|
||||
dict(
|
||||
class_params=dict(
|
||||
|
|
@ -46,6 +68,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
database='postgres'
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--verbose'],
|
||||
not_expected_cmd_opts=[],
|
||||
expected_exit_code=[0, None]
|
||||
|
|
@ -72,6 +95,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
database='postgres'
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--verbose', '--format=d'],
|
||||
not_expected_cmd_opts=[],
|
||||
expected_exit_code=[0, None]
|
||||
|
|
@ -103,6 +127,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
only_schema=True
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--verbose', '--jobs', '2',
|
||||
'--section=pre-data', '--section=data',
|
||||
'--section=post-data'],
|
||||
|
|
@ -136,6 +161,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
dns_owner=True
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--verbose', '--data-only'],
|
||||
not_expected_cmd_opts=[],
|
||||
# Below options should be enabled once we fix the issue #3368
|
||||
|
|
@ -167,6 +193,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
only_data=False
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--no-owner',
|
||||
'--no-tablespaces',
|
||||
'--no-privileges'],
|
||||
|
|
@ -200,6 +227,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
only_data=False
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--no-comments', '--no-publications',
|
||||
'--no-subscriptions', '--no-security-labels'],
|
||||
not_expected_cmd_opts=[],
|
||||
|
|
@ -231,6 +259,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
dns_table_access_method=True
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--no-table-access-method'],
|
||||
not_expected_cmd_opts=[],
|
||||
expected_exit_code=[0, None],
|
||||
|
|
@ -262,6 +291,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
if_exists=True,
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--create', '--clean',
|
||||
'--single-transaction', '--if-exists'],
|
||||
not_expected_cmd_opts=[],
|
||||
|
|
@ -291,6 +321,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
only_schema=False
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--enable-row-security',
|
||||
'--no-data-for-failed-tables'],
|
||||
not_expected_cmd_opts=[],
|
||||
|
|
@ -318,6 +349,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
only_schema=False
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--disable-triggers'],
|
||||
not_expected_cmd_opts=[],
|
||||
expected_exit_code=[0, None]
|
||||
|
|
@ -344,6 +376,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
exit_on_error=True,
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--exit-on-error',
|
||||
'--use-set-session-authorization'],
|
||||
not_expected_cmd_opts=[],
|
||||
|
|
@ -370,6 +403,7 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
exclude_schema="sch*"
|
||||
),
|
||||
url=RESTORE_JOB_URL,
|
||||
expected_cmd='pg_restore',
|
||||
expected_cmd_opts=['--exclude-schema', 'sch*'],
|
||||
not_expected_cmd_opts=[],
|
||||
expected_exit_code=[0, None]
|
||||
|
|
@ -459,6 +493,11 @@ class RestoreCreateJobTest(BaseTestGenerator):
|
|||
self.assertTrue(restore_message_mock.called)
|
||||
self.assertTrue(batch_process_mock.called)
|
||||
|
||||
if self.expected_cmd:
|
||||
self.assertIn(
|
||||
self.expected_cmd,
|
||||
batch_process_mock.call_args_list[0][1]['cmd']
|
||||
)
|
||||
if self.expected_cmd_opts:
|
||||
for opt in self.expected_cmd_opts:
|
||||
self.assertIn(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ describe('RestoreSchema', ()=>{
|
|||
{
|
||||
role: ()=>[],
|
||||
encoding: ()=>[],
|
||||
nodeType: '',
|
||||
},
|
||||
{server: {version: 11000}},
|
||||
pgAdmin.pgBrowser
|
||||
|
|
|
|||
Loading…
Reference in New Issue