diff --git a/web/pgadmin/static/css/overrides.css b/web/pgadmin/static/css/overrides.css index 1927ff1..a909964 100755 --- a/web/pgadmin/static/css/overrides.css +++ b/web/pgadmin/static/css/overrides.css @@ -1101,3 +1101,25 @@ button.pg-alertify-button { div.backform_control_notes label.control-label { min-width: 0px; } + +/* 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..f6d56ee --- /dev/null +++ b/web/pgadmin/tools/backup/__init__.py @@ -0,0 +1,358 @@ +########################################################################## +# +# 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, session +from flask.ext.babel import gettext +from pgadmin.utils.ajax import make_response as ajax_response, \ + make_json_response, internal_server_error, bad_request +from pgadmin.utils import PgAdminModule +from flask.ext.security import login_required +from pgadmin.model import db, Jobs, Server +from config import PG_DEFAULT_DRIVER, UTILITIES +from pgadmin.utils.preferences import Preferences + +# 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 = gettext('Binary Paths') + """ + + LABEL = gettext('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_pg_utility_dir_preference(self): + return self.pg_utility_dir + + def get_edb_utility_dir_preference(self): + return self.edb_utility_dir + + def register_preferences(self): + """ + Get storage directory preference + """ + self.storage_directory = Preferences.module('file_manager') + self.storage_dir = self.storage_directory.preference( + 'storage_dir' + ) + + # Register 'PG specific utility binary directory' preference + self.pg_utility_dir = self.preference.register( + 'options', 'pg_utilities_bin_dir', + gettext("PG bin path"), 'text', '/', + category_label=gettext('BIN path') + ) + + # Register 'EDB specific utility binary directory' preference + self.edb_utility_dir = self.preference.register( + 'options', 'edb_utilities_bin_dir', + gettext("EDB bin path"), 'text', '/', + category_label=gettext('BIN path') + ) + +# Create blueprint for BackupModule class +blueprint = BackupModule( + MODULE_NAME, __name__, static_url_path='') + + +@blueprint.route("/") +@login_required +def index(): + return bad_request(errormsg=gettext("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", _=gettext), + 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_type, backup_type): + """ + Args: + server_type: Server type (PG/PPAS) + backup_type: Type of backup (Server/Objects) + + Returns: + Utility to use for backup with full path taken from preference + """ + if server_type == 'ppas': + # Set file manager directory from preference + edb_utility_dir = blueprint.get_edb_utility_dir_preference().get() + if backup_type == 'server': # For Server/Globals Backup + return os.path.join(edb_utility_dir, UTILITIES['EDB_BACKUP_SERVER']) + else: # For Database/Schema/Table Backup + return os.path.join(edb_utility_dir, UTILITIES['EDB_BACKUP_OBJECT']) + elif server_type == 'pg': + # Set file manager directory from preference + pg_utility_dir = blueprint.get_pg_utility_dir_preference().get() + if backup_type == 'server': + return os.path.join(pg_utility_dir, UTILITIES['PG_BACKUP_SERVER']) + else: + return os.path.join(pg_utility_dir, UTILITIES['PG_BACKUP_OBJECT']) + + +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 = blueprint.storage_dir.get() + 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).first() + + if server is None: + return make_json_response( + success=0, + errormsg=gettext("Couldn't find the given server") + ) + + # To fetch MetaData for the server + from pgadmin.utils.driver import get_driver + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(server.id) + conn = manager.connection() + connected = conn.connected() + + if not connected: + return make_json_response( + success=0, + errormsg=gettext("Please connect to the server first...") + ) + + utility = utility_with_bin_path(manager.server_type, 'server') + + # Fetch args from template + arguments = render_template( + 'arguments/backup_server.args', + server=server, + data=data + ) + + arguments = _format_qtIdents(arguments) + + try: + # Generate random job id + jid = randint(101, 999) + create_job = Jobs( + job_id=jid, + command=utility, + arguments=arguments + ) + # Save it + db.session.add(create_job) + db.session.commit() + + 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).first() + + if server is None: + return make_json_response( + success=0, + errormsg=gettext("Couldn't find the given server") + ) + + # To fetch MetaData for the server + from pgadmin.utils.driver import get_driver + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(server.id) + conn = manager.connection() + connected = conn.connected() + + if not connected: + return make_json_response( + success=0, + errormsg=gettext("Please connect to the server first...") + ) + + utility = utility_with_bin_path(manager.server_type, 'object') + + # Fetch args from template + arguments = render_template( + 'arguments/backup_objects.args', + server=server, + data=data, + conn=conn + ) + + arguments = _format_qtIdents(arguments) + + try: + # Generate random job id + jid = randint(1001, 9999) + create_job = Jobs( + job_id=jid, + command=utility, + arguments=arguments + ) + # Save it + db.session.add(create_job) + db.session.commit() + + 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/arguments/backup_objects.args b/web/pgadmin/tools/backup/templates/arguments/backup_objects.args new file mode 100644 index 0000000..31e3b13 --- /dev/null +++ b/web/pgadmin/tools/backup/templates/arguments/backup_objects.args @@ -0,0 +1,32 @@ +--host {{server.host}} --port {{server.port}} --username "{{conn|qtIdent(server.username)}}" --role {% if data.role %} +"{{ conn|qtIdent(data.role) }}" {% else %}"{{ conn|qtIdent(server.role) }}" {% endif %}--no-password {% if data.verbose %} +--verbose {% endif %}{% if data.dqoute %} +--quote-all-identifiers {% endif %}{% if data.file %} +--file "{{data.file}}" {% endif %}{% if data.format and data.format == 'Custom' %} +--format custom {% if data.blobs %} +--blobs {% endif %}{% if data.ratio %} +--compress {{data.ratio}} {% endif %}{% elif data.format and data.format == 'Tar'%} +--format tar {% if data.blobs %} +--blobs {% endif %}{% elif data.format and data.format == 'Plain'%} +--format plain {% if data.only_data %} +--data-only {% if data.disable_trigger %} +--disable-triggers {% endif %}{% else %} +{% if data.only_schema %}--schema-only {% endif %}{% if data.dns_owner %} +--no-owner {% endif %}{% if data.include_create_database %} +--create {% endif %}{% if data.include_drop_database %} +--clean {% endif %}{% endif %}{% elif data.format and data.format == 'Directory'%} +--format directory {% endif %}{% if data.pre_data %} +--section pre-data {% endif %}{% if data.data %} +--section data {% endif %}{% if data.post_data %} +--section post-data {% endif %}{% if data.dns_privilege %} +--no-privileges {% endif %}{% if data.dns_tablespace %} +--no-tablespaces {% endif %}{% if data.dns_unlogged_tbl_data %} +--no-unlogged-table-data {% endif %}{% if data.use_insert_commands %} +--inserts {% endif %}{% if data.use_column_inserts %} +--column-inserts {% endif %}{% if data.disable_quoting %} +--disable-dollar-quoting {% endif %}{% if data.with_oids %} +--oids {% endif %}{% if data.use_set_session_auth %} +--use-set-session-authorization {% endif %}{% if data.no_of_jobs %} +--jobs {{data.no_of_jobs}} {% endif %}{% if data.tables and data.tables|length > 0 %} +{% for s,t in data.tables %}--table "{{ conn|qtIdent(s, t) }}" {% endfor %}{% endif %}{% if data.schemas and data.schemas|length > 0 %} +{% for s in data.schemas %}--schema "{{ conn|qtIdent(s) }}" {% endfor %}{% endif %}"{{ conn|qtIdent(data.database) }}" \ No newline at end of file diff --git a/web/pgadmin/tools/backup/templates/arguments/backup_server.args b/web/pgadmin/tools/backup/templates/arguments/backup_server.args new file mode 100644 index 0000000..df3a7c7 --- /dev/null +++ b/web/pgadmin/tools/backup/templates/arguments/backup_server.args @@ -0,0 +1,6 @@ +--host {{server.host}} --port {{server.port}} --username "{{server.username}}" --role {% if data.role %} +"{{ data.role }}" {% else %}"{{ server.role }}" {% endif %}--no-password {% if data.verbose %} +--verbose {% endif %}{% if data.dqoute %} +--quote-all-identifiers {% endif %}{% if data.type == 'globals' %} +--database "postgres" --globals-only {% endif %}{% if data.file %} +--file "{{data.file}}"{% endif %} \ No newline at end of file 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..20d5eb9 --- /dev/null +++ b/web/pgadmin/tools/backup/templates/backup/js/backup.js @@ -0,0 +1,639 @@ +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: 'postgres', + 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 = + !_.isUndefined(params['globals']) && params['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) { + var msg = alertify.message('{{ _('Backup job created (Click for more details)') }}', 10); + msg.callback = function (isClicked) { + if(isClicked) + console.log('Show detailed logs >>' + res); + } + }, + 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) { + var msg = alertify.message('{{ _('Backup job created (Click for more details)') }}', 10); + msg.callback = function (isClicked) { + if(isClicked) + console.log('Show detailed logs >>' + res); + } + }, + 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; + }); \ No newline at end of file