public inbox for [email protected]
help / color / mirror / Atom feedFrom: Murtuza Zabuawala <[email protected]>
To: pgadmin-hackers <[email protected]>
Subject: PATCH: Initiale backup utility [pgAdmin4]
Date: Tue, 3 May 2016 17:41:27 +0530
Message-ID: <CAKKotZT0Uz66ZSVmzXBRPbyVL1Zz3dJBvfWChOGYPvuw-6ioBw@mail.gmail.com> (raw)
List-Unsubscribe: <mailto:[email protected]?body=unsub%20pgadmin-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
view thread (3+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected]
Subject: Re: PATCH: Initiale backup utility [pgAdmin4]
In-Reply-To: <CAKKotZT0Uz66ZSVmzXBRPbyVL1Zz3dJBvfWChOGYPvuw-6ioBw@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox