mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-03-04 17:07:55 +00:00
432 lines
16 KiB
TypeScript
432 lines
16 KiB
TypeScript
import { makeNotebookRecord, Notebook, stringifyNotebook, toJS } from "@nteract/commutable";
|
|
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
|
import { from, Observable, of } from "rxjs";
|
|
import { AjaxResponse } from "rxjs/ajax";
|
|
import { HttpStatusCodes } from "../Common/Constants";
|
|
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
|
import * as Logger from "../Common/Logger";
|
|
import * as UrlUtility from "../Common/UrlUtility";
|
|
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
|
import * as Base64Utils from "../Utils/Base64Utils";
|
|
import * as GitHubUtils from "../Utils/GitHubUtils";
|
|
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
|
|
|
|
export interface GitHubContentProviderParams {
|
|
gitHubClient: GitHubClient;
|
|
promptForCommitMsg: (title: string, primaryButtonLabel: string) => Promise<string>;
|
|
}
|
|
|
|
class GitHubContentProviderError extends Error {
|
|
constructor(error: string, public errno: number = GitHubContentProvider.SelfErrorCode) {
|
|
super(error);
|
|
}
|
|
}
|
|
|
|
// Provides 'contents' API for GitHub
|
|
// http://jupyter-api.surge.sh/#!/contents
|
|
export class GitHubContentProvider implements IContentProvider {
|
|
public static readonly SelfErrorCode = 555;
|
|
|
|
constructor(private params: GitHubContentProviderParams) {}
|
|
|
|
public remove(_: ServerConfig, uri: string): Observable<AjaxResponse> {
|
|
return from(
|
|
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
|
try {
|
|
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Delete", "Delete");
|
|
const response = await this.params.gitHubClient.deleteFileAsync(content.data as IGitHubFile, commitMsg);
|
|
if (response.status !== HttpStatusCodes.OK) {
|
|
throw new GitHubContentProviderError("Failed to delete", response.status);
|
|
}
|
|
|
|
return this.createSuccessAjaxResponse(HttpStatusCodes.NoContent, undefined);
|
|
} catch (error) {
|
|
Logger.logError(getErrorMessage(error), "GitHubContentProvider/remove", error.errno);
|
|
return this.createErrorAjaxResponse(error);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
public get(_: ServerConfig, uri: string, params: Partial<IGetParams>): Observable<AjaxResponse> {
|
|
return from(
|
|
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
|
try {
|
|
if (content.status !== HttpStatusCodes.OK) {
|
|
throw new GitHubContentProviderError("Failed to get content", content.status);
|
|
}
|
|
|
|
if (!Array.isArray(content.data) && !content.data.content && params.content !== 0) {
|
|
const file = content.data;
|
|
file.content = (
|
|
await this.params.gitHubClient.getBlobAsync(file.repo.owner, file.repo.name, file.sha)
|
|
).data;
|
|
}
|
|
|
|
return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params));
|
|
} catch (error) {
|
|
Logger.logError(getErrorMessage(error), "GitHubContentProvider/get", error.errno);
|
|
return this.createErrorAjaxResponse(error);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
public update<FT extends FileType>(
|
|
_: ServerConfig,
|
|
uri: string,
|
|
model: Partial<IContent<FT>>
|
|
): Observable<AjaxResponse> {
|
|
return from(
|
|
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
|
try {
|
|
const gitHubFile = content.data as IGitHubFile;
|
|
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename");
|
|
const newUri = model.path;
|
|
const newPath = GitHubUtils.fromContentUri(newUri).path;
|
|
const response = await this.params.gitHubClient.renameFileAsync(
|
|
gitHubFile.repo.owner,
|
|
gitHubFile.repo.name,
|
|
gitHubFile.branch.name,
|
|
commitMsg,
|
|
gitHubFile.path,
|
|
newPath
|
|
);
|
|
if (response.status !== HttpStatusCodes.OK) {
|
|
throw new GitHubContentProviderError("Failed to rename", response.status);
|
|
}
|
|
|
|
gitHubFile.commit = response.data;
|
|
gitHubFile.path = newPath;
|
|
gitHubFile.name = NotebookUtil.getName(gitHubFile.path);
|
|
|
|
return this.createSuccessAjaxResponse(
|
|
HttpStatusCodes.OK,
|
|
this.createContentModel(newUri, gitHubFile, { content: 0 })
|
|
);
|
|
} catch (error) {
|
|
Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno);
|
|
return this.createErrorAjaxResponse(error);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
public create<FT extends FileType>(
|
|
_: ServerConfig,
|
|
uri: string,
|
|
model: Partial<IContent<FT>> & { type: FT }
|
|
): Observable<AjaxResponse> {
|
|
return from(
|
|
this.params.promptForCommitMsg("Create New Notebook", "Create").then(async (commitMsg: string) => {
|
|
try {
|
|
if (!commitMsg) {
|
|
throw new GitHubContentProviderError("Couldn't get a commit message");
|
|
}
|
|
|
|
if (model.type !== "notebook") {
|
|
throw new GitHubContentProviderError("Unsupported content type");
|
|
}
|
|
|
|
const contentInfo = GitHubUtils.fromContentUri(uri);
|
|
if (!contentInfo) {
|
|
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
|
}
|
|
|
|
const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord())));
|
|
const options: Intl.DateTimeFormatOptions = {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "numeric",
|
|
minute: "numeric",
|
|
second: "numeric",
|
|
hour12: false,
|
|
};
|
|
const name = `Untitled-${new Date().toLocaleString("default", options)}.ipynb`;
|
|
let path = name;
|
|
if (contentInfo.path) {
|
|
path = UrlUtility.createUri(contentInfo.path, name);
|
|
}
|
|
|
|
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
|
contentInfo.owner,
|
|
contentInfo.repo,
|
|
contentInfo.branch,
|
|
path,
|
|
commitMsg,
|
|
content
|
|
);
|
|
if (response.status !== HttpStatusCodes.Created) {
|
|
throw new GitHubContentProviderError("Failed to create", response.status);
|
|
}
|
|
|
|
const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
|
|
const newGitHubFile: IGitHubFile = {
|
|
type: "blob",
|
|
name: NotebookUtil.getName(newUri),
|
|
path,
|
|
repo: {
|
|
owner: contentInfo.owner,
|
|
name: contentInfo.repo,
|
|
private: undefined,
|
|
},
|
|
branch: {
|
|
name: contentInfo.branch,
|
|
},
|
|
commit: response.data,
|
|
};
|
|
|
|
return this.createSuccessAjaxResponse(
|
|
HttpStatusCodes.Created,
|
|
this.createContentModel(newUri, newGitHubFile, { content: 0 })
|
|
);
|
|
} catch (error) {
|
|
Logger.logError(getErrorMessage(error), "GitHubContentProvider/create", error.errno);
|
|
return this.createErrorAjaxResponse(error);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
public save<FT extends FileType>(
|
|
_: ServerConfig,
|
|
uri: string,
|
|
model: Partial<IContent<FT>>
|
|
): Observable<AjaxResponse> {
|
|
return from(
|
|
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
|
try {
|
|
let commitMsg: string;
|
|
if (content.status === HttpStatusCodes.NotFound) {
|
|
// We'll create a new file since it doesn't exist
|
|
commitMsg = await this.params.promptForCommitMsg("Save", "Save");
|
|
if (!commitMsg) {
|
|
throw new GitHubContentProviderError("Couldn't get a commit message");
|
|
}
|
|
} else {
|
|
commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
|
}
|
|
|
|
let updatedContent: string;
|
|
if (model.type === "notebook") {
|
|
updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook));
|
|
} else if (model.type === "file") {
|
|
updatedContent = model.content as string;
|
|
if (model.format !== "base64") {
|
|
updatedContent = Base64Utils.utf8ToB64(updatedContent);
|
|
}
|
|
} else {
|
|
throw new GitHubContentProviderError("Unsupported content type");
|
|
}
|
|
|
|
const contentInfo = GitHubUtils.fromContentUri(uri);
|
|
let gitHubFile: IGitHubFile;
|
|
if (content.data) {
|
|
gitHubFile = content.data as IGitHubFile;
|
|
}
|
|
|
|
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
|
contentInfo.owner,
|
|
contentInfo.repo,
|
|
contentInfo.branch,
|
|
contentInfo.path,
|
|
commitMsg,
|
|
updatedContent,
|
|
gitHubFile?.sha
|
|
);
|
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.Created) {
|
|
throw new GitHubContentProviderError("Failed to create or update", response.status);
|
|
}
|
|
|
|
if (gitHubFile) {
|
|
gitHubFile.commit = response.data;
|
|
} else {
|
|
const contentResponse = await this.params.gitHubClient.getContentsAsync(
|
|
contentInfo.owner,
|
|
contentInfo.repo,
|
|
contentInfo.branch,
|
|
contentInfo.path
|
|
);
|
|
if (contentResponse.status !== HttpStatusCodes.OK) {
|
|
throw new GitHubContentProviderError("Failed to get content", response.status);
|
|
}
|
|
|
|
gitHubFile = contentResponse.data as IGitHubFile;
|
|
}
|
|
|
|
return this.createSuccessAjaxResponse(
|
|
HttpStatusCodes.OK,
|
|
this.createContentModel(uri, gitHubFile, { content: 0 })
|
|
);
|
|
} catch (error) {
|
|
Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno);
|
|
return this.createErrorAjaxResponse(error);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
public listCheckpoints(): Observable<AjaxResponse> {
|
|
const error = new GitHubContentProviderError("Not implemented");
|
|
Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno);
|
|
return of(this.createErrorAjaxResponse(error));
|
|
}
|
|
|
|
public createCheckpoint(): Observable<AjaxResponse> {
|
|
const error = new GitHubContentProviderError("Not implemented");
|
|
Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno);
|
|
return of(this.createErrorAjaxResponse(error));
|
|
}
|
|
|
|
public deleteCheckpoint(): Observable<AjaxResponse> {
|
|
const error = new GitHubContentProviderError("Not implemented");
|
|
Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno);
|
|
return of(this.createErrorAjaxResponse(error));
|
|
}
|
|
|
|
public restoreFromCheckpoint(): Observable<AjaxResponse> {
|
|
const error = new GitHubContentProviderError("Not implemented");
|
|
Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno);
|
|
return of(this.createErrorAjaxResponse(error));
|
|
}
|
|
|
|
private async validateContentAndGetCommitMsg(
|
|
content: IGitHubResponse<IGitHubFile | IGitHubFile[]>,
|
|
promptTitle: string,
|
|
promptPrimaryButtonLabel: string
|
|
): Promise<string> {
|
|
if (content.status !== HttpStatusCodes.OK) {
|
|
throw new GitHubContentProviderError("Failed to get content", content.status);
|
|
}
|
|
|
|
if (Array.isArray(content.data)) {
|
|
throw new GitHubContentProviderError("Operation not supported for collections");
|
|
}
|
|
|
|
const commitMsg = await this.params.promptForCommitMsg(promptTitle, promptPrimaryButtonLabel);
|
|
if (!commitMsg) {
|
|
throw new GitHubContentProviderError("Couldn't get a commit message");
|
|
}
|
|
|
|
return commitMsg;
|
|
}
|
|
|
|
private async getContent(uri: string): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
|
|
const contentInfo = GitHubUtils.fromContentUri(uri);
|
|
if (contentInfo) {
|
|
const { owner, repo, branch, path } = contentInfo;
|
|
return this.params.gitHubClient.getContentsAsync(owner, repo, branch, path);
|
|
}
|
|
|
|
return Promise.resolve({ status: GitHubContentProvider.SelfErrorCode, data: undefined });
|
|
}
|
|
|
|
private createContentModel(
|
|
uri: string,
|
|
content: IGitHubFile | IGitHubFile[],
|
|
params: Partial<IGetParams>
|
|
): IContent<FileType> {
|
|
if (Array.isArray(content)) {
|
|
return this.createDirectoryModel(uri, content);
|
|
}
|
|
|
|
if (content.type === "tree") {
|
|
return this.createDirectoryModel(uri, undefined);
|
|
}
|
|
|
|
if (NotebookUtil.isNotebookFile(uri)) {
|
|
return this.createNotebookModel(content, params);
|
|
}
|
|
|
|
return this.createFileModel(content, params);
|
|
}
|
|
|
|
private createDirectoryModel(uri: string, gitHubFiles: IGitHubFile[] | undefined): IContent<"directory"> {
|
|
return {
|
|
name: NotebookUtil.getName(uri),
|
|
path: uri,
|
|
type: "directory",
|
|
writable: true, // TODO: tamitta: we don't know this info here
|
|
created: "", // TODO: tamitta: we don't know this info here
|
|
last_modified: "", // TODO: tamitta: we don't know this info here
|
|
mimetype: undefined,
|
|
content: gitHubFiles?.map(
|
|
(file: IGitHubFile) =>
|
|
this.createContentModel(
|
|
GitHubUtils.toContentUri(file.repo.owner, file.repo.name, file.branch.name, file.path),
|
|
file,
|
|
{
|
|
content: 0,
|
|
}
|
|
) as IEmptyContent<FileType>
|
|
),
|
|
format: "json",
|
|
};
|
|
}
|
|
|
|
private createNotebookModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"notebook"> {
|
|
const content: Notebook = gitHubFile.content && params.content !== 0 ? JSON.parse(gitHubFile.content) : undefined;
|
|
return {
|
|
name: gitHubFile.name,
|
|
path: GitHubUtils.toContentUri(
|
|
gitHubFile.repo.owner,
|
|
gitHubFile.repo.name,
|
|
gitHubFile.branch.name,
|
|
gitHubFile.path
|
|
),
|
|
type: "notebook",
|
|
writable: true, // TODO: tamitta: we don't know this info here
|
|
created: "", // TODO: tamitta: we don't know this info here
|
|
last_modified: gitHubFile.commit.commitDate,
|
|
mimetype: content ? "application/x-ipynb+json" : undefined,
|
|
content,
|
|
format: content ? "json" : undefined,
|
|
};
|
|
}
|
|
|
|
private createFileModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"file"> {
|
|
const content: string = gitHubFile.content && params.content !== 0 ? gitHubFile.content : undefined;
|
|
return {
|
|
name: gitHubFile.name,
|
|
path: GitHubUtils.toContentUri(
|
|
gitHubFile.repo.owner,
|
|
gitHubFile.repo.name,
|
|
gitHubFile.branch.name,
|
|
gitHubFile.path
|
|
),
|
|
type: "file",
|
|
writable: true, // TODO: tamitta: we don't know this info here
|
|
created: "", // TODO: tamitta: we don't know this info here
|
|
last_modified: gitHubFile.commit.commitDate,
|
|
mimetype: content ? "text/plain" : undefined,
|
|
content,
|
|
format: content ? "text" : undefined,
|
|
};
|
|
}
|
|
|
|
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): AjaxResponse {
|
|
return {
|
|
originalEvent: new Event("no-op"),
|
|
xhr: new XMLHttpRequest(),
|
|
request: {},
|
|
status,
|
|
response: content ? content : undefined,
|
|
responseText: content ? JSON.stringify(content) : undefined,
|
|
responseType: "json",
|
|
};
|
|
}
|
|
|
|
private createErrorAjaxResponse(error: GitHubContentProviderError): AjaxResponse {
|
|
return {
|
|
originalEvent: new Event("no-op"),
|
|
xhr: new XMLHttpRequest(),
|
|
request: {},
|
|
status: error.errno,
|
|
response: error,
|
|
responseText: getErrorMessage(error),
|
|
responseType: "json",
|
|
};
|
|
}
|
|
}
|