diff --git a/web/pgadmin/browser/server_groups/servers/types.py b/web/pgadmin/browser/server_groups/servers/types.py index 294844f..999aa05 100644 --- a/web/pgadmin/browser/server_groups/servers/types.py +++ b/web/pgadmin/browser/server_groups/servers/types.py @@ -7,13 +7,11 @@ # ########################################################################## -import six -from abc import ABCMeta, abstractmethod, abstractproperty from flask import render_template from flask.ext.babel import gettext -class ServerType: +class ServerType(object): """ Server Type @@ -72,5 +70,17 @@ class ServerType: 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' + + return None + + # Default Server Type ServerType('pg', gettext("PostgreSQL"), -1) diff --git a/web/pgadmin/static/css/overrides.css b/web/pgadmin/static/css/overrides.css index d37db64..d296a98 100755 --- a/web/pgadmin/static/css/overrides.css +++ b/web/pgadmin/static/css/overrides.css @@ -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; @@ -1156,3 +1156,25 @@ form[name="change_password_form"] .help-block { height: 32px; padding-left: 5px; } + +/* 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; +} diff --git a/web/pgadmin/tools/backup/__init__.py b/web/pgadmin/tools/backup/__init__.py new file mode 100644 index 0000000..b747771 --- /dev/null +++ b/web/pgadmin/tools/backup/__init__.py @@ -0,0 +1,465 @@ +########################################################################## +# +# 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 random import randint +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 = _('Utilities') + + 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 + + # Getter/Setter for preferences + def get_utility_dir_preference(self, stype): + if stype == 'pg': + return self.pg_utility_dir + elif stype == 'ppas': + return self.edb_utility_dir + return None + + def register_preferences(self): + # Register 'PG specific utility binary directory' preference + self.pg_utility_dir = self.preference.register( + 'options', 'pg_utilities_bin_dir', + _("PG bin path"), 'text', '/', + category_label=_('BIN path') + ) + + # Register 'EDB specific utility binary directory' preference + self.edb_utility_dir = self.preference.register( + 'options', 'edb_utilities_bin_dir', + _("EDB bin path"), 'text', '/', + category_label=_('BIN path') + ) + +# Create blueprint for BackupModule class +blueprint = BackupModule( + MODULE_NAME, __name__, static_url_path='' +) + + +class BACKUP(object): + """ + Constants defined for Backup utilities + """ + GLOBAL = 1 + SERVER = 2 + OBJECT = 3 + + +class BackupMessage(IProcessDesc): + """ + BackupMessage(IProcessDesc) + + Defines the message shown for the backup operation. + """ + def __init__(self, _type, _sid, _cmd, _args, **kwargs): + self.backup_type = _type + self.sid = _sid + self.cmd = _cmd + self.args = _args + + if 'database' in kwargs: + self.database = kwargs['database'] + + @property + def message(self): + # Fetch the server details like hostname, port, roles etc + server = Server.query.filter_by( + id=self.sid, user_id=current_user.id + ).first() + res = _('Backup') + + if self.backup_type == BACKUP.OBJECT: + pass + if self.backup_type == BACKUP.GLOBAL: + pass + elif self.backup_type == BACKUP.SERVER: + pass + else: + # It should never reach here. + return "Unknown backup type" + + return res + + @property + def details(self): + pass + + +@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 _format_qtIdents(data): + """ + We have to parse & format qtident words as it contains + "" & "." in database objects, if we do not remove them then + OS will fail to parse & execute arguments + + Args: + data: A string which contains command arguments + + Returns: + Escaped argument string + + Usage: + >>> print(_format_qtIdents(r'""Postgres".table"')) + "\"Postgres\".table" + >>> print(_format_qtIdents(r'"postgres."Table""')) + "postgres.\"Table\"" + >>> print(_format_qtIdents(r'""Postgres"."Table""')) + "\"Postgres\".\"Table\"" + >>> print(_format_qtIdents(r'"postgres.table"')) + "postgres.table" + """ + + import re + # Case-1 ""Postgres + matchObj1 = re.search(r'^""', data) + if matchObj1: + data = re.sub(r'^""', r'"\\"', data) + + # Case-2 STR"."TBL + matchObj2 = re.search(r'[^"]"\\."[^"]', data) + if matchObj2: + data = re.sub(r'([^"])"\\."([^"])', r'\1\\".\\"\2', data) + else: + # Case-3 "STR".tbl + matchObj3 = re.search(r'"\.', data) + if matchObj3: + data = re.sub(r'"\.', r'\\".', data) + + # Case-4 str."TBL" + matchObj4 = re.search(r'\."', data) + if matchObj4: + data = re.sub(r'\."', r'.\\"', data) + + # Case-5 TBL"" + matchObj5 = re.search(r'""$', data) + if matchObj5: + data = re.sub(r'""$', r'\\""', data) + + return data + + +def utility_with_bin_path(server_manager, operation): + """ + Args: + server_manager: Server Manager + backup_type: Type of backup (Server/Objects) + + Returns: + Utility to use for backup with full path taken from preference + """ + # Set file manager directory from preference + + return os.path.join( + blueprint.get_utility_dir_preference( + server_manager.server_type + ).get(), + server_manager.utility(operation) + ) + + +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/', 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 = utility_with_bin_path(manager, 'backup_server') + + args = [ + '--host', + server.host, + '--port', + str(server.port), + '--username', + server.username, + '--no-password', + '--database', + driver.qtIdent(conn, server.maintenance_db), + '--file', + data['file'] + ] + 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='Backup', 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/', 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 = utility_with_bin_path(manager, 'backup') + args = [ + '--host', + server.host, + '--port', + str(server.port), + '--username', + server.username, + '--no-password', + '--file', + data['file'] + ] + + 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, server.maintenance_db)) + + try: + p = BatchProcess(desc='Backup', 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} + ) diff --git a/web/pgadmin/tools/backup/templates/backup/js/backup.js b/web/pgadmin/tools/backup/templates/backup/js/backup.js new file mode 100644 index 0000000..36956fe --- /dev/null +++ b/web/pgadmin/tools/backup/templates/backup/js/backup.js @@ -0,0 +1,636 @@ +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; + } + + var CustomSwitchControl = Backform.CustomSwitchControl = Backform.SwitchControl.extend({ + template: _.template([ + '', + '
', + '
', + ' ', + '
', + '
', + '<% if (helpMessage && helpMessage.length) { %>', + ' <%=helpMessage%>', + '<% } %>' + ].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') }}' + }] + },{ + id: 'todo', label: '{{ _('TODO') }}', text: '{{ _('Add Objects selection tree here') }}', + type: 'note', group: '{{ _('Objects') }}' + }], + 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 = $("
"); + // 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 = $("
"); + 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); + + 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; + }); diff --git a/web/pgadmin/utils/driver/psycopg2/__init__.py b/web/pgadmin/utils/driver/psycopg2/__init__.py index 208b52f..9dcf96b 100644 --- a/web/pgadmin/utils/driver/psycopg2/__init__.py +++ b/web/pgadmin/utils/driver/psycopg2/__init__.py @@ -335,6 +335,7 @@ WHERE for st in kwargs['server_types']: if st.instanceOf(mgr.ver): mgr.server_type = st.stype + mgr.server_cls = st break mgr.update_session() @@ -1017,6 +1018,7 @@ class ServerManager(object): self.ver = None self.sversion = None self.server_type = None + self.server_cls = None self.password = None self.sid = server.id @@ -1188,6 +1190,7 @@ WHERE db.oid = {0}""".format(did)) self.ver = None self.sversion = None self.server_type = None + self.server_cls = None self.password = None self.update_session() @@ -1203,6 +1206,7 @@ WHERE db.oid = {0}""".format(did)) self.ver = None self.sversion = None self.server_type = None + self.server_cls = None self.password = None self.update_session() @@ -1221,6 +1225,18 @@ WHERE db.oid = {0}""".format(did)) managers[self.sid] = updated_mgr session['__pgsql_server_managers'] = managers + def utility(self, operation): + """ + utility(operation) + + Returns: name of the utility which used for the operation + """ + if self.server_cls is not None: + return self.server_cls.utility(operation, self.sversion) + + return None + + class Driver(BaseDriver): """