diff --git a/docs/en_US/images/shared_storage.png b/docs/en_US/images/shared_storage.png new file mode 100644 index 000000000..49c049883 Binary files /dev/null and b/docs/en_US/images/shared_storage.png differ diff --git a/docs/en_US/images/sm_ss.png b/docs/en_US/images/sm_ss.png new file mode 100644 index 000000000..05d494574 Binary files /dev/null and b/docs/en_US/images/sm_ss.png differ diff --git a/docs/en_US/images/storage_manager.png b/docs/en_US/images/storage_manager.png index c2bd8495d..c7b2d4c52 100644 Binary files a/docs/en_US/images/storage_manager.png and b/docs/en_US/images/storage_manager.png differ diff --git a/docs/en_US/storage_manager.rst b/docs/en_US/storage_manager.rst index aee923598..59e2f9d07 100644 --- a/docs/en_US/storage_manager.rst +++ b/docs/en_US/storage_manager.rst @@ -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 ********************* diff --git a/web/config.py b/web/config.py index b36c713c3..9a030de8a 100644 --- a/web/config.py +++ b/web/config.py @@ -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. diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index d3a4c657e..a9218d611 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -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}) diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js index 93a049c88..e44dcfc77 100644 --- a/web/pgadmin/browser/templates/browser/js/utils.js +++ b/web/pgadmin/browser/templates/browser/js/utils.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}}' diff --git a/web/pgadmin/misc/file_manager/__init__.py b/web/pgadmin/misc/file_manager/__init__.py index d63290355..15e5917bb 100644 --- a/web/pgadmin/misc/file_manager/__init__.py +++ b/web/pgadmin/misc/file_manager/__init__.py @@ -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: diff --git a/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx b/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx index 7119c1df6..6b3365d45 100644 --- a/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx +++ b/web/pgadmin/misc/file_manager/static/js/components/FileManager.jsx @@ -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 ( @@ -657,11 +696,16 @@ export default function FileManager({params, closeModal, onOK, onCancel}) { {Boolean(confirmText) && setConfirmFile([null, null])} onYes={onConfirmYes}/>} + { pgAdmin.server_mode == 'True' && pgAdmin.shared_storage.length > 0? + : <>} splitButton + name="menu-shared-storage" ref={sharedSRef} onClick={toggleMenu} className={classes.sharedStorage}/> + : <> + } { - await openDir(fmUtilsObj.config?.options?.homedir); + await openDir(fmUtilsObj.config?.options?.homedir, selectedSS); }} icon={} disabled={showUploader} /> { - await openDir(fmUtilsObj.dirname(fmUtilsObj.currPath)); + await openDir(fmUtilsObj.dirname(fmUtilsObj.currPath), selectedSS); }} icon={} disabled={!fmUtilsObj.dirname(fmUtilsObj.currPath) || showUploader} /> + { - await openDir(); + await openDir(path, selectedSS); }} icon={} disabled={showUploader} /> @@ -714,9 +759,33 @@ export default function FileManager({params, closeModal, onOK, onCancel}) { { e.keepOpen = false; fmUtilsObj.showHiddenFiles = !fmUtilsObj.showHiddenFiles; - await openDir(); + await openDir(fmUtilsObj.currPath, selectedSS); }}>{gettext('Show Hidden Files')} + + { + option.keepOpen = false; + await changeDir(option.value); + }}>{gettext('My Storage')} + + { + pgAdmin.shared_storage.map((ss)=> { + return ( + { + option.keepOpen = false; + await changeDir(option.value); + }}>{gettext(ss)}); + }) + } + + {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' && diff --git a/web/pgadmin/misc/file_manager/static/js/components/FileManagerConstants.js b/web/pgadmin/misc/file_manager/static/js/components/FileManagerConstants.js index 501990383..8a1c28584 100644 --- a/web/pgadmin/misc/file_manager/static/js/components/FileManagerConstants.js +++ b/web/pgadmin/misc/file_manager/static/js/components/FileManagerConstants.js @@ -9,3 +9,5 @@ export const FILE_MANGER_EVENTS = { ADD_FOLDER: 'ADD_FOLDER' }; + +export const MY_STORAGE = 'my_storage'; \ No newline at end of file diff --git a/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx b/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx index 541ad31aa..1f2a1b14a 100644 --- a/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx +++ b/web/pgadmin/misc/file_manager/static/js/components/Uploader.jsx @@ -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({ diff --git a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx index 64206aa76..4547aa05e 100644 --- a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx +++ b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx @@ -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; } diff --git a/web/pgadmin/static/js/SchemaView/MappedControl.jsx b/web/pgadmin/static/js/SchemaView/MappedControl.jsx index 06d89eea7..655ac9cf7 100644 --- a/web/pgadmin/static/js/SchemaView/MappedControl.jsx +++ b/web/pgadmin/static/js/SchemaView/MappedControl.jsx @@ -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' ]; diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 3f71d38d9..b4edf87ad 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -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']) diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx index 7c5b78806..eccc39ce0 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx @@ -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)=>{ diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx index ef875032f..d0a8f4d70 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -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); diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 8f135bd50..59638242e 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -120,3 +120,6 @@ DATABASE_LAST_SYSTEM_OID = 16383 # Drivers PSYCOPG2 = 'psycopg2' PSYCOPG3 = 'psycopg3' + +# Shared storage +MY_STORAGE = 'my_storage' diff --git a/web/pgadmin/utils/paths.py b/web/pgadmin/utils/paths.py index 7c91377ce..2adf64c63 100644 --- a/web/pgadmin/utils/paths.py +++ b/web/pgadmin/utils/paths.py @@ -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): diff --git a/web/pgadmin/utils/preferences.py b/web/pgadmin/utils/preferences.py index 1aa72db5d..d16244b67 100644 --- a/web/pgadmin/utils/preferences.py +++ b/web/pgadmin/utils/preferences.py @@ -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