mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Added support for restoring a tar/custom type backup file in a object.
Tweaked by Ashesh Vashi as below: - Integrated it with the background process executor, and observer. - Improved the message format of the backup module messages. - Created an item in TODO list to list down the objects in the selected backup file.
This commit is contained in:
parent
8ca760ee2b
commit
2da3a710a1
6
TODO.txt
6
TODO.txt
@ -31,3 +31,9 @@ Backup Object
|
|||||||
-------------
|
-------------
|
||||||
|
|
||||||
Allow to select/deselect objects under the object backup operation.
|
Allow to select/deselect objects under the object backup operation.
|
||||||
|
|
||||||
|
Restore Object
|
||||||
|
-------------
|
||||||
|
|
||||||
|
List down the objects within the backup file, and allow the user to
|
||||||
|
select/deselect the only objects, which user may want to restore.
|
||||||
|
@ -9,17 +9,20 @@
|
|||||||
|
|
||||||
"""Implements Backup Utility"""
|
"""Implements Backup Utility"""
|
||||||
|
|
||||||
|
import cgi
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import render_template, request, current_app, \
|
from flask import render_template, request, current_app, \
|
||||||
url_for, Response
|
url_for, Response
|
||||||
from flask.ext.babel import gettext as _
|
from flask.ext.babel import gettext as _
|
||||||
from pgadmin.utils.ajax import make_json_response, bad_request
|
|
||||||
from pgadmin.utils import PgAdminModule, get_storage_directory
|
|
||||||
from flask.ext.security import login_required, current_user
|
from flask.ext.security import login_required, current_user
|
||||||
from pgadmin.model import Server
|
|
||||||
from config import PG_DEFAULT_DRIVER
|
from config import PG_DEFAULT_DRIVER
|
||||||
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
|
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
|
||||||
|
from pgadmin.model import Server
|
||||||
|
from pgadmin.utils.ajax import make_json_response, bad_request
|
||||||
|
from pgadmin.utils import PgAdminModule, get_storage_directory
|
||||||
|
|
||||||
|
|
||||||
# set template path for sql scripts
|
# set template path for sql scripts
|
||||||
@ -77,9 +80,10 @@ class BackupMessage(IProcessDesc):
|
|||||||
|
|
||||||
Defines the message shown for the backup operation.
|
Defines the message shown for the backup operation.
|
||||||
"""
|
"""
|
||||||
def __init__(self, _type, _sid, **kwargs):
|
def __init__(self, _type, _sid, _bfile, **kwargs):
|
||||||
self.backup_type = _type
|
self.backup_type = _type
|
||||||
self.sid = _sid
|
self.sid = _sid
|
||||||
|
self.bfile = _bfile
|
||||||
self.database = None
|
self.database = None
|
||||||
|
|
||||||
if 'database' in kwargs:
|
if 'database' in kwargs:
|
||||||
@ -120,28 +124,36 @@ class BackupMessage(IProcessDesc):
|
|||||||
res = '<div class="h5">'
|
res = '<div class="h5">'
|
||||||
|
|
||||||
if self.backup_type == BACKUP.OBJECT:
|
if self.backup_type == BACKUP.OBJECT:
|
||||||
res += _(
|
res += cgi.escape(
|
||||||
"Backing up an object on the server - '{0}' on database '{1}'"
|
_(
|
||||||
).format(
|
"Backing up an object on the server - '{0}' on database '{1}'"
|
||||||
"{0} ({1}:{2})".format(s.name, s.host, s.port),
|
).format(
|
||||||
self.database
|
"{0} ({1}:{2})".format(s.name, s.host, s.port),
|
||||||
|
self.database
|
||||||
|
)
|
||||||
).encode('ascii', 'xmlcharrefreplace')
|
).encode('ascii', 'xmlcharrefreplace')
|
||||||
if self.backup_type == BACKUP.GLOBALS:
|
if self.backup_type == BACKUP.GLOBALS:
|
||||||
res += _("Backing up the globals for the server - '{0}'!").format(
|
res += cgi.escape(
|
||||||
"{0} ({1}:{2})".format(s.name, s.host, s.port)
|
_("Backing up the globals for the server - '{0}'").format(
|
||||||
|
"{0} ({1}:{2})".format(s.name, s.host, s.port)
|
||||||
|
)
|
||||||
).encode('ascii', 'xmlcharrefreplace')
|
).encode('ascii', 'xmlcharrefreplace')
|
||||||
elif self.backup_type == BACKUP.SERVER:
|
elif self.backup_type == BACKUP.SERVER:
|
||||||
res += _("Backing up the server - '{0}'!").format(
|
res += cgi.escape(
|
||||||
"{0} ({1}:{2})".format(s.name, s.host, s.port)
|
_("Backing up the server - '{0}'").format(
|
||||||
|
"{0} ({1}:{2})".format(s.name, s.host, s.port)
|
||||||
|
)
|
||||||
).encode('ascii', 'xmlcharrefreplace')
|
).encode('ascii', 'xmlcharrefreplace')
|
||||||
else:
|
else:
|
||||||
# It should never reach here.
|
# It should never reach here.
|
||||||
res += "Backup"
|
res += "Backup"
|
||||||
|
|
||||||
res += '</div><div class="h5">'
|
res += '</div><div class="h5">'
|
||||||
res += _("Running command:").encode('ascii', 'xmlcharrefreplace')
|
res += cgi.escape(
|
||||||
res += '<br>'
|
_("Running command:")
|
||||||
res += cmd.encode('ascii', 'xmlcharrefreplace')
|
).encode('ascii', 'xmlcharrefreplace')
|
||||||
|
res += '</b><br><i>'
|
||||||
|
res += cgi.escape(cmd).encode('ascii', 'xmlcharrefreplace')
|
||||||
|
|
||||||
replace_next = False
|
replace_next = False
|
||||||
|
|
||||||
@ -151,7 +163,9 @@ class BackupMessage(IProcessDesc):
|
|||||||
x = x.replace('"', '\\"')
|
x = x.replace('"', '\\"')
|
||||||
x = x.replace('""', '\\"')
|
x = x.replace('""', '\\"')
|
||||||
|
|
||||||
return ' "' + x.encode('ascii', 'xmlcharrefreplace') + '"'
|
return ' "' + cgi.escape(x).encode(
|
||||||
|
'ascii', 'xmlcharrefreplace'
|
||||||
|
) + '"'
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -159,12 +173,14 @@ class BackupMessage(IProcessDesc):
|
|||||||
if arg and len(arg) >= 2 and arg[:2] == '--':
|
if arg and len(arg) >= 2 and arg[:2] == '--':
|
||||||
res += ' ' + arg
|
res += ' ' + arg
|
||||||
elif replace_next:
|
elif replace_next:
|
||||||
res += ' XXX'
|
res += ' "' + cgi.escape(
|
||||||
|
os.path.join('<STORAGE_DIR>', self.bfile)
|
||||||
|
).encode('ascii', 'xmlcharrefreplace') + '"'
|
||||||
else:
|
else:
|
||||||
if arg == '--file':
|
if arg == '--file':
|
||||||
replace_next = True
|
replace_next = True
|
||||||
res += cmdArg(arg)
|
res += cmdArg(arg)
|
||||||
res += '</div>'
|
res += '</i></div>'
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@ -197,8 +213,12 @@ def filename_with_file_manager_path(file):
|
|||||||
Filename to use for backup with full path taken from preference
|
Filename to use for backup with full path taken from preference
|
||||||
"""
|
"""
|
||||||
# Set file manager directory from preference
|
# Set file manager directory from preference
|
||||||
file_manager_dir = get_storage_directory()
|
storage_dir = get_storage_directory()
|
||||||
return os.path.join(file_manager_dir, file)
|
|
||||||
|
if storage_dir:
|
||||||
|
return os.path.join(storage_dir, file)
|
||||||
|
|
||||||
|
return file
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route('/create_job/<int:sid>', methods=['POST'])
|
@blueprint.route('/create_job/<int:sid>', methods=['POST'])
|
||||||
@ -220,7 +240,7 @@ def create_backup_job(sid):
|
|||||||
else:
|
else:
|
||||||
data = json.loads(request.data.decode())
|
data = json.loads(request.data.decode())
|
||||||
|
|
||||||
data['file'] = filename_with_file_manager_path(data['file'])
|
backup_file = filename_with_file_manager_path(data['file'])
|
||||||
|
|
||||||
# Fetch the server details like hostname, port, roles etc
|
# Fetch the server details like hostname, port, roles etc
|
||||||
server = Server.query.filter_by(
|
server = Server.query.filter_by(
|
||||||
@ -250,7 +270,7 @@ def create_backup_job(sid):
|
|||||||
|
|
||||||
args = [
|
args = [
|
||||||
'--file',
|
'--file',
|
||||||
data['file'],
|
backup_file,
|
||||||
'--host',
|
'--host',
|
||||||
server.host,
|
server.host,
|
||||||
'--port',
|
'--port',
|
||||||
@ -275,7 +295,7 @@ def create_backup_job(sid):
|
|||||||
p = BatchProcess(
|
p = BatchProcess(
|
||||||
desc=BackupMessage(
|
desc=BackupMessage(
|
||||||
BACKUP.SERVER if data['type'] != 'global' else BACKUP.GLOBALS,
|
BACKUP.SERVER if data['type'] != 'global' else BACKUP.GLOBALS,
|
||||||
sid
|
sid, data['file']
|
||||||
),
|
),
|
||||||
cmd=utility, args=args
|
cmd=utility, args=args
|
||||||
)
|
)
|
||||||
@ -313,7 +333,7 @@ def create_backup_objects_job(sid):
|
|||||||
else:
|
else:
|
||||||
data = json.loads(request.data.decode())
|
data = json.loads(request.data.decode())
|
||||||
|
|
||||||
data['file'] = filename_with_file_manager_path(data['file'])
|
backup_file = filename_with_file_manager_path(data['file'])
|
||||||
|
|
||||||
# Fetch the server details like hostname, port, roles etc
|
# Fetch the server details like hostname, port, roles etc
|
||||||
server = Server.query.filter_by(
|
server = Server.query.filter_by(
|
||||||
@ -342,7 +362,7 @@ def create_backup_objects_job(sid):
|
|||||||
utility = manager.utility('backup')
|
utility = manager.utility('backup')
|
||||||
args = [
|
args = [
|
||||||
'--file',
|
'--file',
|
||||||
data['file'],
|
backup_file,
|
||||||
'--host',
|
'--host',
|
||||||
server.host,
|
server.host,
|
||||||
'--port',
|
'--port',
|
||||||
@ -353,7 +373,7 @@ def create_backup_objects_job(sid):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def set_param(key, param):
|
def set_param(key, param):
|
||||||
if key in data:
|
if key in data and data[key]:
|
||||||
args.append(param)
|
args.append(param)
|
||||||
|
|
||||||
def set_value(key, param, value):
|
def set_value(key, param, value):
|
||||||
@ -420,8 +440,7 @@ def create_backup_objects_job(sid):
|
|||||||
try:
|
try:
|
||||||
p = BatchProcess(
|
p = BatchProcess(
|
||||||
desc=BackupMessage(
|
desc=BackupMessage(
|
||||||
BACKUP.OBJECT,
|
BACKUP.OBJECT, sid, data['file'], database=data['database']
|
||||||
sid, database=data['database']
|
|
||||||
),
|
),
|
||||||
cmd=utility, args=args)
|
cmd=utility, args=args)
|
||||||
p.start()
|
p.start()
|
||||||
|
@ -483,8 +483,13 @@ TODO LIST FOR BACKUP:
|
|||||||
data:{ 'data': JSON.stringify(args) },
|
data:{ 'data': JSON.stringify(args) },
|
||||||
success: function(res) {
|
success: function(res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
alertify.message('{{ _('Background process for taking backup has been created!') }}', 1);
|
alertify.message(
|
||||||
|
'{{ _('Background process for taking backup has been created!') }}',
|
||||||
|
5
|
||||||
|
);
|
||||||
pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
|
pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
|
||||||
|
} else {
|
||||||
|
console.log(res);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
|
320
web/pgadmin/tools/restore/__init__.py
Normal file
320
web/pgadmin/tools/restore/__init__.py
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
##########################################################################
|
||||||
|
#
|
||||||
|
# pgAdmin 4 - PostgreSQL Tools
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
|
||||||
|
# This software is released under the PostgreSQL Licence
|
||||||
|
#
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
"""Implements Restore Utility"""
|
||||||
|
|
||||||
|
import cgi
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from flask import render_template, request, current_app, \
|
||||||
|
url_for, Response
|
||||||
|
from flask.ext.security import login_required, current_user
|
||||||
|
from flask.ext.babel import gettext as _
|
||||||
|
|
||||||
|
from config import PG_DEFAULT_DRIVER
|
||||||
|
from pgadmin.model import Server
|
||||||
|
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
|
||||||
|
from pgadmin.utils.ajax import make_json_response, bad_request
|
||||||
|
from pgadmin.utils import PgAdminModule, get_storage_directory
|
||||||
|
|
||||||
|
# set template path for sql scripts
|
||||||
|
MODULE_NAME = 'restore'
|
||||||
|
server_info = {}
|
||||||
|
|
||||||
|
|
||||||
|
class RestoreModule(PgAdminModule):
|
||||||
|
"""
|
||||||
|
class RestoreModule(Object):
|
||||||
|
|
||||||
|
It is a utility which inherits PgAdminModule
|
||||||
|
class and define methods to load its own
|
||||||
|
javascript file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
LABEL = _('Restore')
|
||||||
|
|
||||||
|
def get_own_javascripts(self):
|
||||||
|
""""
|
||||||
|
Returns:
|
||||||
|
list: js files used by this module
|
||||||
|
"""
|
||||||
|
return [{
|
||||||
|
'name': 'pgadmin.tools.restore',
|
||||||
|
'path': url_for('restore.index') + 'restore',
|
||||||
|
'when': None
|
||||||
|
}]
|
||||||
|
|
||||||
|
# Create blueprint for RestoreModule class
|
||||||
|
blueprint = RestoreModule(
|
||||||
|
MODULE_NAME, __name__, static_url_path=''
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RestoreMessage(IProcessDesc):
|
||||||
|
|
||||||
|
def __init__(self, _sid, _bfile):
|
||||||
|
self.sid = _sid
|
||||||
|
self.bfile = _bfile
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
# Fetch the server details like hostname, port, roles etc
|
||||||
|
s = Server.query.filter_by(
|
||||||
|
id=self.sid, user_id=current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
return _("Restoring backup on the server - '{0}'").format(
|
||||||
|
"{0} ({1}:{2})".format(s.name, s.host, s.port),
|
||||||
|
)
|
||||||
|
|
||||||
|
def details(self, cmd, args):
|
||||||
|
# Fetch the server details like hostname, port, roles etc
|
||||||
|
s = Server.query.filter_by(
|
||||||
|
id=self.sid, user_id=current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
res = '<div class="h5">'
|
||||||
|
|
||||||
|
res += cgi.escape(
|
||||||
|
_(
|
||||||
|
"Restoring the backup on the server - '{0}'"
|
||||||
|
).format(
|
||||||
|
"{0} ({1}:{2})".format(s.name, s.host, s.port)
|
||||||
|
)
|
||||||
|
).encode('ascii', 'xmlcharrefreplace')
|
||||||
|
|
||||||
|
res += '</div><div class="h5"><b>'
|
||||||
|
res += cgi.escape(
|
||||||
|
_("Running command:")
|
||||||
|
).encode('ascii', 'xmlcharrefreplace')
|
||||||
|
res += '</b><br><i>'
|
||||||
|
res += cgi.escape(cmd).encode('ascii', 'xmlcharrefreplace')
|
||||||
|
|
||||||
|
def cmdArg(x):
|
||||||
|
if x:
|
||||||
|
x = x.replace('\\', '\\\\')
|
||||||
|
x = x.replace('"', '\\"')
|
||||||
|
x = x.replace('""', '\\"')
|
||||||
|
|
||||||
|
return ' "' + cgi.escape(x).encode(
|
||||||
|
'ascii', 'xmlcharrefreplace'
|
||||||
|
) + '"'
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
idx = 0
|
||||||
|
no_args = len(args)
|
||||||
|
for arg in args:
|
||||||
|
if idx < no_args - 1:
|
||||||
|
if arg[:2] == '--':
|
||||||
|
res += ' ' + arg
|
||||||
|
else:
|
||||||
|
res += cmdArg(arg)
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
if no_args > 1:
|
||||||
|
res += ' "' + cgi.escape(
|
||||||
|
os.path.join('<STORAGE_DIR>', self.bfile) + '"'
|
||||||
|
).encode('ascii', 'xmlcharrefreplace')
|
||||||
|
|
||||||
|
res += '</i></div>'
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/")
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
return bad_request(errormsg=_("This URL can not be called directly!"))
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route("/restore.js")
|
||||||
|
@login_required
|
||||||
|
def script():
|
||||||
|
"""render own javascript"""
|
||||||
|
return Response(
|
||||||
|
response=render_template(
|
||||||
|
"restore/js/restore.js", _=_
|
||||||
|
),
|
||||||
|
status=200,
|
||||||
|
mimetype="application/javascript"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def filename_with_file_manager_path(file):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
file: File name returned from client file manager
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filename to use for backup with full path taken from preference
|
||||||
|
"""
|
||||||
|
# Set file manager directory from preference
|
||||||
|
storage_dir = get_storage_directory()
|
||||||
|
|
||||||
|
if storage_dir:
|
||||||
|
return os.path.join(storage_dir, file)
|
||||||
|
|
||||||
|
return file
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/create_job/<int:sid>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def create_restore_job(sid):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
sid: Server ID
|
||||||
|
|
||||||
|
Creates a new job for restore task
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
if request.form:
|
||||||
|
# Convert ImmutableDict to dict
|
||||||
|
data = dict(request.form)
|
||||||
|
data = json.loads(data['data'][0])
|
||||||
|
else:
|
||||||
|
data = json.loads(request.data.decode())
|
||||||
|
|
||||||
|
backup_file = filename_with_file_manager_path(data['file'])
|
||||||
|
|
||||||
|
# Fetch the server details like hostname, port, roles etc
|
||||||
|
server = Server.query.filter_by(
|
||||||
|
id=sid
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if server is None:
|
||||||
|
return make_json_response(
|
||||||
|
success=0,
|
||||||
|
errormsg=_("Couldn't find the given server")
|
||||||
|
)
|
||||||
|
|
||||||
|
# To fetch MetaData for the server
|
||||||
|
from pgadmin.utils.driver import get_driver
|
||||||
|
|
||||||
|
driver = get_driver(PG_DEFAULT_DRIVER)
|
||||||
|
manager = driver.connection_manager(server.id)
|
||||||
|
conn = manager.connection()
|
||||||
|
connected = conn.connected()
|
||||||
|
|
||||||
|
if not connected:
|
||||||
|
return make_json_response(
|
||||||
|
success=0,
|
||||||
|
errormsg=_("Please connect to the server first...")
|
||||||
|
)
|
||||||
|
|
||||||
|
utility = manager.utility('restore')
|
||||||
|
|
||||||
|
args = []
|
||||||
|
|
||||||
|
if 'list' in data:
|
||||||
|
args.append('--list')
|
||||||
|
else:
|
||||||
|
def set_param(key, param):
|
||||||
|
if key in data and data[key]:
|
||||||
|
args.append(param)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_value(key, param, value):
|
||||||
|
if key in data:
|
||||||
|
args.append(param)
|
||||||
|
if value:
|
||||||
|
if value is True:
|
||||||
|
args.append(data[key])
|
||||||
|
else:
|
||||||
|
args.append(value)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_multiple(key, param, with_schema=True):
|
||||||
|
if key in data:
|
||||||
|
data[key] = json.loads(data[key])
|
||||||
|
if len(data[key]) > 0:
|
||||||
|
if with_schema:
|
||||||
|
for s, o in data[key]:
|
||||||
|
args.extend([
|
||||||
|
param,
|
||||||
|
driver.qtIdent(
|
||||||
|
conn, s
|
||||||
|
) + '.' + driver.qtIdent(conn, o)
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
for o in data[key]:
|
||||||
|
args.extend([param, o])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
args.extend([
|
||||||
|
'--host', server.host, '--port', server.port,
|
||||||
|
'--username', server.username, '--no-password'
|
||||||
|
])
|
||||||
|
|
||||||
|
set_value('role', '--role', True)
|
||||||
|
set_value('database', '--dbname', True)
|
||||||
|
|
||||||
|
if data['format'] == 'directory':
|
||||||
|
args.extend(['--format', 'directory'])
|
||||||
|
|
||||||
|
set_value('pre_data', '--section', 'pre-data')
|
||||||
|
set_value('data', '--section', 'data')
|
||||||
|
set_value('post_data', '--section', 'post-data')
|
||||||
|
|
||||||
|
if not set_param('only_data', '--data-only'):
|
||||||
|
set_param('dns_owner', '--no-owner')
|
||||||
|
set_param('dns_privilege ', '--no-privileges')
|
||||||
|
set_param('dns_tablespace', '--no-tablespaces')
|
||||||
|
|
||||||
|
if not set_param('only_schema', '--schema-only'):
|
||||||
|
set_param('disable_trigger', '--disable-triggers')
|
||||||
|
|
||||||
|
set_param('include_create_database', '--create')
|
||||||
|
set_param('clean', '--clean')
|
||||||
|
set_param('single_transaction', '--single-transaction')
|
||||||
|
set_param('no_data_fail_table ', '--no-data-for-failed-tables')
|
||||||
|
set_param('use_set_session_auth ', '--use-set-session-authorization')
|
||||||
|
set_param('exit_on_error', '--exit-on-error')
|
||||||
|
|
||||||
|
set_value('no_of_jobs', '--jobs', True)
|
||||||
|
set_param('verbose', '--verbose')
|
||||||
|
|
||||||
|
set_multiple('schemas', '--schema', False)
|
||||||
|
set_multiple('tables', '--table')
|
||||||
|
set_multiple('functions', '--function')
|
||||||
|
set_multiple('triggers', '--trigger')
|
||||||
|
set_multiple('trigger_funcs', '--function')
|
||||||
|
set_multiple('indexes', '--index')
|
||||||
|
|
||||||
|
args.append(backup_file)
|
||||||
|
|
||||||
|
try:
|
||||||
|
p = BatchProcess(
|
||||||
|
desc=RestoreMessage(sid, data['file']),
|
||||||
|
cmd=utility, args=args
|
||||||
|
)
|
||||||
|
p.start()
|
||||||
|
jid = p.id
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.exception(e)
|
||||||
|
return make_json_response(
|
||||||
|
status=410,
|
||||||
|
success=0,
|
||||||
|
errormsg=str(e)
|
||||||
|
)
|
||||||
|
# Return response
|
||||||
|
return make_json_response(
|
||||||
|
data={'job_id': jid, 'Success': 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
TODO://
|
||||||
|
Add browser tree
|
||||||
|
"""
|
477
web/pgadmin/tools/restore/templates/restore/js/restore.js
Normal file
477
web/pgadmin/tools/restore/templates/restore/js/restore.js
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
define([
|
||||||
|
'jquery', 'underscore', 'underscore.string', 'alertify',
|
||||||
|
'pgadmin.browser', 'backbone', 'backgrid', 'backform',
|
||||||
|
'pgadmin.browser.node'
|
||||||
|
],
|
||||||
|
|
||||||
|
// This defines Restore dialog
|
||||||
|
function($, _, S, alertify, pgBrowser, Backbone, Backgrid, Backform, pgNode) {
|
||||||
|
|
||||||
|
// if module is already initialized, refer to that.
|
||||||
|
if (pgBrowser.Restore) {
|
||||||
|
return pgBrowser.Restore;
|
||||||
|
}
|
||||||
|
|
||||||
|
var CustomSwitchControl = Backform.CustomSwitchControl = Backform.SwitchControl.extend({
|
||||||
|
template: _.template([
|
||||||
|
'<label class="<%=Backform.controlLabelClassName%> custom_switch_label_class"><%=label%></label>',
|
||||||
|
'<div class="<%=Backform.controlsClassName%> custom_switch_control_class">',
|
||||||
|
' <div class="checkbox">',
|
||||||
|
' <label>',
|
||||||
|
' <input type="checkbox" class="<%=extraClasses.join(\' \')%>" name="<%=name%>" <%=value ? "checked=\'checked\'" : ""%> <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%> />',
|
||||||
|
' </label>',
|
||||||
|
' </div>',
|
||||||
|
'</div>',
|
||||||
|
'<% if (helpMessage && helpMessage.length) { %>',
|
||||||
|
' <span class="<%=Backform.helpMessageClassName%>"><%=helpMessage%></span>',
|
||||||
|
'<% } %>'
|
||||||
|
].join("\n")),
|
||||||
|
className: 'pgadmin-control-group form-group col-xs-6'
|
||||||
|
});
|
||||||
|
|
||||||
|
//Restore Model (Objects like Database/Schema/Table)
|
||||||
|
var RestoreObjectModel = Backbone.Model.extend({
|
||||||
|
idAttribute: 'id',
|
||||||
|
defaults: {
|
||||||
|
custom: false,
|
||||||
|
file: undefined,
|
||||||
|
role: null,
|
||||||
|
format: 'Custom or tar',
|
||||||
|
verbose: true,
|
||||||
|
blobs: true,
|
||||||
|
encoding: undefined,
|
||||||
|
database: undefined,
|
||||||
|
schemas: undefined,
|
||||||
|
tables: undefined,
|
||||||
|
functions: undefined,
|
||||||
|
triggers: undefined,
|
||||||
|
trigger_funcs: undefined,
|
||||||
|
indexes: undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
// Default values!
|
||||||
|
initialize: function(attrs, args) {
|
||||||
|
// Set default options according to node type selection by user
|
||||||
|
var node_type = attrs.node_data.type;
|
||||||
|
|
||||||
|
if (node_type) {
|
||||||
|
// Only_Schema option
|
||||||
|
if (node_type === 'function' || node_type === 'index'
|
||||||
|
|| node_type === 'trigger') {
|
||||||
|
this.set({'only_schema': true}, {silent: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only_Data option
|
||||||
|
if (node_type === 'table') {
|
||||||
|
this.set({'only_data': true}, {silent: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean option
|
||||||
|
if (node_type === 'function' || node_type === 'trigger_function') {
|
||||||
|
this.set({'clean': true}, {silent: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Backbone.Model.prototype.initialize.apply(this, arguments);
|
||||||
|
},
|
||||||
|
schema: [{
|
||||||
|
id: 'format', label: '{{ _('Format') }}',
|
||||||
|
type: 'text', disabled: false,
|
||||||
|
control: 'select2', select2: {
|
||||||
|
allowClear: false,
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{label: "Custom or tar", value: "custom"},
|
||||||
|
{label: "Directory", value: "directory"}
|
||||||
|
]
|
||||||
|
},{
|
||||||
|
id: 'file', label: '{{ _('Filename') }}',
|
||||||
|
type: 'text', disabled: false, control: Backform.FileControl,
|
||||||
|
dialog_type: 'select_file', supp_types: ['*', 'backup','sql', 'patch']
|
||||||
|
},{
|
||||||
|
id: 'no_of_jobs', label: '{{ _('Number of jobs') }}',
|
||||||
|
type: 'int'
|
||||||
|
},{
|
||||||
|
id: 'role', label: '{{ _('Role name') }}',
|
||||||
|
control: 'node-list-by-name', node: 'role',
|
||||||
|
select2: { allowClear: false }
|
||||||
|
},{
|
||||||
|
type: 'nested', control: 'fieldset', label: '{{ _('Sections') }}',
|
||||||
|
group: '{{ _('Restore options') }}',
|
||||||
|
schema:[{
|
||||||
|
id: 'pre_data', label: '{{ _('Pre-data') }}',
|
||||||
|
control: Backform.CustomSwitchControl, group: '{{ _('Sections') }}',
|
||||||
|
deps: ['only_data', 'only_schema'], disabled: function(m) {
|
||||||
|
return this.node.type !== 'function' && this.node.type !== 'table'
|
||||||
|
&& this.node.type !== 'trigger'
|
||||||
|
&& this.node.type !== 'trigger_function'
|
||||||
|
&& (m.get('only_data') || m.get('only_schema'));
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
id: 'data', label: '{{ _('Data') }}',
|
||||||
|
control: Backform.CustomSwitchControl, group: '{{ _('Sections') }}',
|
||||||
|
deps: ['only_data', 'only_schema'], disabled: function(m) {
|
||||||
|
return this.node.type !== 'function' && this.node.type !== 'table'
|
||||||
|
&& this.node.type !== 'trigger'
|
||||||
|
&& this.node.type !== 'trigger_function'
|
||||||
|
&& (m.get('only_data') || m.get('only_schema'));
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
id: 'post_data', label: '{{ _('Post-data') }}',
|
||||||
|
control: Backform.CustomSwitchControl, group: '{{ _('Sections') }}',
|
||||||
|
deps: ['only_data', 'only_schema'], disabled: function(m) {
|
||||||
|
return this.node.type !== 'function' && this.node.type !== 'table'
|
||||||
|
&& this.node.type !== 'trigger'
|
||||||
|
&& this.node.type !== 'trigger_function'
|
||||||
|
&& (m.get('only_data') || m.get('only_schema'));
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
type: 'nested', control: 'fieldset', label: '{{ _('Type of objects') }}',
|
||||||
|
group: '{{ _('Restore options') }}',
|
||||||
|
schema:[{
|
||||||
|
id: 'only_data', label: '{{ _('Only data') }}',
|
||||||
|
control: Backform.CustomSwitchControl, group: '{{ _('Type of objects') }}',
|
||||||
|
deps: ['pre_data', 'data', 'post_data','only_schema'], disabled: function(m) {
|
||||||
|
return (this.node.type !== 'database' && this.node.type !== 'schema')
|
||||||
|
|| ( m.get('pre_data')
|
||||||
|
||m.get('data')
|
||||||
|
|| m.get('post_data')
|
||||||
|
|| m.get('only_schema')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
id: 'only_schema', label: '{{ _('Only schema') }}',
|
||||||
|
control: Backform.CustomSwitchControl, group: '{{ _('Type of objects') }}',
|
||||||
|
deps: ['pre_data', 'data', 'post_data', 'only_data'], disabled: function(m) {
|
||||||
|
return (this.node.type !== 'database' && this.node.type !== 'schema')
|
||||||
|
|| ( m.get('pre_data')
|
||||||
|
|| m.get('data')
|
||||||
|
|| m.get('post_data')
|
||||||
|
|| m.get('only_data')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
type: 'nested', control: 'fieldset', label: '{{ _('Do not save') }}',
|
||||||
|
group: '{{ _('Restore options') }}',
|
||||||
|
schema:[{
|
||||||
|
id: 'dns_owner', label: '{{ _('Owner') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Do not save') }}'
|
||||||
|
},{
|
||||||
|
id: 'dns_privilege', label: '{{ _('Privilege') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Do not save') }}'
|
||||||
|
},{
|
||||||
|
id: 'dns_tablespace', label: '{{ _('Tablespace') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Do not save') }}'
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
type: 'nested', control: 'fieldset', label: '{{ _('Queries') }}',
|
||||||
|
group: '{{ _('Restore options') }}',
|
||||||
|
schema:[{
|
||||||
|
id: 'include_create_database', label: '{{ _('Include CREATE DATABASE statement') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Queries') }}'
|
||||||
|
},{
|
||||||
|
id: 'clean', label: '{{ _('Clean before restore') }}',
|
||||||
|
control: Backform.CustomSwitchControl, group: '{{ _('Queries') }}',
|
||||||
|
disabled: function(m) {
|
||||||
|
return this.node.type === 'function' ||
|
||||||
|
this.node.type === 'trigger_function';
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
id: 'single_transaction', label: '{{ _('Single transaction') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Queries') }}'
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
type: 'nested', control: 'fieldset', label: '{{ _('Disable') }}',
|
||||||
|
group: '{{ _('Restore options') }}',
|
||||||
|
schema:[{
|
||||||
|
id: 'disable_trigger', label: '{{ _('Trigger') }}',
|
||||||
|
control: Backform.CustomSwitchControl, group: '{{ _('Disable') }}'
|
||||||
|
},{
|
||||||
|
id: 'no_data_fail_table', label: '{{ _('No data for Failed Tables') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Disable') }}'
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
type: 'nested', control: 'fieldset', label: '{{ _('Miscellaneous / Behavior') }}',
|
||||||
|
group: '{{ _('Restore options') }}',
|
||||||
|
schema:[{
|
||||||
|
id: 'verbose', label: '{{ _('Verbose messages') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false,
|
||||||
|
group: '{{ _('Miscellaneous / Behavior') }}'
|
||||||
|
},{
|
||||||
|
id: 'use_set_session_auth', label: '{{ _('Use SET SESSION AUTHORIZATION') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false,
|
||||||
|
group: '{{ _('Miscellaneous / Behavior') }}'
|
||||||
|
},{
|
||||||
|
id: 'exit_on_error', label: '{{ _('Exit on error') }}',
|
||||||
|
control: Backform.CustomSwitchControl, disabled: false,
|
||||||
|
group: '{{ _('Miscellaneous / Behavior') }}'
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
validate: function() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an Object Restore of pgBrowser class
|
||||||
|
pgBrowser.Restore = {
|
||||||
|
init: function() {
|
||||||
|
if (this.initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
// Define list of nodes on which restore context menu option appears
|
||||||
|
var restore_supported_nodes = [
|
||||||
|
'database', 'schema',
|
||||||
|
'table', 'function',
|
||||||
|
'trigger', 'index'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
Enable/disable restore menu in tools based
|
||||||
|
on node selected
|
||||||
|
if selected node is present in supported_nodes,
|
||||||
|
menu will be enabled otherwise disabled.
|
||||||
|
Also, hide it for system view in catalogs
|
||||||
|
*/
|
||||||
|
menu_enabled = function(itemData, item, data) {
|
||||||
|
var t = pgBrowser.tree, i = item, d = itemData;
|
||||||
|
var parent_item = t.hasParent(i) ? t.parent(i): null,
|
||||||
|
parent_data = parent_item ? t.itemData(parent_item) : null;
|
||||||
|
if(!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
|
||||||
|
return (
|
||||||
|
(_.indexOf(restore_supported_nodes, d._type) !== -1 &&
|
||||||
|
is_parent_catalog(itemData, item, data) ) ? true: false
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
is_parent_catalog = function(itemData, item, data) {
|
||||||
|
var t = pgBrowser.tree, i = item, d = itemData;
|
||||||
|
// To iterate over tree to check parent node
|
||||||
|
while (i) {
|
||||||
|
// If it is schema then allow user to restore
|
||||||
|
if (_.indexOf(['catalog'], d._type) > -1)
|
||||||
|
return false;
|
||||||
|
i = t.hasParent(i) ? t.parent(i) : null;
|
||||||
|
d = i ? t.itemData(i) : null;
|
||||||
|
}
|
||||||
|
// by default we do not want to allow create menu
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the nodes on which the menus to be appear
|
||||||
|
var menus = [{
|
||||||
|
name: 'restore_object', module: this,
|
||||||
|
applies: ['tools'], callback: 'restore_objects',
|
||||||
|
priority: 9, label: '{{_("Restore...") }}',
|
||||||
|
icon: 'fa fa-upload', enable: menu_enabled
|
||||||
|
}];
|
||||||
|
|
||||||
|
for (var idx = 0; idx < restore_supported_nodes.length; idx++) {
|
||||||
|
menus.push({
|
||||||
|
name: 'restore_' + restore_supported_nodes[idx],
|
||||||
|
node: restore_supported_nodes[idx], module: this,
|
||||||
|
applies: ['context'], callback: 'restore_objects',
|
||||||
|
priority: 9, label: '{{_("Restore...") }}',
|
||||||
|
icon: 'fa fa-upload', enable: menu_enabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pgAdmin.Browser.add_menus(menus);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
// Callback to draw Backup Dialog for objects
|
||||||
|
restore_objects: function(action, treeItem) {
|
||||||
|
var title = '{{ _('Restore') }}',
|
||||||
|
tree = pgBrowser.tree,
|
||||||
|
item = treeItem || tree.selected(),
|
||||||
|
data = item && item.length == 1 && tree.itemData(item),
|
||||||
|
node = data && data._type && pgBrowser.Nodes[data._type];
|
||||||
|
|
||||||
|
if (!node)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(!alertify.pg_restore) {
|
||||||
|
// Create Dialog title on the fly with node details
|
||||||
|
alertify.dialog('pg_restore' ,function factory() {
|
||||||
|
return {
|
||||||
|
main: function(title, item, data, node) {
|
||||||
|
this.set('title', title);
|
||||||
|
this.setting('pg_node', node);
|
||||||
|
this.setting('pg_item', item);
|
||||||
|
this.setting('pg_item_data', data);
|
||||||
|
},
|
||||||
|
setup:function() {
|
||||||
|
return {
|
||||||
|
buttons: [{
|
||||||
|
text: '{{ _('Restore') }}', key: 27,
|
||||||
|
className: 'btn btn-primary', restore: true
|
||||||
|
},{
|
||||||
|
text: '{{ _('Cancel') }}', key: 27,
|
||||||
|
className: 'btn btn-danger', restore: false
|
||||||
|
}],
|
||||||
|
// Set options for dialog
|
||||||
|
options: {
|
||||||
|
title: title,
|
||||||
|
//disable both padding and overflow control.
|
||||||
|
padding : !1,
|
||||||
|
overflow: !1,
|
||||||
|
model: 0,
|
||||||
|
resizable: true,
|
||||||
|
maximizable: true,
|
||||||
|
pinnable: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
// triggered when the dialog is closed
|
||||||
|
onclose: function() {
|
||||||
|
if (this.view) {
|
||||||
|
this.view.remove({data: true, internal: true, silent: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings:{
|
||||||
|
pg_node: null,
|
||||||
|
pg_item: null,
|
||||||
|
pg_item_data: null
|
||||||
|
},
|
||||||
|
prepare: function() {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
// Disable Backup button until user provides Filename
|
||||||
|
this.__internal.buttons[0].element.disabled = true;
|
||||||
|
var $container = $("<div class='restore_dialog'></div>");
|
||||||
|
var t = pgBrowser.tree,
|
||||||
|
i = t.selected(),
|
||||||
|
d = i && i.length == 1 ? t.itemData(i) : undefined,
|
||||||
|
node = d && pgBrowser.Nodes[d._type];
|
||||||
|
|
||||||
|
if (!d)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
|
||||||
|
|
||||||
|
var newModel = new RestoreObjectModel(
|
||||||
|
{node_data: node}, {node_info: treeInfo}
|
||||||
|
),
|
||||||
|
fields = Backform.generateViewSchema(
|
||||||
|
treeInfo, newModel, 'create', node, treeInfo.server, true
|
||||||
|
);
|
||||||
|
|
||||||
|
var view = this.view = new Backform.Dialog({
|
||||||
|
el: $container, model: newModel, schema: fields
|
||||||
|
});
|
||||||
|
|
||||||
|
$(this.elements.body.childNodes[0]).addClass(
|
||||||
|
'alertify_tools_dialog_properties obj_properties'
|
||||||
|
);
|
||||||
|
|
||||||
|
view.render();
|
||||||
|
|
||||||
|
this.elements.content.appendChild($container.get(0));
|
||||||
|
|
||||||
|
// Listen to model & if filename is provided then enable Backup button
|
||||||
|
this.view.model.on('change', function() {
|
||||||
|
if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {
|
||||||
|
this.errorModel.clear();
|
||||||
|
self.__internal.buttons[0].element.disabled = false;
|
||||||
|
} else {
|
||||||
|
self.__internal.buttons[0].element.disabled = true;
|
||||||
|
this.errorModel.set('file', '{{ _('Please provide filename') }}')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
// Callback functions when click on the buttons of the Alertify dialogs
|
||||||
|
callback: function(e) {
|
||||||
|
if (e.button.restore) {
|
||||||
|
// Fetch current server id
|
||||||
|
var t = pgBrowser.tree,
|
||||||
|
i = this.settings['pg_item'] || t.selected(),
|
||||||
|
d = this.settings['pg_item_data'] || (
|
||||||
|
i && i.length == 1 ? t.itemData(i) : undefined
|
||||||
|
),
|
||||||
|
node = this.settings['pg_node'] || (
|
||||||
|
d && pgBrowser.Nodes[d._type]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!d)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var info = node.getTreeNodeHierarchy.apply(node, [i]),
|
||||||
|
m = this.view.model;
|
||||||
|
// Set current node info into model
|
||||||
|
m.set('database', info.database.label);
|
||||||
|
if (!m.get('custom')) {
|
||||||
|
switch (d._type) {
|
||||||
|
case 'schema':
|
||||||
|
m.set('schemas', d.label);
|
||||||
|
break;
|
||||||
|
case 'table':
|
||||||
|
m.set('tables', [info.schema.label, d.label]);
|
||||||
|
break;
|
||||||
|
case 'function':
|
||||||
|
m.set('functions', [info.schema.label, d.label]);
|
||||||
|
break;
|
||||||
|
case 'index':
|
||||||
|
m.set('indexes', [info.schema.label, d.label]);
|
||||||
|
break;
|
||||||
|
case 'trigger':
|
||||||
|
m.set('triggers', [info.schema.label, d.label]);
|
||||||
|
break;
|
||||||
|
case 'trigger_func':
|
||||||
|
m.set('trigger_funcs', [info.schema.label, d.label]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO::
|
||||||
|
// When we will implement the object selection in the
|
||||||
|
// import dialog, we will need to select the objects from
|
||||||
|
// the tree selection tab.
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this,
|
||||||
|
baseUrl = "{{ url_for('restore.index') }}create_job/" +
|
||||||
|
info.server._id,
|
||||||
|
args = this.view.model.toJSON();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: baseUrl,
|
||||||
|
method: 'POST',
|
||||||
|
data:{ 'data': JSON.stringify(args) },
|
||||||
|
success: function(res) {
|
||||||
|
if (res.success) {
|
||||||
|
alertify.message(
|
||||||
|
'{{ _('Restore job created!') }}', 5
|
||||||
|
);
|
||||||
|
pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
|
||||||
|
} else {
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
try {
|
||||||
|
var err = $.parseJSON(xhr.responseText);
|
||||||
|
alertify.alert(
|
||||||
|
'{{ _('Backup failed...') }}',
|
||||||
|
err.errormsg
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
alertify.pg_restore(title, item, data, node).resizeTo('65%','60%');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return pgBrowser.Restore;
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user