2020-05-25 21:30:55 -05:00
|
|
|
import * as DataModels from "../../Contracts/DataModels";
|
|
|
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
2021-03-09 04:50:11 +05:30
|
|
|
import * as StringUtils from "../../Utils/StringUtils";
|
2020-05-25 21:30:55 -05:00
|
|
|
import { FileSystemUtil } from "./FileSystemUtil";
|
|
|
|
import { NotebookUtil } from "./NotebookUtil";
|
|
|
|
|
|
|
|
import { ServerConfig, IContent, IContentProvider, FileType, IEmptyContent } from "@nteract/core";
|
|
|
|
import { AjaxResponse } from "rxjs/ajax";
|
|
|
|
import { stringifyNotebook } from "@nteract/commutable";
|
|
|
|
|
2020-07-27 16:05:25 -05:00
|
|
|
export class NotebookContentClient {
|
2020-05-25 21:30:55 -05:00
|
|
|
constructor(
|
|
|
|
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
|
2021-01-12 12:55:21 -06:00
|
|
|
public notebookBasePath: ko.Observable<string>,
|
2020-05-25 21:30:55 -05:00
|
|
|
private contentProvider: IContentProvider
|
|
|
|
) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This updates the item and points all the children's parent to this item
|
|
|
|
* @param item
|
|
|
|
*/
|
|
|
|
public updateItemChildren(item: NotebookContentItem): Promise<void> {
|
2021-01-20 09:15:01 -06:00
|
|
|
return this.fetchNotebookFiles(item.path).then((subItems) => {
|
2020-05-25 21:30:55 -05:00
|
|
|
item.children = subItems;
|
2021-01-20 09:15:01 -06:00
|
|
|
subItems.forEach((subItem) => (subItem.parent = item));
|
2020-05-25 21:30:55 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param parent parent folder
|
|
|
|
*/
|
|
|
|
public createNewNotebookFile(parent: NotebookContentItem): Promise<NotebookContentItem> {
|
|
|
|
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
|
|
|
throw new Error(`Parent must be a directory: ${parent}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const type = "notebook";
|
|
|
|
return this.contentProvider
|
|
|
|
.create<"notebook">(this.getServerConfig(), parent.path, { type })
|
|
|
|
.toPromise()
|
|
|
|
.then((xhr: AjaxResponse) => {
|
|
|
|
if (typeof xhr.response === "string") {
|
|
|
|
throw new Error(`jupyter server response invalid: ${xhr.response}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (xhr.response.type !== type) {
|
|
|
|
throw new Error(`jupyter server response not for notebook: ${xhr.response}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const notebookFile = xhr.response;
|
|
|
|
|
|
|
|
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
|
|
|
|
if (parent.children) {
|
|
|
|
item.parent = parent;
|
|
|
|
parent.children.push(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public deleteContentItem(item: NotebookContentItem): Promise<void> {
|
|
|
|
return this.deleteNotebookFile(item.path).then((path: string) => {
|
|
|
|
if (!path || path !== item.path) {
|
|
|
|
throw new Error("No path provided");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item.parent && item.parent.children) {
|
|
|
|
// Remove deleted child
|
2021-01-20 09:15:01 -06:00
|
|
|
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
2020-05-25 21:30:55 -05:00
|
|
|
item.parent.children = newChildren;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param name file name
|
|
|
|
* @param content file content string
|
|
|
|
* @param parent parent folder
|
|
|
|
*/
|
|
|
|
public async uploadFileAsync(
|
|
|
|
name: string,
|
|
|
|
content: string,
|
|
|
|
parent: NotebookContentItem
|
|
|
|
): Promise<NotebookContentItem> {
|
|
|
|
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
|
|
|
throw new Error(`Parent must be a directory: ${parent}`);
|
|
|
|
}
|
|
|
|
|
2020-08-11 09:27:57 -07:00
|
|
|
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
2020-05-25 21:30:55 -05:00
|
|
|
if (await this.checkIfFilepathExists(filepath)) {
|
|
|
|
throw new Error(`File already exists: ${filepath}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const model: Partial<IContent<"file">> = {
|
|
|
|
content,
|
|
|
|
format: "text",
|
|
|
|
name,
|
2021-01-20 09:15:01 -06:00
|
|
|
type: "file",
|
2020-05-25 21:30:55 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
return this.contentProvider
|
|
|
|
.save(this.getServerConfig(), filepath, model)
|
|
|
|
.toPromise()
|
|
|
|
.then((xhr: AjaxResponse) => {
|
|
|
|
const notebookFile = xhr.response;
|
|
|
|
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
|
|
|
|
if (parent.children) {
|
|
|
|
item.parent = parent;
|
|
|
|
parent.children.push(item);
|
|
|
|
}
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
2020-08-11 09:27:57 -07:00
|
|
|
const parentDirPath = NotebookUtil.getParentPath(filepath);
|
2021-01-12 12:55:21 -06:00
|
|
|
if (parentDirPath) {
|
|
|
|
const items = await this.fetchNotebookFiles(parentDirPath);
|
2021-01-20 09:15:01 -06:00
|
|
|
return items.some((value) => FileSystemUtil.isPathEqual(value.path, filepath));
|
2021-01-12 12:55:21 -06:00
|
|
|
}
|
|
|
|
return false;
|
2020-05-25 21:30:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param sourcePath
|
|
|
|
* @param targetName is not prefixed with path
|
|
|
|
*/
|
|
|
|
public renameNotebook(item: NotebookContentItem, targetName: string): Promise<NotebookContentItem> {
|
|
|
|
const sourcePath = item.path;
|
|
|
|
// Match extension
|
|
|
|
if (sourcePath.indexOf(".") !== -1) {
|
|
|
|
const extension = `.${sourcePath.split(".").pop()}`;
|
|
|
|
if (!StringUtils.endsWith(targetName, extension)) {
|
|
|
|
targetName += extension;
|
|
|
|
}
|
|
|
|
}
|
2020-05-26 13:53:41 -05:00
|
|
|
const targetPath = NotebookUtil.replaceName(sourcePath, targetName);
|
2020-05-25 21:30:55 -05:00
|
|
|
return this.contentProvider
|
|
|
|
.update<"file" | "notebook" | "directory">(this.getServerConfig(), sourcePath, { path: targetPath })
|
|
|
|
.toPromise()
|
2021-01-20 09:15:01 -06:00
|
|
|
.then((xhr) => {
|
2020-05-25 21:30:55 -05:00
|
|
|
if (typeof xhr.response === "string") {
|
|
|
|
throw new Error(`jupyter server response invalid: ${xhr.response}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (xhr.response.type !== "file" && xhr.response.type !== "notebook" && xhr.response.type !== "directory") {
|
|
|
|
throw new Error(`jupyter server response not for notebook/file/directory: ${xhr.response}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const notebookFile = xhr.response;
|
|
|
|
item.name = notebookFile.name;
|
|
|
|
item.path = notebookFile.path;
|
|
|
|
item.timestamp = NotebookUtil.getCurrentTimestamp();
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param parent
|
|
|
|
* @param newDirectoryName basename of the new directory
|
|
|
|
*/
|
|
|
|
public async createDirectory(parent: NotebookContentItem, newDirectoryName: string): Promise<NotebookContentItem> {
|
|
|
|
if (parent.type !== NotebookContentItemType.Directory) {
|
|
|
|
throw new Error(`Parent is not a directory: ${parent.path}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const targetPath = `${parent.path}/${newDirectoryName}`;
|
|
|
|
|
|
|
|
// Reject if already exists
|
|
|
|
if (await this.checkIfFilepathExists(targetPath)) {
|
|
|
|
throw new Error(`Directory already exists: ${targetPath}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const type = "directory";
|
|
|
|
return this.contentProvider
|
|
|
|
.save<"directory">(this.getServerConfig(), targetPath, { type, path: targetPath })
|
|
|
|
.toPromise()
|
|
|
|
.then((xhr: AjaxResponse) => {
|
|
|
|
if (typeof xhr.response === "string") {
|
|
|
|
throw new Error(`jupyter server response invalid: ${xhr.response}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (xhr.response.type !== type) {
|
|
|
|
throw new Error(`jupyter server response not for creating directory: ${xhr.response}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const dir = xhr.response;
|
|
|
|
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
|
|
|
|
item.parent = parent;
|
2021-01-13 17:49:06 -06:00
|
|
|
parent.children?.push(item);
|
2020-05-25 21:30:55 -05:00
|
|
|
return item;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-09-11 19:37:00 -07:00
|
|
|
public async readFileContent(filePath: string): Promise<string> {
|
|
|
|
const xhr = await this.contentProvider.get(this.getServerConfig(), filePath, { content: 1 }).toPromise();
|
|
|
|
const content = (xhr.response as any).content;
|
|
|
|
if (!content) {
|
|
|
|
throw new Error("No content read");
|
|
|
|
}
|
|
|
|
|
|
|
|
const format = (xhr.response as any).format;
|
|
|
|
switch (format) {
|
|
|
|
case "text":
|
|
|
|
return content;
|
|
|
|
case "base64":
|
|
|
|
return atob(content);
|
|
|
|
case "json":
|
2020-05-25 21:30:55 -05:00
|
|
|
return stringifyNotebook(content);
|
2020-09-11 19:37:00 -07:00
|
|
|
default:
|
|
|
|
throw new Error(`Unsupported content format ${format}`);
|
|
|
|
}
|
2020-05-25 21:30:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
private deleteNotebookFile(path: string): Promise<string> {
|
|
|
|
return this.contentProvider
|
|
|
|
.remove(this.getServerConfig(), path)
|
|
|
|
.toPromise()
|
|
|
|
.then((xhr: AjaxResponse) => path);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert rx-jupyter type to our type
|
|
|
|
* @param type
|
|
|
|
*/
|
2021-01-12 12:55:21 -06:00
|
|
|
public static getType(type: FileType): NotebookContentItemType {
|
2020-05-25 21:30:55 -05:00
|
|
|
switch (type) {
|
|
|
|
case "directory":
|
|
|
|
return NotebookContentItemType.Directory;
|
|
|
|
case "notebook":
|
|
|
|
return NotebookContentItemType.Notebook;
|
|
|
|
case "file":
|
|
|
|
return NotebookContentItemType.File;
|
|
|
|
default:
|
|
|
|
throw new Error(`Unknown file type: ${type}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fetchNotebookFiles(path: string): Promise<NotebookContentItem[]> {
|
|
|
|
return this.contentProvider
|
|
|
|
.get(this.getServerConfig(), path, {
|
2021-01-20 09:15:01 -06:00
|
|
|
type: "directory",
|
2020-05-25 21:30:55 -05:00
|
|
|
})
|
|
|
|
.toPromise()
|
2021-01-20 09:15:01 -06:00
|
|
|
.then((xhr) => {
|
2020-05-25 21:30:55 -05:00
|
|
|
if (xhr.status !== 200) {
|
|
|
|
throw new Error(JSON.stringify(xhr.response));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof xhr.response === "string") {
|
|
|
|
throw new Error(`jupyter server response invalid: ${xhr.response}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (xhr.response.type !== "directory") {
|
|
|
|
throw new Error(`jupyter server response not for directory: ${xhr.response}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const list = xhr.response.content as IEmptyContent<FileType>[];
|
|
|
|
return list.map(
|
|
|
|
(item: IEmptyContent<FileType>): NotebookContentItem => ({
|
|
|
|
name: item.name,
|
|
|
|
path: item.path,
|
2021-01-20 09:15:01 -06:00
|
|
|
type: NotebookUtil.getType(item.type),
|
2020-05-25 21:30:55 -05:00
|
|
|
})
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private getServerConfig(): ServerConfig {
|
|
|
|
return {
|
|
|
|
endpoint: this.notebookServerInfo().notebookServerEndpoint,
|
|
|
|
token: this.notebookServerInfo().authToken,
|
2021-01-20 09:15:01 -06:00
|
|
|
crossDomain: true,
|
2020-05-25 21:30:55 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|