public inbox for [email protected]help / color / mirror / Atom feed
PATCH: Initiale backup utility [pgAdmin4] 3+ messages / 2 participants [nested] [flat]
* PATCH: Initiale backup utility [pgAdmin4] @ 2016-05-03 12:11 Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 3+ messages in thread From: Murtuza Zabuawala @ 2016-05-03 12:11 UTC (permalink / raw) To: pgadmin-hackers Hi, PFA patch to add backup server/global/database object functionality. This patch depends on, - File manager control patch & job executer. *TODO:* - Integrate browser tree control -- Regards, Murtuza Zabuawala EnterpriseDB: http://www.enterprisedb.com The Enterprise PostgreSQL Company -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [application/octet-stream] Backup_Utility_v1.patch (40.7K, 3-Backup_Utility_v1.patch) download | inline diff: 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='') + + [email protected]("/") +@login_required +def index(): + return bad_request(errormsg=gettext("This URL can not be called directly!")) + + [email protected]("/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) + + [email protected]('/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).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} + ) + + [email protected]('/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).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([ + '<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: '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 = $("<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) { + 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 = $("<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); + + 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 ^ permalink raw reply [nested|flat] 3+ messages in thread
* Re: PATCH: Initiale backup utility [pgAdmin4] @ 2016-05-13 12:09 Ashesh Vashi <[email protected]> parent: Murtuza Zabuawala <[email protected]> 0 siblings, 1 reply; 3+ messages in thread From: Ashesh Vashi @ 2016-05-13 12:09 UTC (permalink / raw) To: Murtuza Zabuawala <[email protected]>; +Cc: pgadmin-hackers On Tue, May 3, 2016 at 5:41 PM, Murtuza Zabuawala < [email protected]> wrote: > Hi, > > PFA patch to add backup server/global/database object functionality. > > This patch depends on, > - File manager control patch & job executer. > > > *TODO:* > - Integrate browser tree control > Put that in the TODO list. We will select the current object at the moment for backup at the moment. We will add object selection in later phase. Please take look at the patch. It needs to define the message class properly for better message. -- Thanks & Regards, Ashesh Vashi EnterpriseDB INDIA: Enterprise PostgreSQL Company <http://www.enterprisedb.com/; *http://www.linkedin.com/in/asheshvashi* <http://www.linkedin.com/in/asheshvashi; > > > -- > Regards, > Murtuza Zabuawala > EnterpriseDB: http://www.enterprisedb.com > The Enterprise PostgreSQL Company > > > -- > Sent via pgadmin-hackers mailing list ([email protected]) > To make changes to your subscription: > http://www.postgresql.org/mailpref/pgadmin-hackers > > -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [application/octet-stream] Backup_integrated_v1.patch (43.2K, 3-Backup_integrated_v1.patch) download | inline diff: 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 + + [email protected]("/") +@login_required +def index(): + return bad_request(errormsg=_("This URL can not be called directly!")) + + [email protected]("/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) + + [email protected]('/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 = 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} + ) + + [email protected]('/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 = 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([ + '<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') }}' + }] + },{ + 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 = $("<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); + + 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): """ ^ permalink raw reply [nested|flat] 3+ messages in thread
* Re: PATCH: Initiale backup utility [pgAdmin4] @ 2016-05-14 17:05 Murtuza Zabuawala <[email protected]> parent: Ashesh Vashi <[email protected]> 0 siblings, 0 replies; 3+ messages in thread From: Murtuza Zabuawala @ 2016-05-14 17:05 UTC (permalink / raw) To: Ashesh Vashi <[email protected]>; +Cc: pgadmin-hackers Hi, PFA patch for backup which is add-on to last patch which includes TODO list & node selection validation Regards, Murtuza -- Regards, Murtuza Zabuawala EnterpriseDB: http://www.enterprisedb.com The Enterprise PostgreSQL Company On Fri, May 13, 2016 at 5:39 PM, Ashesh Vashi <[email protected] > wrote: > On Tue, May 3, 2016 at 5:41 PM, Murtuza Zabuawala < > [email protected]> wrote: > >> Hi, >> >> PFA patch to add backup server/global/database object functionality. >> >> This patch depends on, >> - File manager control patch & job executer. >> >> >> *TODO:* >> - Integrate browser tree control >> > Put that in the TODO list. > > We will select the current object at the moment for backup at the moment. > > We will add object selection in later phase. > > Please take look at the patch. > It needs to define the message class properly for better message. > > -- > > Thanks & Regards, > > Ashesh Vashi > EnterpriseDB INDIA: Enterprise PostgreSQL Company > <http://www.enterprisedb.com/; > > > *http://www.linkedin.com/in/asheshvashi* > <http://www.linkedin.com/in/asheshvashi; > >> >> >> -- >> Regards, >> Murtuza Zabuawala >> EnterpriseDB: http://www.enterprisedb.com >> The Enterprise PostgreSQL Company >> >> >> -- >> Sent via pgadmin-hackers mailing list ([email protected]) >> To make changes to your subscription: >> http://www.postgresql.org/mailpref/pgadmin-hackers >> >> > -- Sent via pgadmin-hackers mailing list ([email protected]) To make changes to your subscription: http://www.postgresql.org/mailpref/pgadmin-hackers Attachments: [application/octet-stream] Added_selected_object.patch (2.3K, 3-Added_selected_object.patch) download | inline diff: diff --git a/web/pgadmin/tools/backup/templates/backup/js/backup.js b/web/pgadmin/tools/backup/templates/backup/js/backup.js index 36956fe..d8de0d7 100644 --- a/web/pgadmin/tools/backup/templates/backup/js/backup.js +++ b/web/pgadmin/tools/backup/templates/backup/js/backup.js @@ -11,6 +11,24 @@ define([ 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>', @@ -599,6 +617,23 @@ define([ // 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, ^ permalink raw reply [nested|flat] 3+ messages in thread
end of thread, other threads:[~2016-05-14 17:05 UTC | newest] Thread overview: 3+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2016-05-03 12:11 PATCH: Initiale backup utility [pgAdmin4] Murtuza Zabuawala <[email protected]> 2016-05-13 12:09 ` Ashesh Vashi <[email protected]> 2016-05-14 17:05 ` Murtuza Zabuawala <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox