Added support to download utility files at the client-side. Fixes #3318

This commit is contained in:
Rahul Shirsat
2020-10-23 16:14:55 +05:30
committed by Akshay Joshi
parent 7573fac29f
commit c2ad97d0ab
24 changed files with 1842 additions and 61 deletions

View File

@@ -22,7 +22,7 @@ from subprocess import Popen, PIPE
import logging
from pgadmin.utils import u_encode, file_quote, fs_encoding, \
get_complete_file_path
get_complete_file_path, get_storage_directory, IS_WIN
import pytz
from dateutil import parser
@@ -59,6 +59,48 @@ class IProcessDesc(object, metaclass=ABCMeta):
def details(self, cmd, args):
pass
@property
def current_storage_dir(self):
if config.SERVER_MODE:
file = self.bfile
try:
# check if file name is encoded with UTF-8
file = self.bfile.decode('utf-8')
except Exception as e:
str(e)
# do nothing if bfile is not encoded.
path = get_complete_file_path(file)
path = file if path is None else path
if IS_WIN:
path = os.path.realpath(path)
storage_directory = os.path.basename(get_storage_directory())
if storage_directory in path:
start = path.index(storage_directory)
end = start + (len(storage_directory))
last_dir = os.path.dirname(path[end:])
else:
last_dir = file
if IS_WIN:
if '\\' in last_dir:
if len(last_dir) == 1:
last_dir = last_dir.replace('\\', '\\\\')
else:
last_dir = last_dir.replace('\\', '/')
else:
last_dir = last_dir.replace('\\', '/')
return None if hasattr(self, 'is_import') and self.is_import \
else last_dir
return None
class BatchProcess(object):
def __init__(self, **kwargs):
@@ -543,8 +585,12 @@ class BatchProcess(object):
desc = loads(p.desc)
details = desc
type_desc = ''
current_storage_dir = None
if isinstance(desc, IProcessDesc):
from pgadmin.tools.backup import BackupMessage
from pgadmin.tools.import_export import IEMessage
args = []
args_csv = StringIO(
p.arguments.encode('utf-8')
@@ -555,9 +601,11 @@ class BatchProcess(object):
args = args + arg
details = desc.details(p.command, args)
type_desc = desc.type_desc
if isinstance(desc, (BackupMessage, IEMessage)):
current_storage_dir = desc.current_storage_dir
desc = desc.message
return desc, details, type_desc
return desc, details, type_desc, current_storage_dir
@staticmethod
def list():
@@ -584,7 +632,8 @@ class BatchProcess(object):
execution_time = BatchProcess.total_seconds(etime - stime)
desc, details, type_desc = BatchProcess._check_process_desc(p)
desc, details, type_desc, current_storage_dir = BatchProcess.\
_check_process_desc(p)
res.append({
'id': p.pid,
@@ -596,7 +645,8 @@ class BatchProcess(object):
'exit_code': p.exit_code,
'acknowledge': p.acknowledge,
'execution_time': execution_time,
'process_state': p.process_state
'process_state': p.process_state,
'current_storage_dir': current_storage_dir,
})
if changed:

View File

@@ -57,3 +57,7 @@ ol.pg-bg-process-logs {
.pg-bg-bgprocess:hover .bg-close {
opacity: 0.95;
}
.icon-storage-manager:before {
font-icon: url('../img/storage_manager.svg');
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<g>
<path d="M4.3,5.3h8.8V4c0-0.7-0.6-1.3-1.3-1.3H7.5L5.8,1H1.5C0.8,1,0.2,1.6,0.2,2.3v7.4L2,6.5C2.6,5.8,3.4,5.3,4.3,5.3z"/>
<path d="M7.5,8.9c0-1.2,0.7-2.3,1.6-2.8H4.3C3.7,6.1,3.1,6.4,2.8,7l-1.9,3.3c-0.2,0.4,0.1,1,0.6,1h7C7.8,10.7,7.5,9.8,7.5,8.9z"/>
<path d="M15,6.1h-2.6c1,0.6,1.6,1.6,1.6,2.8c0,0.3-0.1,0.6-0.1,0.9L15.5,7C15.8,6.7,15.5,6.1,15,6.1z"/>
<circle cx="10.8" cy="8.9" r="2.8"/>
</g>
<path d="M14,11.8l-1.2-0.3c-1.3,0.9-2.9,0.8-3.8,0l-1.2,0.3C7,12,6.5,12.7,6.5,13.5v0.7c0,0.4,0.3,0.9,0.9,0.9h2.3l1.1-2.4l1.3,2.4
h2.4c0.4,0,0.9-0.3,0.9-0.9v-0.8C15.2,12.6,14.8,11.9,14,11.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 944 B

View File

@@ -20,6 +20,8 @@ define('misc.bgprocess', [
return pgBrowser.BackgroundProcessObsorver;
}
var isServerMode = (function() { return pgAdmin.server_mode == 'True'; })();
var wcDocker = window.wcDocker;
var BGProcess = function(info, notify) {
@@ -61,6 +63,7 @@ define('misc.bgprocess', [
curr_status: null,
state: 0, // 0: NOT Started, 1: Started, 2: Finished, 3: Terminated
completed: false,
current_storage_dir: null,
id: info['id'],
type_desc: null,
@@ -161,6 +164,9 @@ define('misc.bgprocess', [
if ('process_state' in data)
self.state = data.process_state;
if ('current_storage_dir' in data)
self.current_storage_dir = data.current_storage_dir;
if ('out' in data) {
self.out = data.out && data.out.pos;
@@ -325,8 +331,8 @@ define('misc.bgprocess', [
</div>
<div class="pg-bg-etime my-auto mr-2"></div>
<div class="ml-auto">
<button class="btn btn-secondary pg-bg-more-details"><span class="fa fa-info-circle" role="img"></span>&nbsp;` + gettext('More details...') + `</button>
<button class="btn btn-danger bg-process-stop" disabled><span class="fa fa-times-circle" role="img"></span>&nbsp;` + gettext('Stop Process') + `</button>
<button class="btn btn-secondary pg-bg-more-details" title="More Details"><span class="fa fa-info-circle" role="img"></span>&nbsp;` + gettext('More details...') + `</button>
<button class="btn btn-danger bg-process-stop" disabled><span class="fa fa-times-circle" role="img" title="Stop the operation"></span>&nbsp;` + gettext('Stop Process') + `</button>
</div>
</div>
<div class="pg-bg-status py-1">
@@ -387,6 +393,7 @@ define('misc.bgprocess', [
var $status_bar = $(self.container.find('.pg-bg-status'));
$status_bar.html(self.curr_status);
var $btn_stop_process = $(self.container.find('.bg-process-stop'));
// Enable Stop Process button only when process is running
if (parseInt(self.state) === 1) {
$btn_stop_process.attr('disabled', false);
@@ -415,7 +422,27 @@ define('misc.bgprocess', [
$logs = container.find('.bg-process-watcher'),
$header = container.find('.bg-process-details'),
$footer = container.find('.bg-process-footer'),
$btn_stop_process = container.find('.bg-process-stop');
$btn_stop_process = container.find('.bg-process-stop'),
$btn_storage_manager = container.find('.bg-process-storage-manager');
if(self.current_storage_dir && isServerMode) { //for backup & exports with server mode, operate over storage manager
if($btn_storage_manager.length == 0) {
var str_storage_manager_btn = '<button id="bg-process-storage-manager" class="btn btn-secondary bg-process-storage-manager" title="Click to open file location" aria-label="Storage Manager" tabindex="0" disabled><span class="pg-font-icon icon-storage-manager" role="img"></span></button>&nbsp;';
container.find('.bg-process-details .bg-btn-section').prepend(str_storage_manager_btn);
$btn_storage_manager = container.find('.bg-process-storage-manager');
}
// Disable storage manager button only when process is running
if (parseInt(self.state) === 1) {
$btn_storage_manager.attr('disabled', true);
}
else {
$btn_storage_manager.attr('disabled', false);
}
// On Click event for storage manager button.
$btn_storage_manager.off('click').on('click', self.storage_manager.bind(this));
}
// Enable Stop Process button only when process is running
if (parseInt(self.state) === 1) {
@@ -526,6 +553,15 @@ define('misc.bgprocess', [
});
},
storage_manager: function() {
var self = this;
if(self.current_storage_dir) {
pgBrowser.Events.trigger(
'pgadmin:tools:storage_manager', self.current_storage_dir
);
}
},
});
_.extend(
@@ -634,7 +670,7 @@ define('misc.bgprocess', [
'<span>' + gettext('Start time') + ': <span class="bgprocess-start-time"></span>' +
'</span>'+
'</div>' +
'<div class="ml-auto">' +
'<div class="ml-auto bg-btn-section">' +
'<button type="button" class="btn btn-danger bg-process-stop" disabled><span class="fa fa-times-circle" role="img"></span>&nbsp;' + gettext('Stop Process') + '</button>' +
'</div>' +
'</div>' +

View File

@@ -23,7 +23,7 @@ from werkzeug.exceptions import InternalServerError
import simplejson as json
from flask import render_template, Response, session, request as req, \
url_for, current_app
url_for, current_app, send_from_directory
from flask_babelex import gettext
from flask_security import login_required
from pgadmin.utils import PgAdminModule
@@ -1214,11 +1214,16 @@ class Filemanager(object):
'attachment; filename=' + name
return resp
name = path.split('/')[-1]
content = open(orig_path, 'rb')
resp = Response(content)
resp.headers['Content-Disposition'] = 'attachment; filename=' + name
return resp
name = os.path.basename(path)
if orig_path and len(orig_path) > 0:
dir_path = os.path.dirname(orig_path)
else:
dir_path = os.path.dirname(path)
response = send_from_directory(dir_path, name, as_attachment=True)
response.headers["filename"] = name
return response
def permission(self, path=None, req=None):
the_dir = self.dir if self.dir is not None else ''

View File

@@ -9,6 +9,7 @@
import './select_dialogue';
import './create_dialogue';
import './storage_dialogue';
define('misc.file_manager', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
@@ -41,7 +42,10 @@ define('misc.file_manager', [
let dialogHeight = pgAdmin.Browser.stdH.calc(pgAdmin.Browser.stdH.lg);
if (params.dialog_type == 'create_file') {
Alertify.createModeDlg(params).resizeTo(dialogWidth, dialogHeight);
} else {
} else if(params.dialog_type == 'storage_dialog') {
Alertify.fileStorageDlg(params).resizeTo(dialogWidth, dialogHeight);
}
else {
Alertify.fileSelectionDlg(params).resizeTo(dialogWidth, dialogHeight);
}
},

View File

@@ -77,7 +77,8 @@ module.exports = Alertify.dialog('fileSelectionDlg', function() {
$($(self.elements.footer).find('.file_manager_ok')).trigger('click');
});
}, 200);
self.__internal.buttons[1].element.disabled = true;
if(self.__internal.buttons[1])
self.__internal.buttons[1].element.disabled = true;
},
setup: function() {
return {

View File

@@ -0,0 +1,45 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import Alertify from 'pgadmin.alertifyjs';
// Declare the Storage dialog
module.exports = Alertify.dialog('fileStorageDlg', function() {
// Dialog property
return {
settingUpdated: function(key, oldValue, newValue) {
if(key == 'message') {
this.setMessage(newValue);
}
},
setup: function() {
return {
buttons: [{
text: gettext('Cancel'),
key: 27,
className: 'btn btn-secondary fa fa-times pg-alertify-button',
}],
options: {
closableByDimmer: false,
maximizable: false,
closable: false,
movable: true,
padding: !1,
overflow: !1,
model: 0,
resizable: true,
pinnable: false,
modal: false,
autoReset: false,
},
};
},
};
}, true, 'fileSelectionDlg');

View File

@@ -23,6 +23,8 @@ define([
'sources/csrf', 'tablesorter', 'tablesorter-metric',
], function($, _, Alertify, gettext, url_for, Dropzone, pgAdmin, csrf) {
pgAdmin.Browser = pgAdmin.Browser || {};
/*---------------------------------------------------------
Define functions used for various operations
---------------------------------------------------------*/
@@ -179,14 +181,17 @@ define([
$('.file_manager').find('button.download').hide();
} else {
$('.file_manager').find('button.download').off().on('click', function() {
var path;
var path,
params = {};
params[pgAdmin.csrf_token_header] = pgAdmin.csrf_token;
if ($('.fileinfo').data('view') == 'grid') {
path = $('.fileinfo li.selected').find('.clip span').attr('data-alt');
window.open(pgAdmin.FileUtils.fileConnector + '?_=' + Date.now() + 'mode=download&path=' + path, '_blank');
} else {
path = $('.fileinfo').find('table#contents tbody tr.selected td:first-child').attr('title');
window.open(pgAdmin.FileUtils.fileConnector + '?_=' + Date.now() + 'mode=download&path=' + path, '_blank');
}
download_file(path);
});
}
};
@@ -1030,12 +1035,15 @@ define([
$('.file_manager_ok').removeClass('disabled');
$('.file_manager_ok').attr('disabled', false);
$('.file_manager button.delete, .file_manager button.rename').removeAttr(
$('.file_manager button.delete').removeAttr(
'disabled', 'disabled'
);
$('.file_manager button.download').attr(
'disabled', 'disabled'
);
$('.file_manager button.rename').attr(
'disabled', 'disabled'
);
// set selected folder name in breadcrums
$('.file_manager #uploader .input-path').hide();
$('.file_manager #uploader .show_selected_file').remove();
@@ -1078,7 +1086,8 @@ define([
$('.file_manager_ok').removeClass('disabled');
$('.file_manager_ok').attr('disabled', false);
$('.file_manager button.download').attr('disabled', 'disabled');
$('.file_manager button.delete, .file_manager button.rename').removeAttr('disabled');
$('.file_manager button.rename').attr('disabled', 'disabled');
$('.file_manager button.delete').removeAttr('disabled');
// set selected folder name in breadcrums
$('.file_manager #uploader .input-path').hide();
@@ -1168,6 +1177,60 @@ define([
is_protected == undefined;
};
// Download selected file
var download_file = function (path) {
var data = { 'path': path, 'mode': 'download' },
params = {};
params[pgAdmin.csrf_token_header] = pgAdmin.csrf_token;
$.ajax({
type: 'POST',
url: pgAdmin.FileUtils.fileConnector,
contentType: false,
headers: params,
xhrFields: {
responseType: 'blob',
},
cache: false,
data: JSON.stringify(data),
success: function (blob, status, xhr) {
// check for a filename
var filename = xhr.getResponseHeader('filename');
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
window.navigator.msSaveBlob(blob, filename);
} else {
var URL = window.URL || window.webkitURL;
var downloadUrl = URL.createObjectURL(blob);
if (filename) {
// use HTML5 a[download] attribute to specify filename
var a = document.createElement('a');
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location.href = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
} else {
window.location.href = downloadUrl;
}
setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
}
},
error: function (error) {
Alertify.error(error);
},
});
};
/*---------------------------------------------------------
Initialization - Entry point
---------------------------------------------------------*/
@@ -1446,12 +1509,13 @@ define([
) {
$('.file_manager_ok').removeClass('disabled');
$('.file_manager_ok').attr('disabled', false);
$('.file_manager button.delete, .file_manager button.rename').removeAttr(
$('.file_manager button.delete').removeAttr(
'disabled', 'disabled'
);
$('.file_manager button.download').attr(
'disabled', 'disabled'
);
$('.file_manager button.rename').attr('disabled', 'disabled');
// set selected folder name in breadcrums
$('.file_manager #uploader .input-path').hide();
$('.file_manager #uploader .show_selected_file').remove();