Addd support for taking backup for the server.

Tweaked by Ashesh Vashi to integrate the backgroud process, and also
with some improvements as stated below:
* Resolved an issue loading existing preference.
* Improved the background process observer/executor for supporting
  detalied view.
* Added the utility path preferences in the ServerType class.
pull/3/head
Murtuza Zabuawala 2016-05-15 15:59:32 +05:30 committed by Ashesh Vashi
parent fe0911f285
commit 8ca760ee2b
10 changed files with 1282 additions and 73 deletions

View File

@ -26,3 +26,8 @@ Query Tool updateable recordset support
Add smarts to the Query Tool to allow it to recognise if a query produces a
data set that would be updateable (e.g. from a single table, all primary key
columns present), and if so, allow editing.
Backup Object
-------------
Allow to select/deselect objects under the object backup operation.

View File

@ -170,7 +170,7 @@ class ServerModule(sg.ServerGroupPluginModule):
Override it so that - it does not register the show_node preference for
server type.
"""
pass
ServerType.register_preferences()
class ServerMenuItem(MenuItem):
def __init__(self, **kwargs):

View File

@ -7,8 +7,10 @@
#
##########################################################################
import os
from flask import render_template
from flask.ext.babel import gettext
from flask.ext.babel import gettext as _
from pgadmin.utils.preferences import Preferences
class ServerType(object):
@ -26,6 +28,7 @@ class ServerType(object):
self.stype = server_type
self.desc = description
self.spriority = priority
self.utility_path = None
assert(server_type not in ServerType.registry)
ServerType.registry[server_type] = self
@ -38,6 +41,24 @@ class ServerType(object):
def description(self):
return self.desc
@classmethod
def register_preferences(cls):
paths = Preferences('paths', _('Paths'))
for key in cls.registry:
st = cls.registry[key]
st.utility_path = paths.register(
'bin_paths', st.stype + '_bin_dir',
_("{0} bin path").format(st.stype.upper()),
'text', "", category_label=_('Binary paths'),
help_str=_(
"Set the PATH where the {0} binary utilities can be found...".format(
st.desc
)
)
)
@property
def priority(self):
return self.spriority
@ -70,17 +91,29 @@ class ServerType(object):
reverse=True
)
@classmethod
def utility(cls, operation, sverion):
if operation == 'backup':
return 'pg_dump'
if operation == 'backup_server':
return 'pg_dumpall'
if operation == 'restore':
return 'pg_restore'
def utility(self, operation, sverion):
res = None
return None
if operation == 'backup':
res = 'pg_dump'
elif operation == 'backup_server':
res = 'pg_dumpall'
elif operation == 'restore':
res = 'pg_restore'
elif operation == 'sql':
res = 'psql'
else:
raise Exception(
_("Couldn't find the utility for the operation '%s'".format(
operation
))
)
return os.path.join(
self.utility_path.get(),
(res if os.name != 'nt' else (res + '.exe'))
)
# Default Server Type
ServerType('pg', gettext("PostgreSQL"), -1)
ServerType('pg', _("PostgreSQL"), -1)

View File

@ -9,16 +9,15 @@
"""A blueprint module providing utility functions for the application."""
import datetime
from flask import session, current_app
from pgadmin.utils import PgAdminModule
import pgadmin.utils.driver as driver
MODULE_NAME = 'misc'
# Initialise the module
blueprint = PgAdminModule(MODULE_NAME, __name__,
url_prefix='')
blueprint = PgAdminModule(
MODULE_NAME, __name__, url_prefix=''
)
##########################################################################
# A special URL used to "ping" the server

View File

@ -12,7 +12,7 @@
Introduce a function to run the process executor in detached mode.
"""
from __future__ import print_function, unicode_literals
from abc import ABCMeta, abstractproperty
from abc import ABCMeta, abstractproperty, abstractmethod
import csv
from datetime import datetime
from dateutil import parser
@ -50,8 +50,8 @@ class IProcessDesc(object):
def message(self):
pass
@abstractproperty
def details(self):
@abstractmethod
def details(self, cmd, args):
pass
@ -322,7 +322,12 @@ class BatchProcess(object):
details = desc
if isinstance(desc, IProcessDesc):
details = desc.details
args = []
args_csv = StringIO(p.arguments)
args_reader = csv.reader(args_csv, delimiter=str(','))
for arg in args_reader:
args = args + arg
details = desc.details(p.command, args)
desc = desc.message
res.append({

View File

@ -170,11 +170,6 @@ function(_, S, $, pgBrowser, alertify, pgMessages) {
self.curr_status = pgMessages['running'];
}
if ('execution_time' in data) {
self.execution_time = self.execution_time + ' ' +
pgMessages['seconds'];
}
if (!_.isNull(self.exit_code)) {
if (self.exit_code == 0) {
self.curr_status = pgMessages['successfully_finished'];
@ -238,12 +233,12 @@ function(_, S, $, pgBrowser, alertify, pgMessages) {
if (!self.notifier) {
var content = $('<div class="pg-bg-bgprocess row"></div>').append(
$('<div></div>', {
class: "col-xs-12 h3 pg-bg-notify-header"
class: "h5 pg-bg-notify-header"
}).text(
self.desc
)
).append(
$('<div></div>', {class: 'pg-bg-notify-body' }).append(
$('<div></div>', {class: 'pg-bg-notify-body h6' }).append(
$('<div></div>', {class: 'pg-bg-start col-xs-12' }).append(
$('<div></div>').text(self.stime.toString())
).append(
@ -252,10 +247,10 @@ function(_, S, $, pgBrowser, alertify, pgMessages) {
)
),
for_details = $('<div></div>', {
class: "col-xs-12 text-center pg-bg-click"
class: "col-xs-12 text-center pg-bg-click h6"
}).text(pgMessages.CLICK_FOR_DETAILED_MSG).appendTo(content),
status = $('<div></div>', {
class: "pg-bg-status col-xs-12 " + ((self.exit_code === 0) ?
class: "pg-bg-status col-xs-12 h5 " + ((self.exit_code === 0) ?
'bg-success': (self.exit_code == 1) ?
'bg-failed' : '')
}).appendTo(content);
@ -287,22 +282,34 @@ function(_, S, $, pgBrowser, alertify, pgMessages) {
});
}
// TODO:: Formatted execution time
self.container.find('.pg-bg-etime').empty().text(
String(self.execution_time)
self.container.find('.pg-bg-etime').empty().append(
$('<span></span>', {class: 'blink'}).text(
String(self.execution_time)
)
).append(
$('<span></span>').text(' ' + pgMessages['seconds'])
);
self.container.find('.pg-bg-status').empty().append(
self.curr_status
)
self.curr_status
);
} else {
self.show_detailed_view.apply(self)
}
},
show_detailed_view: function() {
var self = this,
panel = this.panel =
panel = this.panel,
is_new = false;
if (!self.panel) {
is_new = true;
panel = this.panel =
pgBrowser.BackgroundProcessObsorver.create_panel();
panel.title('Process Watcher - ' + self.desc);
panel.focus();
panel.title('Process Watcher - ' + self.desc);
panel.focus();
}
var container = panel.$container,
status_class = (
@ -314,52 +321,61 @@ function(_, S, $, pgBrowser, alertify, pgMessages) {
$header = container.find('.bg-process-details'),
$footer = container.find('.bg-process-footer');
if (is_new) {
// set logs
$logs.html(self.logs);
// set logs
$logs.html(self.logs);
// set bgprocess detailed description
$header.find('.bg-detailed-desc').html(self.detailed_desc);
// set bgprocess detailed description
$header.find('.bg-detailed-desc').html(self.detailed_desc);
}
// set bgprocess start time
$header.find('.bg-process-stats .bgprocess-start-time').html(self.stime);
$header.find('.bg-process-stats .bgprocess-start-time').html(
self.stime
);
// set status
$footer.find('.bg-process-status p').addClass(
$footer.find('.bg-process-status p').removeClass().addClass(
status_class
).html(
self.curr_status
);
).html(self.curr_status);
// set bgprocess execution time
$footer.find('.bg-process-exec-time p').html(self.execution_time);
self.details = true;
setTimeout(
function() {
self.status.apply(self);
}, 1000
$footer.find('.bg-process-exec-time p').empty().append(
$('<span></span>', {class: 'blink'}).text(
String(self.execution_time)
)
).append(
$('<span></span>').text(' ' + pgMessages['seconds'])
);
var resize_log_container = function($logs, $header, $footer) {
var h = $header.outerHeight() + $footer.outerHeight();
$logs.css('padding-bottom', h);
}.bind(panel, $logs, $header, $footer);
if (is_new) {
self.details = true;
setTimeout(
function() {
self.status.apply(self);
}, 1000
);
panel.on(wcDocker.EVENT.RESIZED, resize_log_container);
panel.on(wcDocker.EVENT.ATTACHED, resize_log_container);
panel.on(wcDocker.EVENT.DETACHED, resize_log_container);
var resize_log_container = function($logs, $header, $footer) {
var h = $header.outerHeight() + $footer.outerHeight();
$logs.css('padding-bottom', h);
}.bind(panel, $logs, $header, $footer);
resize_log_container();
panel.on(wcDocker.EVENT.RESIZED, resize_log_container);
panel.on(wcDocker.EVENT.ATTACHED, resize_log_container);
panel.on(wcDocker.EVENT.DETACHED, resize_log_container);
panel.on(wcDocker.EVENT.CLOSED, function(process) {
process.panel = null;
resize_log_container();
process.details = false;
if (process.exit_code != null) {
process.acknowledge_server.apply(process);
}
}.bind(panel, this));
panel.on(wcDocker.EVENT.CLOSED, function(process) {
process.panel = null;
process.details = false;
if (process.exit_code != null) {
process.acknowledge_server.apply(process);
}
}.bind(panel, this));
}
},
acknowledge_server: function() {
@ -472,9 +488,9 @@ function(_, S, $, pgBrowser, alertify, pgMessages) {
content: '<div class="bg-process-details col-xs-12">'+
'<p class="bg-detailed-desc"></p>'+
'<div class="bg-process-stats">'+
'<span><b>' + pgMessages['START_TIME'] + ':</b></span>'+
'<p class="bgprocess-start-time"></p>'+
'</div>'+
'<span><b>' + pgMessages['START_TIME'] + ': </b>'+
'<span class="bgprocess-start-time"></span>'+
'</span></div>'+
'</div>'+
'<div class="bg-process-watcher col-xs-12">'+
'</div>'+

View File

@ -1098,6 +1098,7 @@ button.pg-alertify-button {
word-break: break-all;
word-wrap: break-word;
}
div.backform_control_notes label.control-label {
min-width: 0px;
}
@ -1106,7 +1107,6 @@ form[name="change_password_form"] .help-block {
color: #A94442 !important;
}
.file_selection_ctrl .create_input span {
padding-right: 10px;
font-weight: bold;
@ -1165,3 +1165,44 @@ form[name="change_password_form"] .help-block {
.dropzone .dz-preview .dz-progress .dz-upload {
bottom: initial;
}
/* Fix Alertify dialog alignment for Backform controls */
.alertify_tools_dialog_properties {
bottom: 0 !important;
left: 0 !important;
position: absolute !important;
right: 0 !important;
top: 35px !important;
}
/* For Backup & Restore Dialog */
.custom_switch_label_class {
min-width: 0px !important;
padding-bottom: 10px !important;
font-size: 13px !important;
font-weight: normal !important;
}
.custom_switch_control_class {
min-width: 0px !important;
padding-bottom: 10px !important;
}
/* animate blink */
.blink {
animation: blink-animation 1s steps(5, start) infinite;
-webkit-animation: blink-animation 1s steps(5, start) infinite;
}
@keyframes blink-animation {
to {
visibility: hidden;
}
}
@-webkit-keyframes blink-animation {
to {
visibility: hidden;
}
}

View File

@ -0,0 +1,440 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Implements Backup Utility"""
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
# set template path for sql scripts
MODULE_NAME = 'backup'
server_info = {}
class BackupModule(PgAdminModule):
"""
class BackupModule(Object):
It is a utility which inherits PgAdminModule
class and define methods to load its own
javascript file.
"""
LABEL = _('Backup')
def get_own_javascripts(self):
""""
Returns:
list: js files used by this module
"""
return [{
'name': 'pgadmin.tools.backup',
'path': url_for('backup.index') + 'backup',
'when': None
}]
def show_system_objects(self):
"""
return system preference objects
"""
return self.pref_show_system_objects
# Create blueprint for BackupModule class
blueprint = BackupModule(
MODULE_NAME, __name__, static_url_path=''
)
class BACKUP(object):
"""
Constants defined for Backup utilities
"""
GLOBALS = 1
SERVER = 2
OBJECT = 3
class BackupMessage(IProcessDesc):
"""
BackupMessage(IProcessDesc)
Defines the message shown for the backup operation.
"""
def __init__(self, _type, _sid, **kwargs):
self.backup_type = _type
self.sid = _sid
self.database = None
if 'database' in kwargs:
self.database = kwargs['database']
@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()
if self.backup_type == BACKUP.OBJECT:
return _(
"Backing up an object on the server - '{0}' on database '{1}'..."
).format(
"{0} ({1}:{2})".format(s.name, s.host, s.port),
self.database
)
if self.backup_type == BACKUP.GLOBALS:
return _("Backing up the globals for the server - '{0}'...").format(
"{0} ({1}:{2})".format(s.name, s.host, s.port)
)
elif self.backup_type == BACKUP.SERVER:
return _("Backing up the server - '{0}'...").format(
"{0} ({1}:{2})".format(s.name, s.host, s.port)
)
else:
# It should never reach here.
return "Unknown Backup"
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">'
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
).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)
).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)
).encode('ascii', 'xmlcharrefreplace')
else:
# It should never reach here.
res += "Backup"
res += '</div><div class="h5">'
res += _("Running command:").encode('ascii', 'xmlcharrefreplace')
res += '<br>'
res += cmd.encode('ascii', 'xmlcharrefreplace')
replace_next = False
def cmdArg(x):
if x:
x = x.replace('\\', '\\\\')
x = x.replace('"', '\\"')
x = x.replace('""', '\\"')
return ' "' + x.encode('ascii', 'xmlcharrefreplace') + '"'
return ''
for arg in args:
if arg and len(arg) >= 2 and arg[:2] == '--':
res += ' ' + arg
elif replace_next:
res += ' XXX'
else:
if arg == '--file':
replace_next = True
res += cmdArg(arg)
res += '</div>'
return res
@blueprint.route("/")
@login_required
def index():
return bad_request(errormsg=_("This URL can not be called directly!"))
@blueprint.route("/backup.js")
@login_required
def script():
"""render own javascript"""
return Response(
response=render_template(
"backup/js/backup.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
file_manager_dir = get_storage_directory()
return os.path.join(file_manager_dir, file)
@blueprint.route('/create_job/<int:sid>', methods=['POST'])
@login_required
def create_backup_job(sid):
"""
Args:
sid: Server ID
Creates a new job for backup task (Backup Server/Globals)
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())
data['file'] = filename_with_file_manager_path(data['file'])
# Fetch the server details like hostname, port, roles etc
server = Server.query.filter_by(
id=sid, user_id=current_user.id
).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('backup_server')
args = [
'--file',
data['file'],
'--host',
server.host,
'--port',
str(server.port),
'--username',
server.username,
'--no-password',
'--database',
driver.qtIdent(conn, server.maintenance_db)
]
if 'role' in data and data['role']:
args.append('--role')
args.append(data['role'])
if 'verbose' in data and data['verbose']:
args.append('--verbose')
if 'dqoute' in data and data['dqoute']:
args.append('--quote-all-identifiers')
if data['type'] == 'global':
args.append('--globals-only')
try:
p = BatchProcess(
desc=BackupMessage(
BACKUP.SERVER if data['type'] != 'global' else BACKUP.GLOBALS,
sid
),
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}
)
@blueprint.route('/create_job/backup_object/<int:sid>', methods=['POST'])
@login_required
def create_backup_objects_job(sid):
"""
Args:
sid: Server ID
Creates a new job for backup task (Backup Database(s)/Schema(s)/Table(s))
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())
data['file'] = filename_with_file_manager_path(data['file'])
# Fetch the server details like hostname, port, roles etc
server = Server.query.filter_by(
id=sid, user_id=current_user.id
).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('backup')
args = [
'--file',
data['file'],
'--host',
server.host,
'--port',
str(server.port),
'--username',
server.username,
'--no-password'
]
def set_param(key, param):
if key in data:
args.append(param)
def set_value(key, param, value):
if key in data:
args.append(param)
if value:
if value is True:
args.append(param[key])
else:
args.append(value)
set_param('verbose', '--verbose')
set_param('dqoute', '--quote-all-identifiers')
if data['format'] is not None:
if data['format'] == 'custom':
args.extend(['--format', 'custom'])
set_param('blobs', '--blobs')
set_value('ratio', '--compress', True)
elif data['format'] == 'tar':
args.extend(['--format', 'tar'])
set_param('blobs', '--blobs')
elif data['format'] == 'plain':
args.extend(['--format', 'plain'])
if data['only_data']:
args.append('--data-only')
set_param('disable_trigger', '--disable-triggers')
else:
set_param('only_schema', '--schema-only')
set_param('dns_owner', '--no-owner')
set_param('include_create_database', '--create')
set_param('include_drop_database', '--clean')
elif data['format'] == 'directory':
args.extend(['--format', 'directory'])
set_param('pre_data', '--section pre-data')
set_param('data', '--section data')
set_param('post_data', '--section post-data')
set_param('dns_privilege', '--no-privileges')
set_param('dns_tablespace', '--no-tablespaces')
set_param('dns_unlogged_tbl_data', '--no-unlogged-table-data')
set_param('use_insert_commands', '--inserts')
set_param('use_column_inserts', '--column-inserts')
set_param('disable_quoting', '--disable-dollar-quoting')
set_param('with_oids', '--oids')
set_param('use_set_session_auth', '--use-set-session-authorization')
set_value('no_of_jobs', '--jobs', True)
for s in data['schemas']:
args.extend(['--schema', driver.qtIdent(conn, s)])
for s, t in data['tables']:
args.extend([
'--table', driver.qtIdent(conn, s) + '.' + driver.qtIdent(conn, t)
])
args.append(driver.qtIdent(conn, data['database']))
try:
p = BatchProcess(
desc=BackupMessage(
BACKUP.OBJECT,
sid, database=data['database']
),
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}
)

View File

@ -0,0 +1,670 @@
define([
'jquery', 'underscore', 'underscore.string', 'alertify',
'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node'
],
// This defines Backup dialog
function($, _, S, alertify, pgBrowser, Backbone, Backgrid, Backform, pgNode) {
// if module is already initialized, refer to that.
if (pgBrowser.Backup) {
return pgBrowser.Backup;
}
/*
=====================
TODO LIST FOR BACKUP:
=====================
1) Add Object tree on object tab which allows user to select
objects which can be backed up
2) Allow user to select/deselect objects
3) If database is selected in browser
show all database children objects selected in Object tree
4) If schema is selected in browser
show all schema children objects selected in Object tree
5) If table is selected then show table/schema/database selected
in Object tree
6) if root objects like database/schema is not selected and their
children are selected then add them separately with in tables attribute
with schema.
*/
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'
});
//Backup Model (Server Node)
var BackupModel = Backbone.Model.extend({
idAttribute: 'id',
defaults: {
file: undefined,
role: undefined,
dqoute: false,
verbose: true,
type: undefined /* global, server */
},
schema: [{
id: 'file', label: '{{ _('Filename') }}',
type: 'text', disabled: false, control: Backform.FileControl,
dialog_type: 'create_file', supp_types: ['*', 'sql']
},{
id: 'role', label: '{{ _('Role name') }}',
control: 'node-list-by-name', node: 'role',
select2: { allowClear: false }
},{
type: 'nested', control: 'fieldset', label: '{{ _('Miscellaneous') }}',
schema:[{
id: 'verbose', label: '{{ _('Verbose messages') }}',
control: Backform.CustomSwitchControl, disabled: false,
group: '{{ _('Miscellaneous') }}'
},{
id: 'dqoute', label: '{{ _('Force double quote on identifiers') }}',
control: Backform.CustomSwitchControl, disabled: false,
group: '{{ _('Miscellaneous') }}'
}]
},{
id: 'server_note', label: '{{ _('Note') }}',
text: '{{ _('The backup format will be PLAIN') }}',
type: 'note', visible: function(m){
return m.get('type') === 'server';
}
},{
id: 'globals_note', label: '{{ _('Note') }}',
text: '{{ _('Only objects global to the entire database will be backed up in PLAIN format') }}',
type: 'note', visible: function(m){
return m.get('type') === 'globals';
}
},{
}],
validate: function() {
// TODO: HOW TO VALIDATE ???
return null;
}
});
//Backup Model (Objects like Database/Schema/Table)
var BackupObjectModel = Backbone.Model.extend({
idAttribute: 'id',
defaults: {
file: undefined,
role: 'postgres',
format: 'custom',
verbose: true,
blobs: true,
encoding: undefined,
schemas: [],
tables: [],
database: undefined
},
schema: [{
id: 'file', label: '{{ _('Filename') }}',
type: 'text', disabled: false, control: Backform.FileControl,
dialog_type: 'create_file', supp_types: ['*', 'sql']
},{
id: 'format', label: '{{ _('Format') }}',
type: 'text', disabled: false,
control: 'select2', select2: {
allowClear: false,
width: "100%"
},
options: [
{label: "Custom", value: "custom"},
{label: "Tar", value: "tar"},
{label: "Plain", value: "plain"},
{label: "Directory", value: "directory"}
]
},{
id: 'ratio', label: '{{ _('Comprasion ratio') }}',
type: 'int', min: 0, max:9, disabled: false
},{
id: 'encoding', label: '{{ _('Encoding') }}',
type: 'text', disabled: false, node: 'database',
control: 'node-ajax-options', url: 'get_encodings'
},{
id: 'no_of_jobs', label: '{{ _('Number of jobs') }}',
type: 'int', deps: ['format'], disabled: function(m) {
return !(m.get('format') === "Directory");
}
},{
id: 'role', label: '{{ _('Role name') }}',
control: 'node-list-by-name', node: 'role',
select2: { allowClear: false }
},{
type: 'nested', control: 'fieldset', label: '{{ _('Sections') }}',
group: '{{ _('Dump options') }}',
schema:[{
id: 'pre_data', label: '{{ _('Pre-data') }}',
control: Backform.CustomSwitchControl, group: '{{ _('Sections') }}',
deps: ['only_data', 'only_schema'], disabled: function(m) {
return 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 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 m.get('only_data')
|| m.get('only_schema');
}
}]
},{
type: 'nested', control: 'fieldset', label: '{{ _('Type of objects') }}',
group: '{{ _('Dump 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 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 m.get('pre_data')
|| m.get('data')
|| m.get('post_data')
|| m.get('only_data');
}
},{
id: 'blobs', label: '{{ _('Blobs') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Type of objects') }}'
}]
},{
type: 'nested', control: 'fieldset', label: '{{ _('Do not save') }}',
group: '{{ _('Dump 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') }}'
},{
id: 'dns_unlogged_tbl_data', label: '{{ _('Unlogged table data') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Do not save') }}'
}]
},{
type: 'nested', control: 'fieldset', label: '{{ _('Queries') }}',
group: '{{ _('Dump options') }}',
schema:[{
id: 'use_column_inserts', label: '{{ _('Use Column Inserts') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Queries') }}'
},{
id: 'use_insert_commands', label: '{{ _('Use Insert Commands') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Queries') }}'
},{
id: 'include_create_database', label: '{{ _('Include CREATE DATABASE statement') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Queries') }}'
},{
id: 'include_drop_database', label: '{{ _('Include DROP DATABASE statement') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Queries') }}'
}]
},{
type: 'nested', control: 'fieldset', label: '{{ _('Disable') }}',
group: '{{ _('Dump options') }}',
schema:[{
id: 'disable_trigger', label: '{{ _('Trigger') }}',
control: Backform.CustomSwitchControl, group: '{{ _('Disable') }}',
deps: ['only_data'], disabled: function(m) {
return !(m.get('only_data'));
}
},{
id: 'disable_quoting', label: '{{ _('$ quoting') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Disable') }}'
}]
},{
type: 'nested', control: 'fieldset', label: '{{ _('Miscellaneous') }}',
group: '{{ _('Dump options') }}',
schema:[{
id: 'with_oids', label: '{{ _('With OID(s)') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Miscellaneous') }}'
},{
id: 'verbose', label: '{{ _('Verbose messages') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Miscellaneous') }}'
},{
id: 'dqoute', label: '{{ _('Force double quote on identifiers') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Miscellaneous') }}'
},{
id: 'use_set_session_auth', label: '{{ _('Use SET SESSION AUTHORIZATION') }}',
control: Backform.CustomSwitchControl, disabled: false, group: '{{ _('Miscellaneous') }}'
}]
}],
validate: function() {
return null;
}
});
// Create an Object Backup of pgBrowser class
pgBrowser.Backup = {
init: function() {
if (this.initialized)
return;
this.initialized = true;
// Define list of nodes on which backup context menu option appears
var backup_supported_nodes = [
'database', 'schema', 'table'
];
/**
Enable/disable backup 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(backup_supported_nodes, d._type) !== -1 &&
parent_data._type != 'catalog') ? true: false
);
else
return false;
};
menu_enabled_server = 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 server node selected && connected
if(!_.isUndefined(d) && !_.isNull(d))
return (('server' === d._type) && d.connected);
else
false;
};
// Define the nodes on which the menus to be appear
var menus = [{
name: 'backup_global', module: this,
applies: ['tools'], callback: 'start_backup_global',
priority: 10, label: '{{_("Backup Globals...") }}',
icon: 'fa fa-floppy-o', enable: menu_enabled_server
},{
name: 'backup_server', module: this,
applies: ['tools'], callback: 'start_backup_server',
priority: 10, label: '{{_("Backup Server...") }}',
icon: 'fa fa-floppy-o', enable: menu_enabled_server
},{
name: 'backup_global_ctx', module: this, node: 'server',
applies: ['context'], callback: 'start_backup_global',
priority: 10, label: '{{_("Backup Globals...") }}',
icon: 'fa fa-floppy-o', enable: menu_enabled_server
},{
name: 'backup_server_ctx', module: this, node: 'server',
applies: ['context'], callback: 'start_backup_server',
priority: 10, label: '{{_("Backup Server...") }}',
icon: 'fa fa-floppy-o', enable: menu_enabled_server
},{
name: 'backup_object', module: this,
applies: ['tools'], callback: 'backup_objects',
priority: 10, label: '{{_("Backup...") }}',
icon: 'fa fa-floppy-o', enable: menu_enabled
}];
for (var idx = 0; idx < backup_supported_nodes.length; idx++) {
menus.push({
name: 'backup_' + backup_supported_nodes[idx],
node: backup_supported_nodes[idx], module: this,
applies: ['context'], callback: 'backup_objects',
priority: 10, label: '{{_("Backup...") }}',
icon: 'fa fa-floppy-o', enable: menu_enabled
});
}
pgAdmin.Browser.add_menus(menus);
return this;
},
start_backup_global: function(action, item) {
var params = {'globals': true };
this.start_backup_global_server.apply(
this, [action, item, params]
);
},
start_backup_server: function(action, item) {
var params = {'server': true };
this.start_backup_global_server.apply(
this, [action, item, params]
);
},
// Callback to draw Backup Dialog for globals/server
start_backup_global_server: function(action, item, params) {
var of_type = undefined;
// Set Notes according to type of backup
if (!_.isUndefined(params['globals']) && params['globals']) {
of_type = 'globals';
} else {
of_type = 'server';
}
var DialogName = 'BackupDialog_' + of_type,
DialogTitle = ((of_type == 'globals') ?
'{{ _('Backup Globals...') }}' :
'{{ _('Backup Server...') }}');
if(!alertify[DialogName]) {
alertify.dialog(DialogName ,function factory() {
return {
main: function(title) {
this.set('title', title);
},
setup:function() {
return {
buttons: [{
text: '{{ _('Backup') }}', key: 27, className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button'
},{
text: '{{ _('Cancel') }}', key: 27, className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button'
}],
// Set options for dialog
options: {
title: DialogTitle,
//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) {
// clear our backform model/view
this.view.remove({data: true, internal: true, silent: true});
}
}
},
prepare: function() {
var self = this;
// Disable Backup button until user provides Filename
this.__internal.buttons[0].element.disabled = true;
var $container = $("<div class='backup_dialog'></div>");
// Find current/selected node
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;
// Create treeInfo
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
// Instance of backbone model
var newModel = new BackupModel(
{type: of_type}, {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
});
// Add our class to alertify
$(this.elements.body.childNodes[0]).addClass(
'alertify_tools_dialog_properties obj_properties'
);
// Render dialog
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.text === '{{ _('Backup') }}') {
// Fetch current server id
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 self = this,
baseUrl = "{{ url_for('backup.index') }}" +
"create_job/" + treeInfo.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('{{ _('Background process for taking backup has been created!') }}', 1);
pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
}
},
error: function(xhr, status, error) {
try {
var err = $.parseJSON(xhr.responseText);
alertify.alert(
'{{ _('Backup failed...') }}',
err.errormsg
);
} catch (e) {}
}
});
}
}
};
});
}
alertify[DialogName](true).resizeTo('60%','50%');
},
// Callback to draw Backup Dialog for objects
backup_objects: function(action, treeItem) {
var title = S('{{ 'Backup (%s: %s)' }}'),
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;
title = title.sprintf(node.label, data.label).value();
if(!alertify.backup_objects) {
// Create Dialog title on the fly with node details
alertify.dialog('backup_objects' ,function factory() {
return {
main: function(title) {
this.set('title', title);
},
setup:function() {
return {
buttons: [{
text: '{{ _('Backup') }}', key: 27, className: 'btn btn-primary fa fa-lg fa-save pg-alertify-button'
},{
text: '{{ _('Cancel') }}', key: 27, className: 'btn btn-danger fa fa-lg fa-times pg-alertify-button'
}],
// 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});
}
}
},
prepare: function() {
var self = this;
// Disable Backup button until user provides Filename
this.__internal.buttons[0].element.disabled = true;
var $container = $("<div class='backup_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 BackupObjectModel(
{}, {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.text === "Backup") {
// Fetch current server id
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]);
// Set current database into model
this.view.model.set('database', treeInfo.database.label);
// We will remove once object tree is implemented
// If selected node is Schema then add it in model
if(d._type == 'schema') {
var schemas = [];
schemas.push(d.label);
this.view.model.set('schemas', schemas);
}
// If selected node is Table then add it in model along with
// its schema
if(d._type == 'table') {
var tables = [],
selected_table = [];
selected_table.push(treeInfo.schema.label)
selected_table.push(d.label);
this.view.model.set('tables', selected_table);
}
var self = this,
baseUrl = "{{ url_for('backup.index') }}" +
"create_job/backup_object/" + treeInfo.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('{{ _('Background process for taking backup has been created!') }}', 1);
pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
}
},
error: function(xhr, status, error) {
try {
var err = $.parseJSON(xhr.responseText);
alertify.alert(
'{{ _('Backup failed...') }}',
err.errormsg
);
} catch (e) {}
}
});
}
}
};
});
}
alertify.backup_objects(title).resizeTo('65%','60%');
}
};
return pgBrowser.Backup;
});

View File

@ -264,7 +264,7 @@ class Preferences(object):
self.mid = module.id
if name in Preferences.modules:
m = Preferences.modules
m = Preferences.modules[name]
self.categories = m.categories
else:
Preferences.modules[name] = self