Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion jupyter_drives/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ async def get(self, drive: str = "", path: str = ""):
@tornado.web.authenticated
async def post(self, drive: str = "", path: str = ""):
body = self.get_json_body()
result = await self._manager.new_file(drive, path, **body)
if 'location' in body:
result = await self._manager.new_drive(drive, **body)
else:
result = await self._manager.new_file(drive, path, **body)
self.finish(result)

@tornado.web.authenticated
Expand Down
40 changes: 31 additions & 9 deletions jupyter_drives/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,17 +445,22 @@ async def delete_file(self, drive_name, path):
try:
# eliminate leading and trailing backslashes
path = path.strip('/')
is_dir = await self._file_system._isdir(drive_name + '/' + path)
if is_dir == True:
await self._fix_dir(drive_name, path)
await self._file_system._rm(drive_name + '/' + path, recursive = True)
object_name = drive_name # in case we are only deleting the drive itself
if path != '':
# deleting objects within a drive
is_dir = await self._file_system._isdir(drive_name + '/' + path)
if is_dir == True:
await self._fix_dir(drive_name, path)
object_name = drive_name + '/' + path
await self._file_system._rm(object_name, recursive = True)

# checking for remaining directories and deleting them
stream = obs.list(self._content_managers[drive_name]["store"], path, chunk_size=100, return_arrow=True)
async for batch in stream:
contents_list = pyarrow.record_batch(batch).to_pylist()
for object in contents_list:
await self._fix_dir(drive_name, object["path"], delete_only = True)
if object_name != drive_name:
stream = obs.list(self._content_managers[drive_name]["store"], path, chunk_size=100, return_arrow=True)
async for batch in stream:
contents_list = pyarrow.record_batch(batch).to_pylist()
for object in contents_list:
await self._fix_dir(drive_name, object["path"], delete_only = True)

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

return

async def new_drive(self, new_drive_name, location='us-east-1'):
"""Create a new drive in the given location.

Args:
new_drive_name: name of new drive to create
location: (optional) region of bucket
"""
try:
await self._file_system._mkdir(new_drive_name, region_name = location)
except Exception as e:
raise tornado.web.HTTPError(
status_code= httpx.codes.BAD_REQUEST,
reason=f"The following error occured when creating the new drive: {e}",
)

return

