diff --git a/jupyter_drives/handlers.py b/jupyter_drives/handlers.py index 7320e59..8912b33 100644 --- a/jupyter_drives/handlers.py +++ b/jupyter_drives/handlers.py @@ -53,33 +53,36 @@ def initialize(self, logger: logging.Logger, manager: JupyterDrivesManager): @tornado.web.authenticated async def get(self): result = await self._manager.list_drives() - self.finish(json.dumps(result)) + self.finish(result) @tornado.web.authenticated async def post(self): body = self.get_json_body() result = await self._manager.mount_drive(**body) - self.finish(json.dump(result.message)) + self.finish(result["message"]) class ContentsJupyterDrivesHandler(JupyterDrivesAPIHandler): """ Deals with contents of a drive. """ + def initialize(self, logger: logging.Logger, manager: JupyterDrivesManager): + return super().initialize(logger, manager) + @tornado.web.authenticated async def get(self, path: str = "", drive: str = ""): result = await self._manager.get_contents(drive, path) - self.finish(json.dump(result)) + self.finish(result) @tornado.web.authenticated async def post(self, path: str = "", drive: str = ""): result = await self._manager.new_file(drive, path) - self.finish(json.dump(result)) + self.finish(result) @tornado.web.authenticated async def patch(self, path: str = "", drive: str = ""): body = self.get_json_body() result = await self._manager.rename_file(drive, path, **body) - self.finish(json.dump(result)) + self.finish(result) handlers = [ ("drives", ListJupyterDrivesHandler) @@ -121,9 +124,10 @@ def setup_handlers(web_app: tornado.web.Application, config: traitlets.config.Co + [ ( url_path_join( - base_url, NAMESPACE, pattern, r"(?P\w+)", path_regex + base_url, NAMESPACE, pattern, r"(?P(?:[^/]+))"+ path_regex ), handler, + {"logger": log, "manager": manager} ) for pattern, handler in handlers_with_path ] diff --git a/jupyter_drives/managers/s3.py b/jupyter_drives/managers/s3.py index 2da68ad..9473128 100644 --- a/jupyter_drives/managers/s3.py +++ b/jupyter_drives/managers/s3.py @@ -67,12 +67,12 @@ async def list_drives(self): "code": 200 } else: - response = {"code": 400} + response = {"code": 400, "message": "No AWS credentials specified. Please set them in your user jupyter_server_config file."} raise tornado.web.HTTPError( status_code= httpx.codes.BAD_REQUEST, reason="No AWS credentials specified. Please set them in your user jupyter_server_config file.", ) - + return response async def mount_drive(self, drive_name): @@ -85,13 +85,20 @@ async def mount_drive(self, drive_name): S3ContentsManager ''' try : + s3_contents_manager = S3ContentsManager( + access_key_id = self._config.access_key_id, + secret_access_key = self._config.secret_access_key, + endpoint_url = self._config.api_base_url, + bucket = drive_name + ) + # checking if the drive wasn't mounted already - if self.s3_content_managers[drive_name] is None: + if drive_name not in self.s3_content_managers or self.s3_content_managers[drive_name] is None: # dealing with long-term credentials (access key, secret key) if self._config.session_token is None: s3_contents_manager = S3ContentsManager( - access_key = self._config.access_key_id, + access_key_id = self._config.access_key_id, secret_access_key = self._config.secret_access_key, endpoint_url = self._config.api_base_url, bucket = drive_name @@ -100,7 +107,7 @@ async def mount_drive(self, drive_name): # dealing with short-term credentials (access key, secret key, session token) else: s3_contents_manager = S3ContentsManager( - access_key = self._config.access_key_id, + access_key_id = self._config.access_key_id, secret_access_key = self._config.secret_access_key, session_token = self._config.session_token, endpoint_url = self._config.api_base_url, diff --git a/src/contents.ts b/src/contents.ts index 1db63d1..6ab9b29 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -3,8 +3,10 @@ import { Signal, ISignal } from '@lumino/signaling'; import { Contents, ServerConnection } from '@jupyterlab/services'; +import { PathExt } from '@jupyterlab/coreutils'; +import { IDriveInfo } from './token'; -const data: Contents.IModel = { +let data: Contents.IModel = { name: '', path: '', last_modified: '', @@ -26,8 +28,24 @@ export class Drive implements Contents.IDrive { constructor(options: Drive.IOptions = {}) { this._serverSettings = ServerConnection.makeSettings(); this._name = options.name ?? ''; + this._drivesList = options.drivesList ?? []; //this._apiEndpoint = options.apiEndpoint ?? SERVICE_DRIVE_URL; } + + /** + * The drives list getter. + */ + get drivesList(): IDriveInfo[] { + return this._drivesList; + } + + /** + * The drives list setter. + * */ + set drivesList(list: IDriveInfo[]) { + this._drivesList = list; + } + /** * The Drive base URL */ @@ -41,6 +59,7 @@ export class Drive implements Contents.IDrive { set baseUrl(url: string) { this._baseUrl = url; } + /** * The Drive name getter */ @@ -170,6 +189,48 @@ export class Drive implements Contents.IDrive { } else { relativePath = localPath; } + + data = { + name: PathExt.basename(localPath), + path: PathExt.basename(localPath), + last_modified: '', + created: '', + content: [], + format: 'json', + mimetype: '', + size: undefined, + writable: true, + type: 'directory' + }; + } else { + const drivesList: Contents.IModel[] = []; + for (const drive of this._drivesList) { + drivesList.push({ + name: drive.name, + path: drive.name, + last_modified: '', + created: drive.creationDate, + content: [], + format: 'json', + mimetype: '', + size: undefined, + writable: true, + type: 'directory' + }); + } + + data = { + name: this._name, + path: this._name, + last_modified: '', + created: '', + content: drivesList, + format: 'json', + mimetype: '', + size: undefined, + writable: true, + type: 'directory' + }; } console.log('GET: ', relativePath); @@ -532,6 +593,7 @@ export class Drive implements Contents.IDrive { }*/ // private _apiEndpoint: string; + private _drivesList: IDriveInfo[] = []; private _serverSettings: ServerConnection.ISettings; private _name: string = ''; private _provider: string = ''; @@ -548,6 +610,11 @@ export namespace Drive { * The options used to initialize a `Drive`. */ export interface IOptions { + /** + * List of available drives. + */ + drivesList?: IDriveInfo[]; + /** * The name for the `Drive`, which is used in file * paths to disambiguate it from other drives. diff --git a/src/drives.ts b/src/drives.ts deleted file mode 100644 index f2b2616..0000000 --- a/src/drives.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { URLExt } from '@jupyterlab/coreutils'; -import { ServerConnection } from '@jupyterlab/services'; -import { ReadonlyJSONObject } from '@lumino/coreutils'; -import { DrivesResponseError } from './drivesError'; - -/** - * Array of Jupyter Drives Auth Error Messages - */ -export const AUTH_ERROR_MESSAGES = [ - 'Invalid access key or secret access key', - 'could not read Access Key', - 'could not read Secret Access Key', - 'could not read Session Token', - 'Authentication error' -]; - -/** - * Call the API extension - * - * @param endPoint API REST end point for the extension; default '' - * @param method HTML method; default 'GET' - * @param body JSON object to be passed as body or null; default null - * @param namespace API namespace; default 'git' - * @returns The response body interpreted as JSON - * - * @throws {ServerConnection.NetworkError} If the request cannot be made - */ -export async function requestAPI( - endPoint = '', - method = 'GET', - body: Partial | null = null, - namespace = 'jupyter-drives' -): Promise { - // Make request to Jupyter API - const settings = ServerConnection.makeSettings(); - const requestUrl = URLExt.join( - settings.baseUrl, - namespace, // API Namespace - endPoint - ); - - const init: RequestInit = { - method, - body: body ? JSON.stringify(body) : undefined - }; - - let response: Response; - try { - response = await ServerConnection.makeRequest(requestUrl, init, settings); - } catch (error: any) { - throw new ServerConnection.NetworkError(error); - } - - let data: any = await response.text(); - let isJSON = false; - if (data.length > 0) { - try { - data = JSON.parse(data); - isJSON = true; - } catch (error) { - console.log('Not a JSON response body.', response); - } - } - - if (!response.ok) { - if (isJSON) { - const { message, traceback, ...json } = data; - throw new DrivesResponseError( - response, - message || - `Invalid response: ${response.status} ${response.statusText}`, - traceback || '', - json - ); - } else { - throw new DrivesResponseError(response, data); - } - } - - return data; -} diff --git a/src/handler.ts b/src/handler.ts index 7c43a3d..b343596 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -1,45 +1,81 @@ import { URLExt } from '@jupyterlab/coreutils'; - import { ServerConnection } from '@jupyterlab/services'; +import { ReadonlyJSONObject } from '@lumino/coreutils'; + +import { DrivesResponseError } from './drivesError'; + +/** + * Array of Jupyter Drives Auth Error Messages. + */ +export const AUTH_ERROR_MESSAGES = [ + 'Invalid access key or secret access key', + 'could not read Access Key', + 'could not read Secret Access Key', + 'could not read Session Token', + 'Authentication error' +]; /** * Call the API extension * - * @param endPoint API REST end point for the extension - * @param init Initial values for the request + * @param endPoint API REST end point for the extension; default '' + * @param method HTML method; default 'GET' + * @param body JSON object to be passed as body or null; default null + * @param namespace API namespace; default 'git' * @returns The response body interpreted as JSON + * + * @throws {ServerConnection.NetworkError} If the request cannot be made */ export async function requestAPI( endPoint = '', - init: RequestInit = {} + method = 'GET', + body: Partial | null = null, + namespace = 'jupyter-drives' ): Promise { // Make request to Jupyter API const settings = ServerConnection.makeSettings(); const requestUrl = URLExt.join( settings.baseUrl, - 'jupyter-drives', // API Namespace + namespace, // API Namespace endPoint ); + const init: RequestInit = { + method, + body: body ? JSON.stringify(body) : undefined + }; + let response: Response; try { response = await ServerConnection.makeRequest(requestUrl, init, settings); - } catch (error) { - throw new ServerConnection.NetworkError(error as any); + } catch (error: any) { + throw new ServerConnection.NetworkError(error); } let data: any = await response.text(); - + let isJSON = false; if (data.length > 0) { try { data = JSON.parse(data); + isJSON = true; } catch (error) { console.log('Not a JSON response body.', response); } } if (!response.ok) { - throw new ServerConnection.ResponseError(response, data.message || data); + if (isJSON) { + const { message, traceback, ...json } = data; + throw new DrivesResponseError( + response, + message || + `Invalid response: ${response.status} ${response.statusText}`, + traceback || '', + json + ); + } else { + throw new DrivesResponseError(response, data); + } } return data; diff --git a/src/index.ts b/src/index.ts index f1ddf16..0bcfc59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ import { CommandRegistry } from '@lumino/commands'; import { DriveListModel, DriveListView, IDrive } from './drivelistmanager'; import { DriveIcon, driveBrowserIcon } from './icons'; import { Drive } from './contents'; +import { getDrivesList } from './requests'; +import { IDriveInfo, IDrivesList } from './token'; /** * The command IDs used by the driveBrowser plugin. @@ -44,7 +46,7 @@ const FILE_BROWSER_FACTORY = 'DriveBrowser'; /** * The class name added to the drive filebrowser filterbox node. */ -const FILTERBOX_CLASS = 'jp-DriveBrowser-filterBox'; +const FILTERBOX_CLASS = 'jp-drive-browser-search-box'; const openDriveDialogPlugin: JupyterFrontEndPlugin = { id: '@jupyter/drives:widget', @@ -150,6 +152,32 @@ const openDriveDialogPlugin: JupyterFrontEndPlugin = { } }; +/** + * The drives list provider. + */ +const drivesListProvider: JupyterFrontEndPlugin = { + id: '@jupyter/drives:drives-list', + description: 'The drives list provider.', + provides: IDrivesList, + activate: async (_: JupyterFrontEnd): Promise => { + const drives: IDriveInfo[] = []; + try { + const response = await getDrivesList(); + for (const drive of response.data) { + drives.push({ + name: drive.name, + region: drive.region, + provider: drive.provider, + creationDate: drive.creation_date + }); + } + } catch { + console.log('Failed loading available drives list.'); + } + return drives; + } +}; + /** * The drive file browser factory provider. */ @@ -161,7 +189,8 @@ const driveFileBrowser: JupyterFrontEndPlugin = { IFileBrowserFactory, IToolbarWidgetRegistry, ISettingRegistry, - ITranslator + ITranslator, + IDrivesList ], optional: [ IRouter, @@ -175,6 +204,7 @@ const driveFileBrowser: JupyterFrontEndPlugin = { toolbarRegistry: IToolbarWidgetRegistry, settingsRegistry: ISettingRegistry, translator: ITranslator, + drivesList: IDriveInfo[], router: IRouter | null, tree: JupyterFrontEnd.ITreeResolver | null, labShell: ILabShell | null, @@ -187,7 +217,8 @@ const driveFileBrowser: JupyterFrontEndPlugin = { // create drive for drive file browser const drive = new Drive({ - name: 'jupyter-drives-buckets' + name: 'jupyter-drives', + drivesList: drivesList }); app.serviceManager.contents.addDrive(drive); @@ -258,6 +289,7 @@ const driveFileBrowser: JupyterFrontEndPlugin = { const plugins: JupyterFrontEndPlugin[] = [ driveFileBrowser, + drivesListProvider, openDriveDialogPlugin ]; export default plugins; diff --git a/src/requests.ts b/src/requests.ts new file mode 100644 index 0000000..6d16b6a --- /dev/null +++ b/src/requests.ts @@ -0,0 +1,9 @@ +import { requestAPI } from './handler'; + +/** + * Fetch the list of available drives. + * @returns list of drives + */ +export async function getDrivesList() { + return await requestAPI('drives', 'GET'); +} diff --git a/src/token.ts b/src/token.ts new file mode 100644 index 0000000..2656d36 --- /dev/null +++ b/src/token.ts @@ -0,0 +1,18 @@ +import { Token } from '@lumino/coreutils'; + +/** + * A token for the plugin that provides the list of drives. + */ +export const IDrivesList = new Token( + '@jupyter/drives:drives-list-provider' +); + +/** + * An interface for the available drives. + */ +export interface IDriveInfo { + name: string; + region: string; + provider: string; + creationDate: string; +} diff --git a/style/base.css b/style/base.css index e0d3b8e..eb58e6f 100644 --- a/style/base.css +++ b/style/base.css @@ -64,3 +64,7 @@ li { border-left: 2px; background-color: var(--jp-layout-color2); } + +.jp-drive-browser-search-box { + width: 230px; +}