diff --git a/jupyter_drives/handlers.py b/jupyter_drives/handlers.py index 6e8dbd2..db69167 100644 --- a/jupyter_drives/handlers.py +++ b/jupyter_drives/handlers.py @@ -91,6 +91,8 @@ async def post(self, drive: str = "", path: str = ""): body = self.get_json_body() if 'location' in body: result = await self._manager.new_drive(drive, **body) + if 'public' in body: + result = await self._manager.add_public_drive(drive) else: result = await self._manager.new_file(drive, path, **body) self.finish(result) diff --git a/jupyter_drives/manager.py b/jupyter_drives/manager.py index 47028e2..c2d81a2 100644 --- a/jupyter_drives/manager.py +++ b/jupyter_drives/manager.py @@ -52,6 +52,7 @@ def __init__(self, config: traitlets.config.Config) -> None: self._content_managers = {} self._max_files_listed = 1025 self._drives = None + self._external_drives = {} # instate fsspec file system self._file_system = fsspec.filesystem(self._config.provider, asynchronous=True) @@ -178,7 +179,7 @@ async def list_drives(self): """ data = [] if self._config.access_key_id and self._config.secret_access_key: - if self._drives is None: + if self._drives is None and len(self._external_drives) == 0: raise tornado.web.HTTPError( status_code= httpx.codes.NOT_IMPLEMENTED, reason="Listing drives not supported for given provider.", @@ -187,7 +188,7 @@ async def list_drives(self): results = [] for drive in self._drives: try: - results += drive.list_containers() + results += drive.list_containers() except Exception as e: raise tornado.web.HTTPError( status_code=httpx.codes.BAD_REQUEST, @@ -204,6 +205,23 @@ async def list_drives(self): "provider": self._config.provider } ) + + if len(self._external_drives) != 0: + for drive in self._external_drives.values(): + try: + data.append({ + "name": drive['url'], + "region": self._config.region_name, + "creationDate": datetime.now().isoformat(timespec='milliseconds').replace('+00:00', 'Z'), + "mounted": False if result.name not in self._content_managers else True, + "provider": self._config.provider + }) + except Exception as e: + raise tornado.web.HTTPError( + status_code=httpx.codes.BAD_REQUEST, + reason=f"The following error occured when listing drives: {e}", + ) + else: raise tornado.web.HTTPError( status_code= httpx.codes.BAD_REQUEST, @@ -624,6 +642,26 @@ async def new_drive(self, new_drive_name, location='us-east-1'): return + async def add_public_drive(self, drive_name): + """Mount a drive. + + Args: + drive_name: name of public bucket to mount + """ + try: + drive = { + "is_public": True, + "url": drive_name + }; + self._external_drives[drive_name] = drive; + except Exception as e: + raise tornado.web.HTTPError( + status_code= httpx.codes.BAD_REQUEST, + reason= f"The following error occured when adding the public drive: {e}" + ) + + return + async def _get_drive_location(self, drive_name): """Helping function for getting drive region. diff --git a/src/contents.ts b/src/contents.ts index 52dc47b..35593aa 100644 --- a/src/contents.ts +++ b/src/contents.ts @@ -10,6 +10,7 @@ import { IRegisteredFileTypes } from './token'; import { + addPublicDrive, saveObject, getContents, mountDrive, @@ -662,6 +663,26 @@ export class Drive implements Contents.IDrive { return data; } + /** + * Add public drive. + * + * @param options: The options used to add the public drive. + * + * @returns A promise which resolves with the contents model. + */ + async addPublicDrive(driveUrl: string): Promise { + data = await addPublicDrive(driveUrl); + + Contents.validateContentsModel(data); + this._fileChanged.emit({ + type: 'new', + oldValue: null, + newValue: data + }); + + return data; + } + /** * Create a checkpoint for a file. * diff --git a/src/plugins/driveBrowserPlugin.ts b/src/plugins/driveBrowserPlugin.ts index de44ac2..9b54d5c 100644 --- a/src/plugins/driveBrowserPlugin.ts +++ b/src/plugins/driveBrowserPlugin.ts @@ -333,6 +333,55 @@ namespace Private { } } + /** + * Create the node for adding a public drive handler. + */ + const addPublicDriveNode = (): HTMLElement => { + const body = document.createElement('div'); + + const drive = document.createElement('label'); + drive.textContent = 'Name'; + drive.className = CREATE_DRIVE_TITLE_CLASS; + const driveName = document.createElement('input'); + + body.appendChild(drive); + body.appendChild(driveName); + return body; + }; + + /** + * A widget used to add a public drive. + */ + export class AddPublicDriveHandler extends Widget { + /** + * Construct a new "add-public-drive" dialog. + */ + constructor(newDriveName: string) { + super({ node: addPublicDriveNode() }); + this.onAfterAttach(); + } + + protected onAfterAttach(): void { + this.addClass(FILE_DIALOG_CLASS); + const drive = this.driveInput.value; + this.driveInput.setSelectionRange(0, drive.length); + } + + /** + * Get the input text node for drive name. + */ + get driveInput(): HTMLInputElement { + return this.node.getElementsByTagName('input')[0] as HTMLInputElement; + } + + /** + * Get the value of the widget. + */ + getValue(): string { + return this.driveInput.value; + } + } + export function addCommands( app: JupyterFrontEnd, drive: Drive, @@ -373,6 +422,38 @@ namespace Private { rank: 105 }); + app.commands.addCommand(CommandIDs.addPublicDrive, { + isEnabled: () => { + return browser.model.path === 's3:'; + }, + execute: async () => { + return showDialog({ + title: 'Add Public Drive', + body: new Private.AddPublicDriveHandler(drive.name), + focusNodeSelector: 'input', + buttons: [ + Dialog.cancelButton(), + Dialog.okButton({ + label: 'Add', + ariaLabel: 'Add Drive' + }) + ] + }).then(result => { + if (result.value) { + drive.addPublicDrive(result.value); + } + }); + }, + label: 'Add Public Drive', + icon: driveBrowserIcon.bindprops({ stylesheet: 'menuItem' }) + }); + + app.contextMenu.addItem({ + command: CommandIDs.addPublicDrive, + selector: '#drive-file-browser.jp-SidePanel .jp-DirListing-content', + rank: 110 + }); + app.commands.addCommand(CommandIDs.toggleFileFilter, { execute: () => { // Update toggled state, then let the toolbar button update diff --git a/src/requests.ts b/src/requests.ts index ebfb222..872618e 100644 --- a/src/requests.ts +++ b/src/requests.ts @@ -533,6 +533,33 @@ export async function createDrive( return data; } +/** + * Add public drive. + * + * @param driveUrl The public drive URL. + * + * @returns A promise which resolves with the contents model. + */ +export async function addPublicDrive(driveUrl: string) { + await requestAPI('drives/' + driveUrl + '/', 'POST', { + public: true + }); + + data = { + name: driveUrl, + path: driveUrl, + last_modified: '', + created: '', + content: [], + format: 'json', + mimetype: '', + size: 0, + writable: true, + type: 'directory' + }; + return data; +} + namespace Private { /** * Helping function for renaming files inside diff --git a/src/token.ts b/src/token.ts index 311e4ac..979143a 100644 --- a/src/token.ts +++ b/src/token.ts @@ -8,6 +8,7 @@ export namespace CommandIDs { export const openPath = 'drives:open-path'; export const toggleBrowser = 'drives:toggle-main'; export const createNewDrive = 'drives:create-new-drive'; + export const addPublicDrive = 'drives:add-public-drive'; export const launcher = 'launcher:create'; export const toggleFileFilter = 'drives:toggle-file-filter'; export const createNewDirectory = 'drives:create-new-directory';