Skip to content

Commit bc38762

Browse files
committed
merge with main
2 parents 67a49d7 + 811c12c commit bc38762

File tree

6 files changed

+236
-36
lines changed

6 files changed

+236
-36
lines changed

jupyter_drives/handlers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ async def get(self, drive: str = "", path: str = ""):
8989
@tornado.web.authenticated
9090
async def post(self, drive: str = "", path: str = ""):
9191
body = self.get_json_body()
92-
result = await self._manager.new_file(drive, path, **body)
92+
if 'location' in body:
93+
result = await self._manager.new_drive(drive, **body)
94+
else:
95+
result = await self._manager.new_file(drive, path, **body)
9396
self.finish(result)
9497

9598
@tornado.web.authenticated

jupyter_drives/manager.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -445,17 +445,22 @@ async def delete_file(self, drive_name, path):
445445
try:
446446
# eliminate leading and trailing backslashes
447447
path = path.strip('/')
448-
is_dir = await self._file_system._isdir(drive_name + '/' + path)
449-
if is_dir == True:
450-
await self._fix_dir(drive_name, path)
451-
await self._file_system._rm(drive_name + '/' + path, recursive = True)
448+
object_name = drive_name # in case we are only deleting the drive itself
449+
if path != '':
450+
# deleting objects within a drive
451+
is_dir = await self._file_system._isdir(drive_name + '/' + path)
452+
if is_dir == True:
453+
await self._fix_dir(drive_name, path)
454+
object_name = drive_name + '/' + path
455+
await self._file_system._rm(object_name, recursive = True)
452456

453457
# checking for remaining directories and deleting them
454-
stream = obs.list(self._content_managers[drive_name]["store"], path, chunk_size=100, return_arrow=True)
455-
async for batch in stream:
456-
contents_list = pyarrow.record_batch(batch).to_pylist()
457-
for object in contents_list:
458-
await self._fix_dir(drive_name, object["path"], delete_only = True)
458+
if object_name != drive_name:
459+
stream = obs.list(self._content_managers[drive_name]["store"], path, chunk_size=100, return_arrow=True)
460+
async for batch in stream:
461+
contents_list = pyarrow.record_batch(batch).to_pylist()
462+
for object in contents_list:
463+
await self._fix_dir(drive_name, object["path"], delete_only = True)
459464

