diff --git a/TODO.txt b/TODO.txt
index 17c473703..ed5109e98 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -31,3 +31,9 @@ Backup Object
-------------
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.
diff --git a/web/pgadmin/tools/backup/__init__.py b/web/pgadmin/tools/backup/__init__.py
index 38df62afe..6b97552d7 100644
--- a/web/pgadmin/tools/backup/__init__.py
+++ b/web/pgadmin/tools/backup/__init__.py
@@ -9,17 +9,20 @@
"""Implements Backup Utility"""
+import cgi
import json
import os
+
from flask import render_template, request, current_app, \
url_for, Response
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 pgadmin.model import Server
+
from config import PG_DEFAULT_DRIVER
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
@@ -77,9 +80,10 @@ class BackupMessage(IProcessDesc):
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.sid = _sid
+ self.bfile = _bfile
self.database = None
if 'database' in kwargs:
@@ -120,28 +124,36 @@ class BackupMessage(IProcessDesc):
res = '
'
if self.backup_type == BACKUP.OBJECT:
- res += _(
- "Backing up an object on the server - '{0}' on database '{1}'"
- ).format(
- "{0} ({1}:{2})".format(s.name, s.host, s.port),
- self.database
+ res += cgi.escape(
+ _(
+ "Backing up an object on the server - '{0}' on database '{1}'"
+ ).format(
+ "{0} ({1}:{2})".format(s.name, s.host, s.port),
+ self.database
+ )
).encode('ascii', 'xmlcharrefreplace')
if self.backup_type == BACKUP.GLOBALS:
- res += _("Backing up the globals for the server - '{0}'!").format(
- "{0} ({1}:{2})".format(s.name, s.host, s.port)
+ res += cgi.escape(
+ _("Backing up the globals for the server - '{0}'").format(
+ "{0} ({1}:{2})".format(s.name, s.host, s.port)
+ )
).encode('ascii', 'xmlcharrefreplace')
elif self.backup_type == BACKUP.SERVER:
- res += _("Backing up the server - '{0}'!").format(
- "{0} ({1}:{2})".format(s.name, s.host, s.port)
+ res += cgi.escape(
+ _("Backing up the server - '{0}'").format(
+ "{0} ({1}:{2})".format(s.name, s.host, s.port)
+ )
).encode('ascii', 'xmlcharrefreplace')
else:
# It should never reach here.
res += "Backup"
res += '
'
- res += _("Running command:").encode('ascii', 'xmlcharrefreplace')
- res += ' '
- res += cmd.encode('ascii', 'xmlcharrefreplace')
+ res += cgi.escape(
+ _("Running command:")
+ ).encode('ascii', 'xmlcharrefreplace')
+ res += ' '
+ res += cgi.escape(cmd).encode('ascii', 'xmlcharrefreplace')
replace_next = False
@@ -151,7 +163,9 @@ class BackupMessage(IProcessDesc):
x = x.replace('"', '\\"')
x = x.replace('""', '\\"')
- return ' "' + x.encode('ascii', 'xmlcharrefreplace') + '"'
+ return ' "' + cgi.escape(x).encode(
+ 'ascii', 'xmlcharrefreplace'
+ ) + '"'
return ''
@@ -159,12 +173,14 @@ class BackupMessage(IProcessDesc):
if arg and len(arg) >= 2 and arg[:2] == '--':
res += ' ' + arg
elif replace_next:
- res += ' XXX'
+ res += ' "' + cgi.escape(
+ os.path.join('', self.bfile)
+ ).encode('ascii', 'xmlcharrefreplace') + '"'
else:
if arg == '--file':
replace_next = True
res += cmdArg(arg)
- res += '
'
+ 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
"""
# Set file manager directory from preference
- file_manager_dir = get_storage_directory()
- return os.path.join(file_manager_dir, file)
+ storage_dir = get_storage_directory()
+
+ if storage_dir:
+ return os.path.join(storage_dir, file)
+
+ return file
@blueprint.route('/create_job/', methods=['POST'])
@@ -220,7 +240,7 @@ def create_backup_job(sid):
else:
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
server = Server.query.filter_by(
@@ -250,7 +270,7 @@ def create_backup_job(sid):
args = [
'--file',
- data['file'],
+ backup_file,
'--host',
server.host,
'--port',
@@ -275,7 +295,7 @@ def create_backup_job(sid):
p = BatchProcess(
desc=BackupMessage(
BACKUP.SERVER if data['type'] != 'global' else BACKUP.GLOBALS,
- sid
+ sid, data['file']
),
cmd=utility, args=args
)
@@ -313,7 +333,7 @@ def create_backup_objects_job(sid):
else:
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
server = Server.query.filter_by(
@@ -342,7 +362,7 @@ def create_backup_objects_job(sid):
utility = manager.utility('backup')
args = [
'--file',
- data['file'],
+ backup_file,
'--host',
server.host,
'--port',
@@ -353,7 +373,7 @@ def create_backup_objects_job(sid):
]
def set_param(key, param):
- if key in data:
+ if key in data and data[key]:
args.append(param)
def set_value(key, param, value):
@@ -420,8 +440,7 @@ def create_backup_objects_job(sid):
try:
p = BatchProcess(
desc=BackupMessage(
- BACKUP.OBJECT,
- sid, database=data['database']
+ BACKUP.OBJECT, sid, data['file'], database=data['database']
),
cmd=utility, args=args)
p.start()
diff --git a/web/pgadmin/tools/backup/templates/backup/js/backup.js b/web/pgadmin/tools/backup/templates/backup/js/backup.js
index ce70d53e5..082a6dab0 100644
--- a/web/pgadmin/tools/backup/templates/backup/js/backup.js
+++ b/web/pgadmin/tools/backup/templates/backup/js/backup.js
@@ -483,8 +483,13 @@ TODO LIST FOR BACKUP:
data:{ 'data': JSON.stringify(args) },
success: function(res) {
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);
+ } else {
+ console.log(res);
}
},
error: function(xhr, status, error) {
diff --git a/web/pgadmin/tools/restore/__init__.py b/web/pgadmin/tools/restore/__init__.py
new file mode 100644
index 000000000..ab04107ec
--- /dev/null
+++ b/web/pgadmin/tools/restore/__init__.py
@@ -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 = '
'
+
+ 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 += '
'
+ res += cgi.escape(
+ _("Running command:")
+ ).encode('ascii', 'xmlcharrefreplace')
+ res += ' '
+ 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('', self.bfile) + '"'
+ ).encode('ascii', 'xmlcharrefreplace')
+
+ res += '
'
+
+ 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/', 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
+"""
diff --git a/web/pgadmin/tools/restore/templates/restore/js/restore.js b/web/pgadmin/tools/restore/templates/restore/js/restore.js
new file mode 100644
index 000000000..787f14bb6
--- /dev/null
+++ b/web/pgadmin/tools/restore/templates/restore/js/restore.js
@@ -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([
+ '',
+ '