diff --git a/web/pgadmin/tools/import/__init__.py b/web/pgadmin/tools/import/__init__.py
new file mode 100644
index 0000000..318f56f
--- /dev/null
+++ b/web/pgadmin/tools/import/__init__.py
@@ -0,0 +1,280 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2016, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""A blueprint module implementing the import and export functionality"""
+
+import json
+import os
+
+from flask import url_for, Response, render_template, request, current_app
+from flask.ext.babel import gettext as _
+from flask.ext.security import login_required, current_user
+
+from config import PG_DEFAULT_DRIVER
+from pgadmin.utils import PgAdminModule, get_storage_directory, html
+from pgadmin.utils.ajax import make_json_response, bad_request
+from pgadmin.model import Server
+from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
+
+MODULE_NAME = 'import'
+
+
+class ImportExportModule(PgAdminModule):
+ """
+ class ImportExportModule(PgAdminModule)
+
+ A module class for import which is derived from PgAdminModule.
+
+ Methods:
+ -------
+ * get_own_javascripts(self)
+ - Method is used to load the required javascript files for import module
+ """
+
+ LABEL = _('Import')
+
+ def get_own_javascripts(self):
+ scripts = list()
+ for name, script in [
+ ['pgadmin.tools.import', 'js/import']
+ ]:
+ scripts.append({
+ 'name': name,
+ 'path': url_for('import.index') + script,
+ 'when': None
+ })
+
+ return scripts
+
+
+blueprint = ImportExportModule(MODULE_NAME, __name__)
+
+
+class Message(IProcessDesc):
+ """
+ Message(IProcessDesc)
+
+ Defines the message shown for the Message operation.
+ """
+ def __init__(self, _sid, _schema, _tbl, _database, _storage):
+ self.sid = _sid
+ self.schema = _schema
+ self.table = _tbl
+ self.database = _database
+ self.storage = _storage
+
+ @property
+ def message(self):
+ # Fetch the server details like hostname, port, roles etc
+ s = Server.query.filter_by(
+ id=self.sid, user_id=current_user.id
+ ).first()
+
+ return _(
+ "Copying table data - '{0}.{1}' on database '{2}' and server ({3}{4})..."
+ ).format(
+ self.schema, self.table, self.database, s.host, s.port
+ )
+
+ def details(self, cmd, args):
+ # Fetch the server details like hostname, port, roles etc
+ s = Server.query.filter_by(
+ id=self.sid, user_id=current_user.id
+ ).first()
+
+ res = '
'
+ res += html.safe_str(
+ _(
+ "Copying table data '{0}.{1}' on database '{2}' for the server - '{3}'"
+ ).format(
+ self.schema, self.table, self.database,
+ "{0} ({1}:{2})".format(s.name, s.host, s.port)
+ )
+ )
+
+ res += '
'
+ res += html.safe_str(
+ _("Running command:")
+ )
+ res += '
'
+ res += html.safe_str(cmd)
+
+ replace_next = False
+
+ def cmdArg(x):
+ if x:
+ x = x.replace('\\', '\\\\')
+ x = x.replace('"', '\\"')
+ x = x.replace('""', '\\"')
+
+ return ' "' + html.safe_str(x) + '"'
+
+ return ''
+
+ for arg in args:
+ if arg and len(arg) >= 2 and arg[:2] == '--':
+ res += ' ' + arg
+ elif replace_next:
+ if self.storage:
+ arg = arg.replace(self.storage, '')
+ res += ' "' + html.safe_str(arg) + '"'
+ else:
+ if arg == '--command':
+ replace_next = True
+ res += cmdArg(arg)
+ res += '
'
+
+ return res
+
+
+@blueprint.route("/")
+@login_required
+def index():
+ return bad_request(errormsg=_("This URL can not be called directly!"))
+
+
+@blueprint.route("/js/import.js")
+@login_required
+def script():
+ """render the import javascript file"""
+ return Response(response=render_template("import/js/import.js", _=_),
+ status=200,
+ mimetype="application/javascript")
+
+
+@blueprint.route('/create_job/', methods=['POST'])
+@login_required
+def create_import_job(sid):
+ """
+ Args:
+ sid: Server ID
+
+ Creates a new job for import and export table data functionality
+
+ 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())
+
+ # 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=_("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...")
+ )
+
+ # Get the utility path from the connection manager
+ utility = manager.utility('sql')
+
+ # Get the storage path from preference
+ storage_dir = get_storage_directory()
+ if 'filename' in data:
+ if os.name == 'nt':
+ data['filename'] = data['filename'].replace('/', '\\')
+ if storage_dir:
+ storage_dir = storage_dir.replace('/', '\\')
+ data['filename'] = data['filename'].replace('\\', '\\\\')
+ data['filename'] = os.path.join(storage_dir, data['filename'])
+ else:
+ data['filename'] = os.path.join(storage_dir, data['filename'])
+ else:
+ return make_json_response(
+ data={'status': False, 'info': 'Please specify a valid file'}
+ )
+
+ ignore_column_list = ''
+ column_list_import = ''
+
+ if data['ignore_column']:
+ new_ignore_col_list = json.loads(data['ignore_column'])
+
+ # format the ignore column list required as per copy command
+ # requirement
+ if new_ignore_col_list:
+ ignore_column_list = '('
+ ignore_col_length = len(new_ignore_col_list)
+ for i in range(ignore_col_length):
+ ignore_column_list += new_ignore_col_list[i]
+ if i != (ignore_col_length - 1):
+ ignore_column_list += ','
+ ignore_column_list += ')'
+
+ # format the column import list required as per copy command requirement
+ if data['column_import']:
+ column_list_import = '('
+ import_col_length = len(data['column_import'])
+ for indexCnt in range(import_col_length):
+ column_list_import += data['column_import'][indexCnt]
+ if indexCnt != (import_col_length - 1):
+ column_list_import += ','
+ column_list_import += ')'
+
+ # Fetch arguments from template
+ arguments = render_template(
+ 'import/arguments/import.args',
+ conn=conn,
+ data=data,
+ column_list_import=column_list_import,
+ ignore_column_list=ignore_column_list
+ )
+
+ args = [
+ '--host', server.host, '--port', str(server.port),
+ '--username', server.username, '--dbname',
+ driver.qtIdent(conn, data['database']),
+ '--command', arguments
+ ]
+
+ try:
+ p = BatchProcess(
+ desc=Message(
+ sid,
+ data['schema'],
+ data['table'],
+ data['database'],
+ storage_dir
+ ),
+ cmd=utility, args=args
+ )
+ manager.export_password_env(p.id)
+ 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/import/templates/import/arguments/import.args b/web/pgadmin/tools/import/templates/import/arguments/import.args
new file mode 100644
index 0000000..355b019
--- /dev/null
+++ b/web/pgadmin/tools/import/templates/import/arguments/import.args
@@ -0,0 +1 @@
+\copy {{ conn|qtIdent(data.schema, data.table) }} {% if column_list_import %} {{ column_list_import }} {% endif %} {% if data.is_import %}FROM{% else %}TO{% endif %} {{ data.filename|qtLiteral }} {% if data.oid %} OIDS {% endif %}{% if data.delimiter and data.delimiter == '[tab]' %} DELIMITER E'\\t' {% elif data.delimiter %} DELIMITER {{ data.delimiter|qtLiteral }}{% endif %}{% if data.format == 'csv' %} CSV HEADER {% endif %}{% if data.encoding %} ENCODING {{ data.encoding|qtLiteral }}{% endif %}{% if data.quote %} QUOTE {{ data.quote|qtLiteral }}{% endif %}{% if data.null_string %} NULL {{ data.null_string|qtLiteral }}{% endif %}{% if data.escape %} ESCAPE {{ data.escape|qtLiteral }}{% endif %}{% if data.format == 'csv' and ignore_column_list %} FORCE_NOT_NULL {{ ignore_column_list }} {% endif %};
diff --git a/web/pgadmin/tools/import/templates/import/js/import.js b/web/pgadmin/tools/import/templates/import/js/import.js
new file mode 100644
index 0000000..2d288f2
--- /dev/null
+++ b/web/pgadmin/tools/import/templates/import/js/import.js
@@ -0,0 +1,418 @@
+define(
+ ['jquery', 'underscore', 'underscore.string', 'alertify', 'pgadmin',
+ 'pgadmin.browser', 'backbone', 'backgrid', 'backform',
+ 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui'],
+ function($, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid, Backform) {
+
+ pgAdmin = pgAdmin || window.pgAdmin || {};
+
+ var pgTools = pgAdmin.Tools = pgAdmin.Tools || {};
+
+ // Return back, this has been called more than once
+ if (pgAdmin.Tools.import_utility)
+ return pgAdmin.Tools.import_utility;
+
+ // Main model for Import/Export functionality
+ var ImportExportModel = Backbone.Model.extend({
+ defaults: {
+ is_import: true, /* false for Export */
+ filename: undefined,
+ format: 'text',
+ encoding: undefined,
+ oid: undefined,
+ header: undefined,
+ delimiter: undefined,
+ quote: undefined,
+ escape: undefined,
+ null_string: undefined,
+ column_import: [],
+ ignore_column: [],
+ database: undefined,
+ schema: undefined,
+ table: undefined
+ },
+ schema: [{
+ id: 'is_import', label:'{{ _('Import/Export') }}', cell: 'switch',
+ type: 'switch', group: '{{ _('Files')}}',
+ options: {
+ 'onText': '{{ _('Import') }}', 'offText': '{{ _('Export') }}',
+ 'onColor': 'success', 'offColor': 'primary'
+ }
+ }, { /* select file control for import */
+ id: 'filename', label: '{{ _('Filename')}}', deps: ['is_import'],
+ type: 'text', control: Backform.FileControl, group: '{{ _('Files')}}',
+ dialog_type: 'select_file', supp_types: ['csv', 'txt', '*'],
+ visible: 'isSelectVisible'
+ }, { /* create file control for export */
+ id: 'filename', label: '{{ _('Filename')}}', deps: ['is_import'],
+ type: 'text', control: Backform.FileControl, group: '{{ _('Files')}}',
+ dialog_type: 'create_file', supp_types: ['csv', 'txt', '*'],
+ visible: 'isCreateVisible'
+ },
+ {
+ id: 'format', label: '{{ _("Format") }}', cell: 'string',
+ control: 'select2', group: '{{ _('Files')}}',
+ options:[
+ {'label': 'text', 'value': 'text'},
+ {'label': 'csv', 'value': 'csv'},
+ {'label': 'binary', 'value': 'binary'},
+ ],
+ disabled: 'isDisabled', select2: {allowClear: false, width: "100%" },
+ },
+ {
+ id: 'encoding', label: '{{ _("Encoding") }}', cell: 'string',
+ control: 'node-ajax-options', node: 'database', url: 'get_encodings', first_empty: true,
+ group: '{{ _('Files')}}'
+ },
+ {
+ id: 'column_import', label: '{{ _("Columns to/for import/export") }}', cell: 'string',
+ type: 'array', control: Backform.MultiSelectAjaxControl.extend({
+ // By default, all the import columns should be selected
+ initialize: function() {
+ Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments);
+ var self = this,
+ options = self.field.get('options'),
+ op_vals = [];
+ if (_.isFunction(options)) {
+ try {
+ options = options.apply(self)
+ } catch(e) {
+ // Do nothing
+ options = [];
+ }
+ }
+ _.each(options, function(op){
+ op_vals.push(op['value']);
+ });
+
+ self.model.set(self.field.get('name'),op_vals);
+ }
+ }),
+ node: 'column', url: 'nodes', group: '{{ _('Columns')}}',
+ transform: function(rows) {
+ var self = this,
+ node = self.field.get('schema_node'),
+ res = [];
+
+ _.each(rows, function(r) {
+ var l = (_.isFunction(node['node_label']) ?
+ (node['node_label']).apply(node, [r, self.model, self]) :
+ r.label),
+ image = (_.isFunction(node['node_image']) ?
+ (node['node_image']).apply(
+ node, [r, self.model, self]
+ ) :
+ (node['node_image'] || ('icon-' + node.type)));
+ res.push({
+ 'value': r.label,
+ 'image': image,
+ 'label': l
+ });
+ });
+
+ return res;
+ },
+ select2: { multiple: true, allowClear: true, placeholder: '{{ _('Select columns to import...') }}'},
+ },
+ {
+ id: 'null_string', label: '{{ _("NULL Strings") }}', cell: 'string',
+ type: 'text', group: '{{ _('Columns')}}', disabled: 'isDisabled', deps: ['format']
+ },
+ {
+ id: 'ignore_column', label: '{{ _("Ignore Columns") }}', cell: 'string',
+ control: 'node-list-by-name', node: 'column',
+ group: '{{ _('Columns')}}', deps: ['format'], disabled: 'isDisabled',
+ select2: { multiple: true, allowClear: true, placeholder: '{{ _('Select columns to ignore...') }}'},
+ },
+ {
+ type: 'nested', control: 'fieldset', label: '{{ _('Miscellaneous') }}',
+ group: '{{ _('Options') }}',
+ schema:[{
+ id: 'oid', label:'{{ _('OID') }}', cell: 'string',
+ type: 'switch', group: '{{ _('Miscellaneous') }}'
+ },{
+ id: 'header', label:'{{ _('Header') }}', cell: 'string',
+ type: 'switch', group: '{{ _('Miscellaneous') }}', deps: ['format'], disabled: 'isDisabled'
+ },{
+ id: 'delimiter', label:'{{ _('Delimiter') }}', cell: 'string', first_empty: true, deps: ['format'],
+ type: 'text', control: 'node-ajax-options', group: '{{ _('Miscellaneous') }}', disabled: 'isDisabled',
+ options:[
+
+ {'label': ';', 'value': ';'},
+ {'label': ',', 'value': ','},
+ {'label': '|', 'value': '|'},
+ {'label': '[tab]', 'value': '[tab]'},
+
+ ],
+ select2: {
+ allowClear: false,
+ width: "100%",
+ placeholder: '{{ _('Select from list...') }}'
+ },
+ }]
+ },
+ {
+ type: 'nested', control: 'fieldset', label: '{{ _('Quote') }}',
+ group: '{{ _('Options') }}',
+ schema:[{
+ id: 'quote', label:'{{ _('Quote') }}', cell: 'string',
+ type: 'text', control: 'node-ajax-options', group: '{{ _('Quote') }}',
+ disabled: 'isDisabled', deps: ['format'], first_empty: true,
+ options:[
+ {'label': '\"', 'value': '\"'},
+ {'label': '\'', 'value': '\''},
+ ],
+ select2: {
+ allowClear: false,
+ width: "100%",
+ placeholder: '{{ _('Select from list...') }}'
+ },
+ },{
+ id: 'escape', label:'{{ _('Escape') }}', cell: 'string',
+ type: 'text', control: 'node-ajax-options', group: '{{ _('Quote') }}',
+ disabled: 'isDisabled', deps: ['format'], first_empty: true,
+ options:[
+ {'label': '\"', 'value': '\"'},
+ {'label': '\'', 'value': '\''},
+ ],
+ select2: {
+ allowClear: false,
+ width: "100%",
+ placeholder: '{{ _('Select from list...') }}'
+ },
+ }]
+ }
+ ],
+
+ // Enable/Disable the items based on the user file format selection
+ isDisabled: function(m) {
+ name = this.name;
+ switch(name) {
+ case 'quote':
+ case 'escape':
+ case 'header':
+ case 'ignore_column':
+ if (m.get('format') != 'csv') {
+ return true;
+ }
+ else {
+ return false;
+ }
+ break;
+ case 'null_string':
+ case 'delimiter':
+ if (m.get('format') == 'binary') {
+ return true;
+ }
+ else {
+ return false;
+ }
+ break;
+ default:
+ return false;
+ }
+ return false;
+ },
+ isSelectVisible: function(m) {
+ name = this.name;
+ switch(name) {
+ case 'filename':
+ if (m.get('is_import')) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ break;
+ default:
+ return false;
+ }
+ return false;
+ },
+ isCreateVisible: function(m) {
+ name = this.name;
+ switch(name) {
+ case 'filename':
+ if (m.get('is_import')) {
+ return false;
+ }
+ else {
+ return true;
+ }
+ default:
+ return false;
+ }
+ return false;
+ },
+ });
+
+ pgTools.import_utility = {
+ init: function() {
+ // We do not want to initialize the module multiple times.
+ if (this.initialized)
+ return;
+
+ this.initialized = true;
+
+ /**
+ Enable/disable import menu in tools based on node selected
+ Import menu will be enabled only when user select table node.
+ */
+ 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(['table'], d._type) !== -1 &&
+ parent_data._type != 'catalog') ? true: false
+ );
+ else
+ return false;
+ };
+
+ // Initialize the context menu to display the import options when user open the context menu for table
+ pgBrowser.add_menus([{
+ name: 'import', node: 'table', module: this,
+ applies: ['tools', 'context'], callback: 'callback_import',
+ category: 'import', priority: 10, label: '{{ _('Import...') }}',
+ data: {object: 'table', type: 'import'}, icon: 'fa fa-sign-in', enable: menu_enabled
+ },{
+ name: 'export', node: 'table', module: this,
+ applies: ['tools', 'context'], callback: 'callback_import',
+ category: 'export', priority: 11, label: '{{ _('Export...') }}',
+ data: {object: 'table', type: 'export'}, icon: 'fa fa-sign-out', enable: menu_enabled
+ }
+ ]);
+ },
+
+ /*
+ Open the dialog for the import functionality
+ */
+ callback_import: function(args, item) {
+ var self = this;
+ var input = args || {},
+ t = pgBrowser.tree,
+ i = item || t.selected(),
+ d = i && i.length == 1 ? t.itemData(i) : undefined,
+ node = d && pgBrowser.Nodes[d._type];
+
+ if (!d)
+ return;
+
+ var objName = d.label;
+ var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]);
+ var isImport = (args.type == "import") ? true: false;
+
+ if (!Alertify.ImportDialog) {
+ Alertify.dialog('ImportDialog', function factory() {
+
+ return {
+ main:function(title, pg_import, node, item, data) {
+ this.set('title', title);
+ this.setting('pg_import', pg_import);
+ this.setting('pg_node', node);
+ this.setting('pg_item', item);
+ this.setting('pg_item_data', data);
+ },
+ setup:function() {
+ return {
+ buttons:[{ text: "{{ _('OK') }}", 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" }],
+ options: { modal: 0}
+ };
+ },
+ settings: {
+ pg_import: true,
+ pg_node: null,
+ pg_item: null,
+ pg_item_data: null
+ },
+ // Callback functions when click on the buttons of the Alertify dialogs
+ callback: function(e) {
+ if (e.button.text === "{{ _('OK') }}") {
+
+ var n = this.settings['pg_node'],
+ i = this.settings['pg_item'],
+ treeInfo = n.getTreeNodeHierarchy.apply(n, [i])
+
+ this.view.model.set({
+ 'database': treeInfo.database.label,
+ 'schema': treeInfo.schema.label,
+ 'table': treeInfo.table.label
+ });
+ var self = this,
+ baseUrl = "{{ url_for('import.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 import/export has been created!') }}', 1);
+ pgBrowser.Events.trigger('pgadmin-bgprocess:created', self);
+ }
+ },
+ error: function(xhr, status, error) {
+ try {
+ var err = $.parseJSON(xhr.responseText);
+ Alertify.alert(
+ '{{ _('Import failed...') }}',
+ err.errormsg
+ );
+ } catch (e) {}
+ }
+ });
+ }
+ },
+ hooks: {
+ onclose: function() {
+ if (this.view) {
+ this.view.remove({data: true, internal: true, silent: true});
+ }
+ }
+ },
+ prepare:function() {
+ // Main import module container
+ var self = this,
+ $container = $(""),
+ n = this.settings.pg_node,
+ i = this.settings.pg_item,
+ treeInfo = n.getTreeNodeHierarchy.apply(n, [i]),
+ newModel = new ImportExportModel ({
+ 'is_import': this.settings['pg_import']
+ }, {
+ node_info: treeInfo
+ }),
+ fields = Backform.generateViewSchema(
+ treeInfo, newModel, 'create', node, treeInfo.server, true
+ ),
+ 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));
+ }
+ };
+ });
+ }
+
+ // Open the Alertify dialog for the import/export module
+ Alertify.ImportDialog(
+ S(
+ "{{ _("Import/Export data to/from file/table - '%%s'") }}"
+ ).sprintf(treeInfo.table.label).value(), isImport, node, i, d
+ ).set('resizable',true).resizeTo('60%','70%');
+ }
+ };
+
+ return pgAdmin.Tools.import_utility;
+ });