460465
except Exception as e:
461466
raise tornado.web.HTTPError(
@@ -563,6 +568,23 @@ async def check_file(self, drive_name, path):
563568

564569
return
565570

571+
async def new_drive(self, new_drive_name, location='us-east-1'):
572+
"""Create a new drive in the given location.
573+
574+
Args:
575+
new_drive_name: name of new drive to create
576+
location: (optional) region of bucket
577+
"""
578+
try:
579+
await self._file_system._mkdir(new_drive_name, region_name = location)
580+
except Exception as e:
581+
raise tornado.web.HTTPError(
582+
status_code= httpx.codes.BAD_REQUEST,
583+
reason=f"The following error occured when creating the new drive: {e}",
584+
)
585+
586+
return
587+
566588
async def _get_drive_location(self, drive_name):
567589
"""Helping function for getting drive region.
568590

src/contents.ts

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
renameObjects,
2121
copyObjects,
2222
presignedLink,
23+
createDrive,
2324
getDrivesList
2425
} from './requests';
2526

@@ -245,30 +246,37 @@ export class Drive implements Contents.IDrive {
245246
} else {
246247
// retriving list of contents from root
247248
// in our case: list available drives
248-
const drivesList: Contents.IModel[] = [];
249+
const drivesListInfo: Contents.IModel[] = [];
249250
// fetch list of available drives
250-
this._drivesList = await getDrivesList();
251-
for (const drive of this._drivesList) {
252-
drivesList.push({
253-
name: drive.name,
254-
path: drive.name,
255-
last_modified: '',
256-
created: drive.creationDate,
257-
content: [],
258-
format: 'json',
259-
mimetype: '',
260-
size: undefined,
261-
writable: true,
262-
type: 'directory'
263-
});
251+
try {
252+
this._drivesList = await getDrivesList();
253+
for (const drive of this._drivesList) {
254+
drivesListInfo.push({
255+
name: drive.name,
256+
path: drive.name,
257+
last_modified: '',
258+
created: drive.creationDate,
259+
content: [],
260+
format: 'json',
261+
mimetype: '',
262+
size: undefined,
263+
writable: true,
264+
type: 'directory'
265+
});
266+
}
267+
} catch (error) {
268+
console.log(
269+
'Failed loading available drives list, with error: ',
270+
error
271+
);
264272
}
265273

266274
data = {
267275
name: this._name,
268276
path: this._name,
269277
last_modified: '',
270278
created: '',
271-
content: drivesList,
279+
content: drivesListInfo,
272280
format: 'json',
273281
mimetype: '',
274282
size: undefined,
@@ -393,16 +401,11 @@ export class Drive implements Contents.IDrive {
393401
* @returns A promise which resolves when the file is deleted.
394402
*/
395403
async delete(localPath: string): Promise<void> {
396-
if (localPath !== '') {
397-
const currentDrive = extractCurrentDrive(localPath, this._drivesList);
404+
const currentDrive = extractCurrentDrive(localPath, this._drivesList);
398405

399-
await deleteObjects(currentDrive.name, {
400-
path: formatPath(localPath)
401-
});
402-
} else {
403-
// create new element at root would mean modifying a drive
404-
console.warn('Operation not supported.');
405-
}
406+
await deleteObjects(currentDrive.name, {
407+
path: formatPath(localPath)
408+
});
406409

407410
this._fileChanged.emit({
408411
type: 'delete',
@@ -633,6 +636,31 @@ export class Drive implements Contents.IDrive {
633636
return data;
634637
}
635638

639+
/**
640+
* Create a new drive.
641+
*
642+
* @param options: The options used to create the drive.
643+
*
644+
* @returns A promise which resolves with the contents model.
645+
*/
646+
async newDrive(
647+
newDriveName: string,
648+
region: string
649+
): Promise<Contents.IModel> {
650+
data = await createDrive(newDriveName, {
651+
location: region
652+
});
653+
654+
Contents.validateContentsModel(data);
655+
this._fileChanged.emit({
656+
type: 'new',
657+
oldValue: null,
658+
newValue: data
659+
});
660+
661+
return data;
662+
}
663+
636664
/**
637665
* Create a checkpoint for a file.
638666
*

src/plugins/driveBrowserPlugin.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ import { ITranslator } from '@jupyterlab/translation';
1414
import {
1515
createToolbarFactory,
1616
IToolbarWidgetRegistry,
17-
setToolbar
17+
setToolbar,
18+
showDialog,
19+
Dialog
1820
} from '@jupyterlab/apputils';
1921
import { ISettingRegistry } from '@jupyterlab/settingregistry';
2022
import { FilenameSearcher, IScore } from '@jupyterlab/ui-components';
2123
import { CommandRegistry } from '@lumino/commands';
24+
import { Widget } from '@lumino/widgets';
2225

2326
import { driveBrowserIcon } from '../icons';
2427
import { Drive } from '../contents';
@@ -35,6 +38,16 @@ const FILE_BROWSER_FACTORY = 'DriveBrowser';
3538
*/
3639
const FILTERBOX_CLASS = 'jp-drive-browser-search-box';
3740

41+
/**
42+
* The class name added to dialogs.
43+
*/
44+
const FILE_DIALOG_CLASS = 'jp-FileDialog';
45+
46+
/**
47+
* The class name added for the new drive label in the creating new drive dialog.
48+
*/
49+
const CREATE_DRIVE_TITLE_CLASS = 'jp-new-drive-title';
50+
3851
/**
3952
* The drive file browser factory provider.
4053
*/
@@ -163,6 +176,9 @@ export const driveFileBrowser: JupyterFrontEndPlugin<void> = {
163176

164177
// Listen for your plugin setting changes using Signal
165178
setting.changed.connect(loadSetting);
179+
180+
// Add commands
181+
Private.addCommands(app, drive);
166182
})
167183
.catch(reason => {
168184
console.error(
@@ -225,4 +241,101 @@ namespace Private {
225241
};
226242
router.routed.connect(listener);
227243
}
244+
245+
/**
246+
* Create the node for a creating a new drive handler.
247+
*/
248+
const createNewDriveNode = (newDriveName: string): HTMLElement => {
249+
const body = document.createElement('div');
250+
251+
const drive = document.createElement('label');
252+
drive.textContent = 'Name';
253+
drive.className = CREATE_DRIVE_TITLE_CLASS;
254+
const driveName = document.createElement('input');
255+
256+
const region = document.createElement('label');
257+
region.textContent = 'Region';
258+
region.className = CREATE_DRIVE_TITLE_CLASS;
259+
const regionName = document.createElement('input');
260+
regionName.placeholder = 'us-east-1';
261+
262+
body.appendChild(drive);
263+
body.appendChild(driveName);
264+
body.appendChild(region);
265+
body.appendChild(regionName);
266+
return body;
267+
};
268+
269+
/**
270+
* A widget used to create a new drive.
271+
*/
272+
export class CreateDriveHandler extends Widget {
273+
/**
274+
* Construct a new "create-drive" dialog.
275+
*/
276+
constructor(newDriveName: string) {
277+
super({ node: createNewDriveNode(newDriveName) });
278+
this.onAfterAttach();
279+
}
280+
281+
protected onAfterAttach(): void {
282+
this.addClass(FILE_DIALOG_CLASS);
283+
const drive = this.driveInput.value;
284+
this.driveInput.setSelectionRange(0, drive.length);
285+
const region = this.regionInput.value;
286+
this.regionInput.setSelectionRange(0, region.length);
287+
}
288+
289+
/**
290+
* Get the input text node for drive name.
291+
*/
292+
get driveInput(): HTMLInputElement {
293+
return this.node.getElementsByTagName('input')[0] as HTMLInputElement;
294+
}
295+
296+
/**
297+
* Get the input text node for region.
298+
*/
299+
get regionInput(): HTMLInputElement {
300+
return this.node.getElementsByTagName('input')[1] as HTMLInputElement;
301+
}
302+
303+
/**
304+
* Get the value of the widget.
305+
*/
306+
getValue(): string[] {
307+
return [this.driveInput.value, this.regionInput.value];
308+
}
309+
}
310+
311+
export function addCommands(app: JupyterFrontEnd, drive: Drive): void {
312+
app.commands.addCommand(CommandIDs.createNewDrive, {
313+
execute: async () => {
314+
return showDialog({
315+
title: 'New Drive',
316+
body: new Private.CreateDriveHandler(drive.name),
317+
focusNodeSelector: 'input',
318+
buttons: [
319+
Dialog.cancelButton(),
320+
Dialog.okButton({
321+
label: 'Create',
322+
ariaLabel: 'Create New Drive'
323+
})
324+
]
325+
}).then(result => {
326+
if (result.value) {
327+
drive.newDrive(result.value[0], result.value[1]);
328+
}
329+
});
330+
},
331+
label: 'New Drive',
332+
icon: driveBrowserIcon.bindprops({ stylesheet: 'menuItem' })
333+
});
334+
335+
app.contextMenu.addItem({
336+
command: CommandIDs.createNewDrive,
337+
selector: '#drive-file-browser.jp-SidePanel .jp-DirListing-content',
338+
rank: 100
339+
});
340+
}
228341
}

src/requests.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,39 @@ export const countObjectNameAppearances = async (
500500
return counter;
501501
};
502502

503+
/**
504+
* Create a new drive.
505+
*
506+
* @param newDriveName The new drive name.
507+
* @param options.location The region where drive should be located.
508+
*
509+
* @returns A promise which resolves with the contents model.
510+
*/
511+
export async function createDrive(
512+
newDriveName: string,
513+
options: {
514+
location: string;
515+
}
516+
) {
517+
await requestAPI<any>('drives/' + newDriveName + '/', 'POST', {
518+
location: options.location
519+
});
520+
521+
data = {
522+
name: newDriveName,
523+
path: newDriveName,
524+
last_modified: '',
525+
created: '',
526+
content: [],
527+
format: 'json',
528+
mimetype: '',
529+
size: 0,
530+
writable: true,
531+
type: 'directory'
532+
};
533+
return data;
534+
}
535+
503536
namespace Private {
504537
/**
505538
* Helping function for renaming files inside

src/token.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export namespace CommandIDs {
88
export const openDrivesDialog = 'drives:open-drives-dialog';
99
export const openPath = 'drives:open-path';
1010
export const toggleBrowser = 'drives:toggle-main';
11+
export const createNewDrive = 'drives:create-new-drive';
1112
export const launcher = 'launcher:create';
1213
}
1314

0 commit comments

Comments
 (0)