async def _get_drive_location(self, drive_name):
"""Helping function for getting drive region.

Expand Down
81 changes: 56 additions & 25 deletions src/contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
countObjectNameAppearances,
renameObjects,
copyObjects,
presignedLink
presignedLink,
createDrive,
getDrivesList
} from './requests';

let data: Contents.IModel = {
Expand Down Expand Up @@ -244,28 +246,37 @@ export class Drive implements Contents.IDrive {
} else {
// retriving list of contents from root
// in our case: list available drives
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'
});
const drivesListInfo: Contents.IModel[] = [];
// fetch list of available drives
try {
this._drivesList = await getDrivesList();
for (const drive of this._drivesList) {
drivesListInfo.push({
name: drive.name,
path: drive.name,
last_modified: '',
created: drive.creationDate,
content: [],
format: 'json',
mimetype: '',
size: undefined,
writable: true,
type: 'directory'
});
}
} catch (error) {
console.log(
'Failed loading available drives list, with error: ',
error
);
}

data = {
name: this._name,
path: this._name,
last_modified: '',
created: '',
content: drivesList,
content: drivesListInfo,
format: 'json',
mimetype: '',
size: undefined,
Expand Down Expand Up @@ -390,16 +401,11 @@ export class Drive implements Contents.IDrive {
* @returns A promise which resolves when the file is deleted.
*/
async delete(localPath: string): Promise<void> {
if (localPath !== '') {
const currentDrive = extractCurrentDrive(localPath, this._drivesList);
const currentDrive = extractCurrentDrive(localPath, this._drivesList);

await deleteObjects(currentDrive.name, {
path: formatPath(localPath)
});
} else {
// create new element at root would mean modifying a drive
console.warn('Operation not supported.');
}
await deleteObjects(currentDrive.name, {
path: formatPath(localPath)
});

this._fileChanged.emit({
type: 'delete',
Expand Down Expand Up @@ -630,6 +636,31 @@ export class Drive implements Contents.IDrive {
return data;
}

/**
* Create a new drive.
*
* @param options: The options used to create the drive.
*
* @returns A promise which resolves with the contents model.
*/
async newDrive(
newDriveName: string,
region: string
): Promise<Contents.IModel> {
data = await createDrive(newDriveName, {
location: region
});

Contents.validateContentsModel(data);
this._fileChanged.emit({
type: 'new',
oldValue: null,
newValue: data
});

return data;
}

/**
* Create a checkpoint for a file.
*
Expand Down
115 changes: 114 additions & 1 deletion src/plugins/driveBrowserPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import { ITranslator } from '@jupyterlab/translation';
import {
createToolbarFactory,
IToolbarWidgetRegistry,
setToolbar
setToolbar,
showDialog,
Dialog
} from '@jupyterlab/apputils';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { FilenameSearcher, IScore } from '@jupyterlab/ui-components';
import { CommandRegistry } from '@lumino/commands';
import { Widget } from '@lumino/widgets';

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

/**
* The class name added to dialogs.
*/
const FILE_DIALOG_CLASS = 'jp-FileDialog';

/**
* The class name added for the new drive label in the creating new drive dialog.
*/
const CREATE_DRIVE_TITLE_CLASS = 'jp-new-drive-title';

/**
* The drives list provider.
*/
Expand Down Expand Up @@ -184,6 +197,9 @@ export const driveFileBrowser: JupyterFrontEndPlugin<void> = {

// Listen for your plugin setting changes using Signal
setting.changed.connect(loadSetting);

// Add commands
Private.addCommands(app, drive);
})
.catch(reason => {
console.error(
Expand Down Expand Up @@ -246,4 +262,101 @@ namespace Private {
};
router.routed.connect(listener);
}

/**
* Create the node for a creating a new drive handler.
*/
const createNewDriveNode = (newDriveName: string): 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');

const region = document.createElement('label');
region.textContent = 'Region';
region.className = CREATE_DRIVE_TITLE_CLASS;
const regionName = document.createElement('input');
regionName.placeholder = 'us-east-1';

body.appendChild(drive);
body.appendChild(driveName);
body.appendChild(region);
body.appendChild(regionName);
return body;
};

/**
* A widget used to create a new drive.
*/
export class CreateDriveHandler extends Widget {
/**
* Construct a new "create-drive" dialog.
*/
constructor(newDriveName: string) {
super({ node: createNewDriveNode(newDriveName) });
this.onAfterAttach();
}

protected onAfterAttach(): void {
this.addClass(FILE_DIALOG_CLASS);
const drive = this.driveInput.value;
this.driveInput.setSelectionRange(0, drive.length);
const region = this.regionInput.value;
this.regionInput.setSelectionRange(0, region.length);
}

/**
* Get the input text node for drive name.
*/
get driveInput(): HTMLInputElement {
return this.node.getElementsByTagName('input')[0] as HTMLInputElement;
}

/**
* Get the input text node for region.
*/
get regionInput(): HTMLInputElement {
return this.node.getElementsByTagName('input')[1] as HTMLInputElement;
}

/**
* Get the value of the widget.
*/
getValue(): string[] {
return [this.driveInput.value, this.regionInput.value];
}
}

export function addCommands(app: JupyterFrontEnd, drive: Drive): void {
app.commands.addCommand(CommandIDs.createNewDrive, {
execute: async () => {
return showDialog({
title: 'New Drive',
body: new Private.CreateDriveHandler(drive.name),
focusNodeSelector: 'input',
buttons: [
Dialog.cancelButton(),
Dialog.okButton({
label: 'Create',
ariaLabel: 'Create New Drive'
})
]
}).then(result => {
if (result.value) {
drive.newDrive(result.value[0], result.value[1]);
}
});
},
label: 'New Drive',
icon: driveBrowserIcon.bindprops({ stylesheet: 'menuItem' })
});

app.contextMenu.addItem({
command: CommandIDs.createNewDrive,
selector: '#drive-file-browser.jp-SidePanel .jp-DirListing-content',
rank: 100
});
}
}
33 changes: 33 additions & 0 deletions src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,39 @@ export const countObjectNameAppearances = async (
return counter;
};

/**
* Create a new drive.
*
* @param newDriveName The new drive name.
* @param options.location The region where drive should be located.
*
* @returns A promise which resolves with the contents model.
*/
export async function createDrive(
newDriveName: string,
options: {
location: string;
}
) {
await requestAPI<any>('drives/' + newDriveName + '/', 'POST', {
location: options.location
});

data = {
name: newDriveName,
path: newDriveName,
last_modified: '',
created: '',
content: [],
format: 'json',
mimetype: '',
size: 0,
writable: true,
type: 'directory'
};
return data;
}

namespace Private {
/**
* Helping function for renaming files inside
Expand Down
1 change: 1 addition & 0 deletions src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export namespace CommandIDs {
export const openDrivesDialog = 'drives:open-drives-dialog';
export const openPath = 'drives:open-path';
export const toggleBrowser = 'drives:toggle-main';
export const createNewDrive = 'drives:create-new-drive';
export const launcher = 'launcher:create';
}

Expand Down
Loading