Added support for mounting shared storage in server mode. #5014
parent
c35d449d7e
commit
9da8a188fa
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 124 KiB |
|
@ -7,6 +7,7 @@
|
|||
*Storage Manager* is a feature that helps you manage your systems storage device. You can use *Storage Manager* to:
|
||||
|
||||
* Download, upload, or manage operating system files. To use this feature, *pgAdmin* must be running in *Server Mode* on your client machine.
|
||||
* The shared storage option allows users to access the shared storages that are shared by admin users.
|
||||
* Download *backup* or *export* files (custom, tar and plain text format) on a client machine.
|
||||
* Download *export* dump files of tables.
|
||||
|
||||
|
@ -18,6 +19,12 @@ You can access *Storage Manager* from the *Tools* Menu.
|
|||
|
||||
Use icons on the top of the *Storage Manager* window to manage storage:
|
||||
|
||||
Use the ``Folder`` icon |Shared Storage| to access shared storage. In order to enable shared storage,
|
||||
admins need to add the SHARED_STORAGE variable to the config file. Users can access the shared storage
|
||||
with this and share files with one another.
|
||||
|
||||
.. |Shared Storage| image:: images/sm_ss.png
|
||||
|
||||
Use the ``Home`` icon |home| to return to the home directory.
|
||||
|
||||
.. |home| image:: images/sm_home.png
|
||||
|
@ -40,6 +47,17 @@ Use the ``New Folder`` icon |folder| to add a new folder.
|
|||
|
||||
Use the *Format* drop down list to select the format of the files to be displayed; choose from *sql*, *csv*, or *All Files*.
|
||||
|
||||
Shared Storage
|
||||
*********************
|
||||
.. image:: images/shared_storage.png
|
||||
:alt: Other options
|
||||
:align: center
|
||||
|
||||
In shared storage the ``My Storage`` is the user's storage directory, and other directories are shared storage set by
|
||||
the admin user. Using this shared storage users can share the files with other users through pgAdmin.
|
||||
Admin users can mark the shared storage as restricted to restrict non-admin users from deleting, uploading,
|
||||
adding, and renaming files/folders in shared storage by setting the restricted_access flag in config.
|
||||
|
||||
Other Options
|
||||
*********************
|
||||
|
||||
|
|
|
@ -832,6 +832,18 @@ ENABLE_PSQL = False
|
|||
##########################################################################
|
||||
ENABLE_BINARY_PATH_BROWSING = False
|
||||
|
||||
##########################################################################
|
||||
# In server mode, the SHARED_STORAGE setting is used to enable shared storage.
|
||||
# Specify the name, path, and restricted_access values that should be shared
|
||||
# between users. When restricted_access is set to True, non-admin users cannot
|
||||
# upload/add, delete, or rename files/folders in shared storage, only admins
|
||||
# can do that. Users must provide the absolute path to the folder, and the name
|
||||
# can be anything they see on the user interface.
|
||||
# [{ 'name': 'Shared 1', 'path': '/shared_folder',
|
||||
# 'restricted_access': True/False}]
|
||||
##########################################################################
|
||||
SHARED_STORAGE = []
|
||||
|
||||
#############################################################################
|
||||
# AUTO_DISCOVER_SERVERS setting is used to enable the pgAdmin to discover the
|
||||
# database server automatically on the local machine.
|
||||
|
|
|
@ -532,6 +532,37 @@ def index():
|
|||
return response
|
||||
|
||||
|
||||
def validate_shared_storage_config(data, shared_storage_keys):
|
||||
"""
|
||||
Validate the config values are correct or not
|
||||
"""
|
||||
if shared_storage_keys.issubset(data.keys()):
|
||||
if isinstance(data['name'], str) and isinstance(
|
||||
data['path'], str) and \
|
||||
isinstance(data['restricted_access'], bool):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_shared_storage_list():
|
||||
"""
|
||||
Return the shared storage list after checking all required keys are present
|
||||
or not in config. This is for server mode only.
|
||||
"""
|
||||
shared_storage_config = []
|
||||
shared_storage_list = []
|
||||
if config.SERVER_MODE:
|
||||
shared_storage_keys = set(['name', 'path', 'restricted_access'])
|
||||
shared_storage_config = [
|
||||
sdir for sdir in config.SHARED_STORAGE if
|
||||
validate_shared_storage_config(sdir, shared_storage_keys)]
|
||||
|
||||
config.SHARED_STORAGE = shared_storage_config
|
||||
shared_storage_list = [sdir['name'] for sdir in shared_storage_config]
|
||||
|
||||
return shared_storage_list
|
||||
|
||||
|
||||
@blueprint.route("/js/utils.js")
|
||||
@pgCSRFProtect.exempt
|
||||
@login_required
|
||||
|
@ -599,6 +630,8 @@ def utils():
|
|||
auth_source = session['auth_source_manager'][
|
||||
'source_friendly_name']
|
||||
|
||||
shared_storage_list = get_shared_storage_list()
|
||||
|
||||
return make_response(
|
||||
render_template(
|
||||
'browser/js/utils.js',
|
||||
|
@ -630,7 +663,8 @@ def utils():
|
|||
auth_source=auth_source,
|
||||
heartbeat_timeout=config.SERVER_HEARTBEAT_TIMEOUT,
|
||||
password_length_min=config.PASSWORD_LENGTH_MIN,
|
||||
current_ui_lock=current_ui_lock
|
||||
current_ui_lock=current_ui_lock,
|
||||
shared_storage_list=shared_storage_list,
|
||||
),
|
||||
200, {'Content-Type': MIMETYPE_APP_JS})
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ define('pgadmin.browser.utils',
|
|||
|
||||
/* GET PSQL Tool related config */
|
||||
pgAdmin['enable_psql'] = '{{enable_psql}}' == 'True';
|
||||
pgAdmin['shared_storage'] = {{shared_storage_list}}
|
||||
pgAdmin['platform'] = '{{platform}}';
|
||||
pgAdmin['qt_default_placeholder'] = '{{qt_default_placeholder}}'
|
||||
pgAdmin['vw_edt_default_placeholder'] = '{{vw_edt_default_placeholder}}'
|
||||
|
|
|
@ -16,6 +16,7 @@ import string
|
|||
import time
|
||||
from urllib.parse import unquote
|
||||
from sys import platform as _platform
|
||||
from flask_security import current_user
|
||||
import config
|
||||
import codecs
|
||||
import pathlib
|
||||
|
@ -30,7 +31,8 @@ from pgadmin.utils import get_storage_directory
|
|||
from pgadmin.utils.ajax import make_json_response, unauthorized, \
|
||||
internal_server_error
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
from pgadmin.utils.constants import PREF_LABEL_OPTIONS, MIMETYPE_APP_JS
|
||||
from pgadmin.utils.constants import PREF_LABEL_OPTIONS, MIMETYPE_APP_JS, \
|
||||
MY_STORAGE
|
||||
from pgadmin.settings.utils import get_file_type_setting
|
||||
|
||||
# Checks if platform is Windows
|
||||
|
@ -152,6 +154,12 @@ class FileManagerModule(PgAdminModule):
|
|||
gettext("Last directory visited"), 'text', '/',
|
||||
category_label=PREF_LABEL_OPTIONS
|
||||
)
|
||||
self.last_storage = self.preference.register(
|
||||
'options', 'last_storage',
|
||||
gettext("Last storage"), 'text', '',
|
||||
category_label=PREF_LABEL_OPTIONS,
|
||||
hidden=True
|
||||
)
|
||||
self.file_dialog_view = self.preference.register(
|
||||
'options', 'file_dialog_view',
|
||||
gettext("File dialog view"), 'select', 'list',
|
||||
|
@ -228,6 +236,7 @@ def init_filemanager():
|
|||
"platform_type": data['platform_type'],
|
||||
"show_volumes": data['show_volumes'],
|
||||
"homedir": data['homedir'],
|
||||
'storage_folder': data['storage_folder'],
|
||||
"last_selected_format": last_selected_format
|
||||
},
|
||||
"security": {
|
||||
|
@ -263,6 +272,7 @@ def delete_trans_id(trans_id):
|
|||
@login_required
|
||||
def save_last_directory_visited(trans_id):
|
||||
blueprint.last_directory_visited.set(req.json['path'])
|
||||
blueprint.last_storage.set(req.json['storage_folder'])
|
||||
return make_json_response(status=200)
|
||||
|
||||
|
||||
|
@ -297,9 +307,10 @@ class Filemanager():
|
|||
'Code': 0
|
||||
}
|
||||
|
||||
def __init__(self, trans_id):
|
||||
def __init__(self, trans_id, ss=''):
|
||||
self.trans_id = trans_id
|
||||
self.dir = get_storage_directory()
|
||||
self.sharedDir = get_storage_directory(shared_storage=ss)
|
||||
|
||||
if self.dir is not None and isinstance(self.dir, list):
|
||||
self.dir = ""
|
||||
|
@ -394,6 +405,20 @@ class Filemanager():
|
|||
if 'init_path' in params:
|
||||
blueprint.last_directory_visited.get(params['init_path'])
|
||||
last_dir = blueprint.last_directory_visited.get()
|
||||
last_ss_name = blueprint.last_storage.get()
|
||||
if last_ss_name and last_ss_name != MY_STORAGE \
|
||||
and len(config.SHARED_STORAGE) > 0:
|
||||
selectedDir = [sdir for sdir in config.SHARED_STORAGE if
|
||||
sdir['name'] == last_ss_name]
|
||||
last_ss = selectedDir[0]['path'] if len(
|
||||
selectedDir) == 1 else storage_dir
|
||||
else:
|
||||
if last_ss_name != MY_STORAGE:
|
||||
last_dir = '/'
|
||||
blueprint.last_storage.set(MY_STORAGE)
|
||||
|
||||
last_ss = storage_dir
|
||||
|
||||
check_dir_exists = False
|
||||
if last_dir is None:
|
||||
last_dir = "/"
|
||||
|
@ -404,12 +429,13 @@ class Filemanager():
|
|||
last_dir = homedir
|
||||
|
||||
if check_dir_exists:
|
||||
last_dir = Filemanager.get_closest_parent(storage_dir, last_dir)
|
||||
last_dir = Filemanager.get_closest_parent(last_ss, last_dir)
|
||||
|
||||
# create configs using above configs
|
||||
configs = {
|
||||
"fileroot": last_dir,
|
||||
"homedir": homedir,
|
||||
'storage_folder': last_ss_name,
|
||||
"dialog_type": fm_type,
|
||||
"title": title,
|
||||
"upload": {
|
||||
|
@ -743,7 +769,11 @@ class Filemanager():
|
|||
trans_data = Filemanager.get_trasaction_selection(self.trans_id)
|
||||
the_dir = None
|
||||
if config.SERVER_MODE:
|
||||
the_dir = self.dir
|
||||
if self.sharedDir and len(config.SHARED_STORAGE) > 0:
|
||||
the_dir = self.sharedDir
|
||||
else:
|
||||
the_dir = self.dir
|
||||
|
||||
if the_dir is not None and not the_dir.endswith('/'):
|
||||
the_dir += '/'
|
||||
|
||||
|
@ -751,6 +781,23 @@ class Filemanager():
|
|||
the_dir, path, trans_data, file_type, show_hidden)
|
||||
return filelist
|
||||
|
||||
def check_access(self, ss, mode):
|
||||
if self.sharedDir:
|
||||
selectedDirList = [sdir for sdir in config.SHARED_STORAGE if
|
||||
sdir['name'] == ss]
|
||||
selectedDir = selectedDirList[0] if len(
|
||||
selectedDirList) == 1 else None
|
||||
|
||||
if selectedDir:
|
||||
if selectedDir[
|
||||
'restricted_access'] and not current_user.has_role(
|
||||
"Administrator"):
|
||||
raise PermissionError(gettext(
|
||||
"Access denied: This shared folder has restricted "
|
||||
"access, you are not allowed to rename, delete, "
|
||||
"or upload any files/folders. Please contact "
|
||||
"Administrator to gain access."))
|
||||
|
||||
def rename(self, old=None, new=None):
|
||||
"""
|
||||
Rename file or folder
|
||||
|
@ -758,7 +805,10 @@ class Filemanager():
|
|||
if not self.validate_request('rename'):
|
||||
return unauthorized(self.ERROR_NOT_ALLOWED['Error'])
|
||||
|
||||
the_dir = self.dir if self.dir is not None else ''
|
||||
if self.sharedDir:
|
||||
the_dir = self.sharedDir
|
||||
else:
|
||||
the_dir = self.dir if self.dir is not None else ''
|
||||
|
||||
Filemanager.check_access_permission(the_dir, old)
|
||||
Filemanager.check_access_permission(the_dir, new)
|
||||
|
@ -801,8 +851,10 @@ class Filemanager():
|
|||
"""
|
||||
if not self.validate_request('delete'):
|
||||
return unauthorized(self.ERROR_NOT_ALLOWED['Error'])
|
||||
|
||||
the_dir = self.dir if self.dir is not None else ''
|
||||
if self.sharedDir:
|
||||
the_dir = self.sharedDir
|
||||
else:
|
||||
the_dir = self.dir if self.dir is not None else ''
|
||||
orig_path = "{0}{1}".format(the_dir, path)
|
||||
|
||||
Filemanager.check_access_permission(the_dir, path)
|
||||
|
@ -825,7 +877,10 @@ class Filemanager():
|
|||
if not self.validate_request('upload'):
|
||||
return unauthorized(self.ERROR_NOT_ALLOWED['Error'])
|
||||
|
||||
the_dir = self.dir if self.dir is not None else ''
|
||||
if self.sharedDir:
|
||||
the_dir = self.sharedDir
|
||||
else:
|
||||
the_dir = self.dir if self.dir is not None else ''
|
||||
|
||||
try:
|
||||
path = req.form.get('currentpath')
|
||||
|
@ -990,7 +1045,10 @@ class Filemanager():
|
|||
if not self.validate_request('create'):
|
||||
return unauthorized(self.ERROR_NOT_ALLOWED['Error'])
|
||||
|
||||
user_dir = self.dir if self.dir is not None else ''
|
||||
if self.sharedDir and len(config.SHARED_STORAGE) > 0:
|
||||
user_dir = self.sharedDir
|
||||
else:
|
||||
user_dir = self.dir if self.dir is not None else ''
|
||||
|
||||
Filemanager.check_access_permission(user_dir, "{}{}".format(
|
||||
path, name))
|
||||
|
@ -1018,7 +1076,11 @@ class Filemanager():
|
|||
if not self.validate_request('download'):
|
||||
return unauthorized(self.ERROR_NOT_ALLOWED['Error'])
|
||||
|
||||
the_dir = self.dir if self.dir is not None else ''
|
||||
if self.sharedDir and len(config.SHARED_STORAGE) > 0:
|
||||
the_dir = self.sharedDir
|
||||
else:
|
||||
the_dir = self.dir if self.dir is not None else ''
|
||||
|
||||
orig_path = "{0}{1}".format(the_dir, path)
|
||||
|
||||
Filemanager.check_access_permission(
|
||||
|
@ -1057,13 +1119,13 @@ def file_manager(trans_id):
|
|||
It gets unique transaction id from post request and
|
||||
rotate it into Filemanager class.
|
||||
"""
|
||||
my_fm = Filemanager(trans_id)
|
||||
mode = ''
|
||||
kwargs = {}
|
||||
if req.method == 'POST':
|
||||
if req.files:
|
||||
mode = 'add'
|
||||
kwargs = {'req': req}
|
||||
kwargs = {'req': req,
|
||||
'storage_folder': req.form.get('storage_folder', None)}
|
||||
else:
|
||||
kwargs = json.loads(req.data)
|
||||
kwargs['req'] = req
|
||||
|
@ -1075,14 +1137,23 @@ def file_manager(trans_id):
|
|||
'name': req.args['name'] if 'name' in req.args else ''
|
||||
}
|
||||
mode = req.args['mode']
|
||||
ss = kwargs['storage_folder'] if 'storage_folder' in kwargs else None
|
||||
my_fm = Filemanager(trans_id, ss)
|
||||
|
||||
if ss and mode in ['upload', 'rename', 'delete', 'addfolder', 'add']:
|
||||
my_fm.check_access(ss, mode)
|
||||
func = getattr(my_fm, mode)
|
||||
try:
|
||||
if mode in ['getfolder', 'download']:
|
||||
kwargs.pop('name', None)
|
||||
|
||||
if mode in ['add']:
|
||||
kwargs.pop('storage_folder', None)
|
||||
|
||||
if mode in ['addfolder', 'getfolder', 'rename', 'delete',
|
||||
'is_file_exist', 'req', 'permission', 'download']:
|
||||
kwargs.pop('req', None)
|
||||
kwargs.pop('storage_folder', None)
|
||||
|
||||
res = func(**kwargs)
|
||||
except PermissionError as e:
|
||||
|
|
|
@ -11,13 +11,17 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { DefaultButton, PgButtonGroup, PgIconButton, PrimaryButton } from '../../../../../static/js/components/Buttons';
|
||||
import { useModalStyles } from '../../../../../static/js/helpers/ModalProvider';
|
||||
import CloseIcon from '@material-ui/icons/CloseRounded';
|
||||
import FolderSharedIcon from '@material-ui/icons/FolderShared';
|
||||
import FolderIcon from '@material-ui/icons/Folder';
|
||||
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
|
||||
import HomeRoundedIcon from '@material-ui/icons/HomeRounded';
|
||||
import ArrowUpwardRoundedIcon from '@material-ui/icons/ArrowUpwardRounded';
|
||||
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
||||
import MoreHorizRoundedIcon from '@material-ui/icons/MoreHorizRounded';
|
||||
import SyncRoundedIcon from '@material-ui/icons/SyncRounded';
|
||||
import CreateNewFolderRoundedIcon from '@material-ui/icons/CreateNewFolderRounded';
|
||||
import GetAppRoundedIcon from '@material-ui/icons/GetAppRounded';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import gettext from 'sources/gettext';
|
||||
import clsx from 'clsx';
|
||||
import { FormFooterMessage, InputSelectNonSearch, InputText, MESSAGE_TYPE } from '../../../../../static/js/components/FormComponents';
|
||||
|
@ -32,6 +36,7 @@ import convert from 'convert-units';
|
|||
import PropTypes from 'prop-types';
|
||||
import { downloadBlob } from '../../../../../static/js/utils';
|
||||
import ErrorBoundary from '../../../../../static/js/helpers/ErrorBoundary';
|
||||
import { MY_STORAGE } from './FileManagerConstants';
|
||||
import _ from 'lodash';
|
||||
|
||||
const useStyles = makeStyles((theme)=>({
|
||||
|
@ -85,6 +90,15 @@ const useStyles = makeStyles((theme)=>({
|
|||
backgroundColor: theme.palette.background.default,
|
||||
width: '100%',
|
||||
...theme.mixins.panelBorder.all,
|
||||
},
|
||||
sharedStorage: {
|
||||
width: '3rem !important',
|
||||
},
|
||||
storageName: {
|
||||
paddingLeft: '0.2rem'
|
||||
},
|
||||
sharedIcon: {
|
||||
width: '1.3rem'
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -137,6 +151,7 @@ export class FileManagerUtils {
|
|||
this.config = {};
|
||||
this.currPath = '';
|
||||
this.separator = '/';
|
||||
this.storage_folder = '';
|
||||
}
|
||||
|
||||
get transId() {
|
||||
|
@ -197,23 +212,25 @@ export class FileManagerUtils {
|
|||
return filename.split('.').pop();
|
||||
}
|
||||
|
||||
async getFolder(path) {
|
||||
async getFolder(path, sharedFolder=null) {
|
||||
const newPath = path || this.fileRoot;
|
||||
let res = await this.api.post(this.fileConnectorUrl, {
|
||||
'path': newPath,
|
||||
'mode': 'getfolder',
|
||||
'file_type': this.config.options.last_selected_format || '*',
|
||||
'show_hidden': this.showHiddenFiles,
|
||||
'storage_folder': sharedFolder,
|
||||
});
|
||||
this.currPath = newPath;
|
||||
return res.data.data.result;
|
||||
}
|
||||
|
||||
async addFolder(row) {
|
||||
async addFolder(row, ss) {
|
||||
let res = await this.api.post(this.fileConnectorUrl, {
|
||||
'path': this.currPath,
|
||||
'mode': 'addfolder',
|
||||
'name': row.Filename,
|
||||
'storage_folder': ss
|
||||
});
|
||||
return {
|
||||
Filename: res.data.data.result.Name,
|
||||
|
@ -225,11 +242,12 @@ export class FileManagerUtils {
|
|||
};
|
||||
}
|
||||
|
||||
async renameItem(row) {
|
||||
async renameItem(row, ss) {
|
||||
let res = await this.api.post(this.fileConnectorUrl, {
|
||||
'mode': 'rename',
|
||||
'old': row.Path,
|
||||
'new': row.Filename,
|
||||
'storage_folder': ss
|
||||
});
|
||||
return {
|
||||
...row,
|
||||
|
@ -238,20 +256,22 @@ export class FileManagerUtils {
|
|||
};
|
||||
}
|
||||
|
||||
async deleteItem(row, fileName) {
|
||||
async deleteItem(row, ss, fileName) {
|
||||
const path = fileName ? this.join(row.Path, fileName) : row.Path;
|
||||
await this.api.post(this.fileConnectorUrl, {
|
||||
'mode': 'delete',
|
||||
'path': path,
|
||||
'storage_folder': ss
|
||||
});
|
||||
return path;
|
||||
}
|
||||
|
||||
async uploadItem(fileObj, onUploadProgress) {
|
||||
async uploadItem(fileObj, ss, onUploadProgress) {
|
||||
const formData = new FormData();
|
||||
formData.append('newfile', fileObj);
|
||||
formData.append('mode', 'add');
|
||||
formData.append('currentpath', this.join(this.currPath, ''));
|
||||
formData.append('storage_folder', ss);
|
||||
return this.api({
|
||||
method: 'POST',
|
||||
url: this.fileConnectorUrl,
|
||||
|
@ -263,15 +283,16 @@ export class FileManagerUtils {
|
|||
});
|
||||
}
|
||||
|
||||
async setLastVisitedDir(path) {
|
||||
async setLastVisitedDir(path, ss) {
|
||||
return this.api.post(url_for('file_manager.save_last_dir', {
|
||||
trans_id: this.transId,
|
||||
}), {
|
||||
'path': path,
|
||||
'storage_folder': ss
|
||||
});
|
||||
}
|
||||
|
||||
async downloadFile(row) {
|
||||
async downloadFile(row, ss) {
|
||||
let res = await this.api({
|
||||
method: 'POST',
|
||||
url: this.fileConnectorUrl,
|
||||
|
@ -279,6 +300,7 @@ export class FileManagerUtils {
|
|||
data: {
|
||||
'mode': 'download',
|
||||
'path': row.Path,
|
||||
'storage_folder': ss,
|
||||
},
|
||||
});
|
||||
downloadBlob(res.data, res.headers.filename);
|
||||
|
@ -354,6 +376,7 @@ export class FileManagerUtils {
|
|||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function ConfirmFile({text, onYes, onNo}) {
|
||||
|
@ -401,6 +424,8 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
const selectedRowIdx = useRef();
|
||||
const optionsRef = React.useRef(null);
|
||||
const saveAsRef = React.useRef(null);
|
||||
const sharedSRef = React.useRef(null);
|
||||
const [selectedSS, setSelectedSS] = React.useState(MY_STORAGE);
|
||||
const [operation, setOperation] = useState({
|
||||
type: null, idx: null
|
||||
});
|
||||
|
@ -421,24 +446,30 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
return `${filteredItems.length} of ${items.length} ${suffix}`;
|
||||
}, [items, filteredItems]);
|
||||
|
||||
const openDir = async (dirPath)=>{
|
||||
const changeDir = async(storage) => {
|
||||
setSelectedSS(storage);
|
||||
fmUtilsObj.storage_folder = storage;
|
||||
await openDir('/', storage);
|
||||
};
|
||||
const openDir = async (dirPath, changeStoragePath=null)=>{
|
||||
setErrorMsg('');
|
||||
setLoaderText('Loading...');
|
||||
try {
|
||||
if(fmUtilsObj.isWinDrive(dirPath)) {
|
||||
dirPath += fmUtilsObj.separator;
|
||||
}
|
||||
let newItems = await fmUtilsObj.getFolder(dirPath || fmUtilsObj.currPath);
|
||||
let newItems = await fmUtilsObj.getFolder(dirPath || fmUtilsObj.currPath, changeStoragePath);
|
||||
setItems(newItems);
|
||||
setPath(fmUtilsObj.currPath);
|
||||
params.dialog_type == 'storage_dialog' && fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath);
|
||||
params.dialog_type == 'storage_dialog' && fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath, selectedSS);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setErrorMsg(parseApiError(error));
|
||||
}
|
||||
setLoaderText('');
|
||||
};
|
||||
const completeOperation = async (oldRow, newRow, rowIdx, func)=>{
|
||||
|
||||
const completeOperation = async (oldRow, newRow, rowIdx, selectedSS, func)=>{
|
||||
setOperation({});
|
||||
if(oldRow?.Filename == newRow.Filename) {
|
||||
setItems((prev)=>[
|
||||
|
@ -454,7 +485,7 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
...prev.slice(rowIdx+1)
|
||||
]);
|
||||
try {
|
||||
const actualRow = await func(newRow);
|
||||
const actualRow = await func(newRow, selectedSS);
|
||||
setItems((prev)=>[
|
||||
...prev.slice(0, rowIdx),
|
||||
actualRow,
|
||||
|
@ -479,7 +510,7 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
const onDownload = async ()=>{
|
||||
setLoaderText('Downloading...');
|
||||
try {
|
||||
await fmUtilsObj.downloadFile(filteredItems[selectedRowIdx.current]);
|
||||
await fmUtilsObj.downloadFile(filteredItems[selectedRowIdx.current], selectedSS);
|
||||
} catch (error) {
|
||||
setErrorMsg(parseApiError(error));
|
||||
console.error(error);
|
||||
|
@ -497,7 +528,7 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
onComplete: async (row, rowIdx)=>{
|
||||
setErrorMsg('');
|
||||
setLoaderText('Creating folder...');
|
||||
await completeOperation(null, row, rowIdx, fmUtilsObj.addFolder.bind(fmUtilsObj));
|
||||
await completeOperation(null, row, rowIdx, selectedSS, fmUtilsObj.addFolder.bind(fmUtilsObj));
|
||||
setLoaderText('');
|
||||
}
|
||||
});
|
||||
|
@ -515,7 +546,7 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
setErrorMsg('');
|
||||
setLoaderText('Renaming...');
|
||||
let oldRow = items[rowIdx];
|
||||
await completeOperation(oldRow, row, rowIdx, fmUtilsObj.renameItem.bind(fmUtilsObj));
|
||||
await completeOperation(oldRow, row, rowIdx, selectedSS,fmUtilsObj.renameItem.bind(fmUtilsObj));
|
||||
setLoaderText('');
|
||||
}
|
||||
});
|
||||
|
@ -530,7 +561,7 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
setConfirmFile([null, null]);
|
||||
setLoaderText('Deleting...');
|
||||
try {
|
||||
await fmUtilsObj.deleteItem(items[selectedRowIdx.current]);
|
||||
await fmUtilsObj.deleteItem(items[selectedRowIdx.current],selectedSS);
|
||||
setItems((prev)=>[
|
||||
...prev.slice(0, selectedRowIdx.current),
|
||||
...prev.slice(selectedRowIdx.current+1),
|
||||
|
@ -567,8 +598,8 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
if(exists) {
|
||||
setLoaderText('');
|
||||
setConfirmFile([gettext('Are you sure you want to replace this file?'), async ()=>{
|
||||
await fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath);
|
||||
onOK?.(onOkPath);
|
||||
await fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath, selectedSS);
|
||||
onOK?.(onOkPath, selectedSS);
|
||||
closeModal();
|
||||
}]);
|
||||
return;
|
||||
|
@ -576,13 +607,13 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
} else if(selectedRowIdx?.current >= 0 && filteredItems[selectedRowIdx?.current]) {
|
||||
onOkPath = filteredItems[selectedRowIdx?.current]['Path'];
|
||||
}
|
||||
await fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath);
|
||||
onOK?.(onOkPath);
|
||||
await fmUtilsObj.setLastVisitedDir(fmUtilsObj.currPath, selectedSS);
|
||||
onOK?.(onOkPath, selectedSS);
|
||||
closeModal();
|
||||
}, [filteredItems, saveAs, fileType]);
|
||||
const onItemEnter = useCallback(async (row)=>{
|
||||
if(row.file_type == 'dir' || row.file_type == 'drive') {
|
||||
await openDir(row.Path);
|
||||
await openDir(row.Path, selectedSS);
|
||||
} else {
|
||||
if(params.dialog_type == 'select_file') {
|
||||
onOkClick();
|
||||
|
@ -625,8 +656,15 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
} else {
|
||||
setViewMode('list');
|
||||
}
|
||||
openDir(params?.path);
|
||||
params?.path && fmUtilsObj.setLastVisitedDir(params?.path);
|
||||
if (fmUtilsObj.config.options.storage_folder == '') {
|
||||
setSelectedSS(MY_STORAGE);
|
||||
} else {
|
||||
fmUtilsObj.storage_folder = fmUtilsObj.config.options.storage_folder;
|
||||
setSelectedSS(fmUtilsObj.config.options.storage_folder);
|
||||
}
|
||||
|
||||
openDir(params?.path, fmUtilsObj.config.options.storage_folder);
|
||||
params?.path && fmUtilsObj.setLastVisitedDir(params?.path, selectedSS);
|
||||
};
|
||||
init();
|
||||
setTimeout(()=>{
|
||||
|
@ -649,6 +687,7 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
okBtnText = gettext('Create');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Box display="flex" flexDirection="column" height="100%" className={modalClasses.container}>
|
||||
|
@ -657,11 +696,16 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
{Boolean(confirmText) && <ConfirmFile text={confirmText} onNo={()=>setConfirmFile([null, null])} onYes={onConfirmYes}/>}
|
||||
<Box className={classes.toolbar}>
|
||||
<PgButtonGroup size="small" style={{flexGrow: 1}}>
|
||||
{ pgAdmin.server_mode == 'True' && pgAdmin.shared_storage.length > 0?
|
||||
<PgIconButton title={ selectedSS == MY_STORAGE ? gettext('My Storage') :gettext(selectedSS)} icon={ selectedSS == MY_STORAGE ? <><FolderIcon/><KeyboardArrowDownIcon style={{marginLeft: '-10px'}} /></> : <><FolderSharedIcon /><KeyboardArrowDownIcon style={{marginLeft: '-10px'}} /></>} splitButton
|
||||
name="menu-shared-storage" ref={sharedSRef} onClick={toggleMenu} className={classes.sharedStorage}/>
|
||||
: <></>
|
||||
}
|
||||
<PgIconButton title={gettext('Home')} onClick={async ()=>{
|
||||
await openDir(fmUtilsObj.config?.options?.homedir);
|
||||
await openDir(fmUtilsObj.config?.options?.homedir, selectedSS);
|
||||
}} icon={<HomeRoundedIcon />} disabled={showUploader} />
|
||||
<PgIconButton title={gettext('Go Back')} onClick={async ()=>{
|
||||
await openDir(fmUtilsObj.dirname(fmUtilsObj.currPath));
|
||||
await openDir(fmUtilsObj.dirname(fmUtilsObj.currPath), selectedSS);
|
||||
}} icon={<ArrowUpwardRoundedIcon />} disabled={!fmUtilsObj.dirname(fmUtilsObj.currPath) || showUploader} />
|
||||
<InputText className={classes.inputFilename}
|
||||
data-label="file-path"
|
||||
|
@ -672,8 +716,9 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
await openDir(path);
|
||||
}
|
||||
}} value={path} onChange={setPath} readonly={showUploader} />
|
||||
|
||||
<PgIconButton title={gettext('Refresh')} onClick={async ()=>{
|
||||
await openDir();
|
||||
await openDir(path, selectedSS);
|
||||
}} icon={<SyncRoundedIcon />} disabled={showUploader} />
|
||||
</PgButtonGroup>
|
||||
<InputText type="search" className={classes.inputSearch} data-label="search" placeholder={gettext('Search')} value={search} onChange={setSearch} />
|
||||
|
@ -714,9 +759,33 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
<PgMenuItem hasCheck checked={fmUtilsObj.showHiddenFiles} onClick={async (e)=>{
|
||||
e.keepOpen = false;
|
||||
fmUtilsObj.showHiddenFiles = !fmUtilsObj.showHiddenFiles;
|
||||
await openDir();
|
||||
await openDir(fmUtilsObj.currPath, selectedSS);
|
||||
}}>{gettext('Show Hidden Files')}</PgMenuItem>
|
||||
</PgMenu>
|
||||
<PgMenu
|
||||
anchorRef={sharedSRef}
|
||||
open={openMenuName=='menu-shared-storage'}
|
||||
onClose={onMenuClose}
|
||||
label={gettext(`${selectedSS}`)}
|
||||
>
|
||||
<PgMenuItem hasCheck value="my_storage" checked={selectedSS == MY_STORAGE}
|
||||
onClick={async (option)=> {
|
||||
option.keepOpen = false;
|
||||
await changeDir(option.value);
|
||||
}}><FolderIcon className={classes.sharedIcon}/><Box className={classes.storageName}>{gettext('My Storage')}</Box></PgMenuItem>
|
||||
|
||||
{
|
||||
pgAdmin.shared_storage.map((ss)=> {
|
||||
return (
|
||||
<PgMenuItem key={ss} hasCheck value={ss} checked={selectedSS == ss}
|
||||
onClick={async(option)=> {
|
||||
option.keepOpen = false;
|
||||
await changeDir(option.value);
|
||||
}}><FolderSharedIcon className={classes.sharedIcon}/><Box className={classes.storageName}>{gettext(ss)}</Box></PgMenuItem>);
|
||||
})
|
||||
}
|
||||
|
||||
</PgMenu>
|
||||
</Box>
|
||||
<Box flexGrow="1" display="flex" flexDirection="column" position="relative" overflow="hidden">
|
||||
{showUploader &&
|
||||
|
@ -724,7 +793,7 @@ export default function FileManager({params, closeModal, onOK, onCancel}) {
|
|||
onClose={async (filesUploaded)=>{
|
||||
setShowUploader(false);
|
||||
if(filesUploaded) {
|
||||
await openDir();
|
||||
await openDir(fmUtilsObj.currPath, selectedSS);
|
||||
}
|
||||
}}/>}
|
||||
{viewMode == 'list' &&
|
||||
|
|
|
@ -9,3 +9,5 @@
|
|||
export const FILE_MANGER_EVENTS = {
|
||||
ADD_FOLDER: 'ADD_FOLDER'
|
||||
};
|
||||
|
||||
export const MY_STORAGE = 'my_storage';
|
|
@ -148,7 +148,7 @@ export default function Uploader({fmUtilsObj, onClose}) {
|
|||
type: 'started',
|
||||
id: upfile.id,
|
||||
});
|
||||
await fmUtilsObj.uploadItem(upfile.file, (progressEvent)=>{
|
||||
await fmUtilsObj.uploadItem(upfile.file, fmUtilsObj.storage_folder, (progressEvent)=>{
|
||||
const {loaded, total} = progressEvent;
|
||||
const percent = Math.floor((loaded * 100) / total);
|
||||
dispatchFileAction({
|
||||
|
|
|
@ -369,7 +369,7 @@ export default function PreferencesComponent({ ...props }) {
|
|||
pgAdmin.Browser.Events.on('preferences:tree:selected', (event, item) => {
|
||||
if (item.type == FileType.File) {
|
||||
prefSchema.current.schemaFields.forEach((field) => {
|
||||
field.visible = field.parentId === item._metadata.data.id;
|
||||
field.visible = field.parentId === item._metadata.data.id && !field?.hidden ;
|
||||
if(field.visible && _.isNull(firstElement)) {
|
||||
firstElement = field;
|
||||
}
|
||||
|
|
|
@ -200,7 +200,7 @@ const ALLOWED_PROPS_FIELD_COMMON = [
|
|||
'mode', 'value', 'readonly', 'disabled', 'hasError', 'id',
|
||||
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
|
||||
'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
|
||||
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton', 'btnName'
|
||||
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton', 'btnName', 'hidden'
|
||||
];
|
||||
|
||||
const ALLOWED_PROPS_FIELD_FORM = [
|
||||
|
@ -208,7 +208,7 @@ const ALLOWED_PROPS_FIELD_FORM = [
|
|||
];
|
||||
|
||||
const ALLOWED_PROPS_FIELD_CELL = [
|
||||
'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType', 'hideBrowseButton'
|
||||
'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType', 'hideBrowseButton', 'hidden'
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -51,7 +51,8 @@ from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
|
|||
from pgadmin.tools.sqleditor.utils.macros import get_macros,\
|
||||
get_user_macros, set_macros
|
||||
from pgadmin.utils.constants import MIMETYPE_APP_JS, \
|
||||
SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, ERROR_FETCHING_DATA
|
||||
SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, \
|
||||
ERROR_FETCHING_DATA, MY_STORAGE
|
||||
from pgadmin.model import Server, ServerGroup
|
||||
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
|
||||
from pgadmin.settings import get_setting
|
||||
|
@ -1808,7 +1809,9 @@ def load_file():
|
|||
file_path = unquote(file_data['file_name'])
|
||||
|
||||
# retrieve storage directory path
|
||||
storage_manager_path = get_storage_directory()
|
||||
storage_manager_path = get_storage_directory(
|
||||
shared_storage=file_data['storage'])
|
||||
|
||||
try:
|
||||
Filemanager.check_access_permission(storage_manager_path, file_path)
|
||||
except Exception as e:
|
||||
|
@ -1850,7 +1853,13 @@ def save_file():
|
|||
file_data = json.loads(request.data)
|
||||
|
||||
# retrieve storage directory path
|
||||
storage_manager_path = get_storage_directory()
|
||||
last_storage = Preferences.module('file_manager').preference(
|
||||
'last_storage').get()
|
||||
if last_storage != MY_STORAGE:
|
||||
storage_manager_path = get_storage_directory(
|
||||
shared_storage=last_storage)
|
||||
else:
|
||||
storage_manager_path = get_storage_directory()
|
||||
|
||||
# generate full path of file
|
||||
file_path = unquote(file_data['file_name'])
|
||||
|
|
|
@ -439,8 +439,8 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
|
|||
'supported_types': ['*', 'sql'], // file types allowed
|
||||
'dialog_type': 'select_file', // open select file dialog
|
||||
};
|
||||
pgAdmin.Tools.FileManager.show(fileParams, (fileName)=>{
|
||||
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName);
|
||||
pgAdmin.Tools.FileManager.show(fileParams, (fileName, storage)=>{
|
||||
eventBus.current.fireEvent(QUERY_TOOL_EVENTS.LOAD_FILE, fileName, storage);
|
||||
}, null, modal);
|
||||
}],
|
||||
[QUERY_TOOL_EVENTS.TRIGGER_SAVE_FILE, (isSaveAs=false)=>{
|
||||
|
|
|
@ -350,9 +350,10 @@ export default function Query() {
|
|||
}
|
||||
});
|
||||
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_FILE, (fileName)=>{
|
||||
eventBus.registerListener(QUERY_TOOL_EVENTS.LOAD_FILE, (fileName, storage)=>{
|
||||
queryToolCtx.api.post(url_for('sqleditor.load_file'), {
|
||||
'file_name': decodeURI(fileName),
|
||||
'storage': storage
|
||||
}, {transformResponse: [(data, headers) => {
|
||||
if(headers['content-type'].includes('application/json')) {
|
||||
return JSON.parse(data);
|
||||
|
|
|
@ -120,3 +120,6 @@ DATABASE_LAST_SYSTEM_OID = 16383
|
|||
# Drivers
|
||||
PSYCOPG2 = 'psycopg2'
|
||||
PSYCOPG3 = 'psycopg3'
|
||||
|
||||
# Shared storage
|
||||
MY_STORAGE = 'my_storage'
|
||||
|
|
|
@ -14,21 +14,32 @@ import os
|
|||
from flask import current_app, url_for
|
||||
from flask_security import current_user
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from pgadmin.utils.constants import MY_STORAGE
|
||||
|
||||
|
||||
def get_storage_directory(user=current_user):
|
||||
def get_storage_directory(user=current_user, shared_storage=''):
|
||||
import config
|
||||
if config.SERVER_MODE is not True:
|
||||
return None
|
||||
|
||||
storage_dir = getattr(
|
||||
config, 'STORAGE_DIR',
|
||||
os.path.join(
|
||||
os.path.realpath(
|
||||
os.path.expanduser('~/.pgadmin/')
|
||||
), 'storage'
|
||||
is_shared_storage = False
|
||||
if shared_storage != MY_STORAGE and shared_storage:
|
||||
is_shared_storage = True
|
||||
selectedDir = [sdir for sdir in config.SHARED_STORAGE if
|
||||
sdir['name'] == shared_storage]
|
||||
storage_dir = None
|
||||
if len(selectedDir) > 0:
|
||||
the_dir = selectedDir[0]['path']
|
||||
storage_dir = the_dir
|
||||
else:
|
||||
storage_dir = getattr(
|
||||
config, 'STORAGE_DIR',
|
||||
os.path.join(
|
||||
os.path.realpath(
|
||||
os.path.expanduser('~/.pgadmin/')
|
||||
), 'storage'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if storage_dir is None:
|
||||
return None
|
||||
|
@ -55,12 +66,19 @@ def get_storage_directory(user=current_user):
|
|||
|
||||
username = _preprocess_username(user.username)
|
||||
|
||||
# Figure out the new style storage directory name
|
||||
storage_dir = os.path.join(
|
||||
storage_dir.decode('utf-8') if hasattr(storage_dir, 'decode')
|
||||
else storage_dir,
|
||||
username
|
||||
)
|
||||
if is_shared_storage:
|
||||
# Figure out the new style storage directory name
|
||||
storage_dir = os.path.join(
|
||||
storage_dir.decode('utf-8') if hasattr(storage_dir, 'decode')
|
||||
else storage_dir
|
||||
)
|
||||
else:
|
||||
# Figure out the new style storage directory name
|
||||
storage_dir = os.path.join(
|
||||
storage_dir.decode('utf-8') if hasattr(storage_dir, 'decode')
|
||||
else storage_dir,
|
||||
username
|
||||
)
|
||||
|
||||
# Rename an old-style storage directory, if the new style doesn't exist
|
||||
if os.path.exists(old_storage_dir) and not os.path.exists(storage_dir):
|
||||
|
|
|
@ -72,6 +72,7 @@ class _Preference():
|
|||
self.options = kwargs.get('options', None)
|
||||
self.select = kwargs.get('select', None)
|
||||
self.fields = kwargs.get('fields', None)
|
||||
self.hidden = kwargs.get('hidden', None)
|
||||
self.allow_blanks = kwargs.get('allow_blanks', None)
|
||||
self.disabled = kwargs.get('disabled', False)
|
||||
self.dependents = kwargs.get('dependents', None)
|
||||
|
@ -262,6 +263,7 @@ class _Preference():
|
|||
'select': self.select,
|
||||
'value': self.get(),
|
||||
'fields': self.fields,
|
||||
'hidden': self.hidden,
|
||||
'disabled': self.disabled,
|
||||
'dependents': self.dependents
|
||||
}
|
||||
|
@ -436,6 +438,7 @@ class Preferences():
|
|||
category_label = kwargs.get('category_label', None)
|
||||
select = kwargs.get('select', None)
|
||||
fields = kwargs.get('fields', None)
|
||||
hidden = kwargs.get('hidden', None)
|
||||
allow_blanks = kwargs.get('allow_blanks', None)
|
||||
disabled = kwargs.get('disabled', False)
|
||||
dependents = kwargs.get('dependents', None)
|
||||
|
@ -457,7 +460,7 @@ class Preferences():
|
|||
min_val=min_val, max_val=max_val, options=options,
|
||||
select=select, fields=fields, allow_blanks=allow_blanks,
|
||||
disabled=disabled, dependents=dependents,
|
||||
control_props=control_props
|
||||
control_props=control_props, hidden=hidden
|
||||
)
|
||||
|
||||
return res
|
||||
|
|
Loading…
Reference in New Issue