Added support for mounting shared storage in server mode. #5014

pull/5917/head
Nikhil Mohite 2023-03-06 17:03:47 +05:30 committed by GitHub
parent c35d449d7e
commit 9da8a188fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 307 additions and 66 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/en_US/images/sm_ss.png Normal file

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

View File

@ -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
*********************

View File

@ -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.

View File

@ -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})

View File

@ -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}}'

View File

@ -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:

View File

@ -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' &&

View File

@ -9,3 +9,5 @@
export const FILE_MANGER_EVENTS = {
ADD_FOLDER: 'ADD_FOLDER'
};
export const MY_STORAGE = 'my_storage';

View File

@ -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({

View File

@ -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;
}

View File

@ -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'
];

View File

@ -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'])

View File

@ -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)=>{

View File

@ -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);

View File

@ -120,3 +120,6 @@ DATABASE_LAST_SYSTEM_OID = 16383
# Drivers
PSYCOPG2 = 'psycopg2'
PSYCOPG3 = 'psycopg3'
# Shared storage
MY_STORAGE = 'my_storage'

View File

@ -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):

View File

@ -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