1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import { URLExt, PathExt } from '@jupyterlab/coreutils';
- import { ModelDB } from '@jupyterlab/observables';
- import { JSONObject } from '@phosphor/coreutils';
- import { each } from '@phosphor/algorithm';
- import { IDisposable } from '@phosphor/disposable';
- import { ISignal, Signal } from '@phosphor/signaling';
- import { ServerConnection } from '..';
- import * as validate from './validate';
- /**
- * The url for the default drive service.
- */
- const SERVICE_DRIVE_URL = 'api/contents';
- /**
- * The url for the file access.
- */
- const FILES_URL = 'files';
- /**
- * A namespace for contents interfaces.
- */
- export namespace Contents {
- /**
- * A contents model.
- */
- export interface IModel {
- /**
- * Name of the contents file.
- *
- * #### Notes
- * Equivalent to the last part of the `path` field.
- */
- readonly name: string;
- /**
- * The full file path.
- *
- * #### Notes
- * It will *not* start with `/`, and it will be `/`-delimited.
- */
- readonly path: string;
- /**
- * The type of file.
- */
- readonly type: ContentType;
- /**
- * Whether the requester has permission to edit the file.
- */
- readonly writable: boolean;
- /**
- * File creation timestamp.
- */
- readonly created: string;
- /**
- * Last modified timestamp.
- */
- readonly last_modified: string;
- /**
- * Specify the mime-type of file contents.
- *
- * #### Notes
- * Only non-`null` when `content` is present and `type` is `"file"`.
- */
- readonly mimetype: string;
- /**
- * The optional file content.
- */
- readonly content: any;
- /**
- * The chunk of the file upload.
- */
- readonly chunk?: number;
- /**
- * The format of the file `content`.
- *
- * #### Notes
- * Only relevant for type: 'file'
- */
- readonly format: FileFormat;
- }
- /**
- * Validates an IModel, thowing an error if it does not pass.
- */
- export function validateContentsModel(contents: IModel): void {
- validate.validateContentsModel(contents);
- }
- /**
- * A contents file type.
- */
- export type ContentType = 'notebook' | 'file' | 'directory';
- /**
- * A contents file format.
- */
- export type FileFormat = 'json' | 'text' | 'base64';
- /**
- * The options used to fetch a file.
- */
- export interface IFetchOptions {
- /**
- * The override file type for the request.
- */
- type?: ContentType;
- /**
- * The override file format for the request.
- */
- format?: FileFormat;
- /**
- * Whether to include the file content.
- *
- * The default is `true`.
- */
- content?: boolean;
- }
- /**
- * The options used to create a file.
- */
- export interface ICreateOptions {
- /**
- * The directory in which to create the file.
- */
- path?: string;
- /**
- * The optional file extension for the new file (e.g. `".txt"`).
- *
- * #### Notes
- * This ignored if `type` is `'notebook'`.
- */
- ext?: string;
- /**
- * The file type.
- */
- type?: ContentType;
- }
- /**
- * Checkpoint model.
- */
- export interface ICheckpointModel {
- /**
- * The unique identifier for the checkpoint.
- */
- readonly id: string;
- /**
- * Last modified timestamp.
- */
- readonly last_modified: string;
- }
- /**
- * Validates an ICheckpointModel, thowing an error if it does not pass.
- */
- export function validateCheckpointModel(checkpoint: ICheckpointModel): void {
- validate.validateCheckpointModel(checkpoint);
- }
- /**
- * The change args for a file change.
- */
- export interface IChangedArgs {
- /**
- * The type of change.
- */
- type: 'new' | 'delete' | 'rename' | 'save';
- /**
- * The new contents.
- */
- oldValue: Partial<IModel> | null;
- /**
- * The old contents.
- */
- newValue: Partial<IModel> | null;
- }
- /**
- * The interface for a contents manager.
- */
- export interface IManager extends IDisposable {
- /**
- * A signal emitted when a file operation takes place.
- */
- readonly fileChanged: ISignal<IManager, IChangedArgs>;
- /**
- * The server settings associated with the manager.
- */
- readonly serverSettings: ServerConnection.ISettings;
- /**
- * Add an `IDrive` to the manager.
- */
- addDrive(drive: IDrive): void;
- /**
- * Given a path of the form `drive:local/portion/of/it.txt`
- * get the local part of it.
- *
- * @param path: the path.
- *
- * @returns The local part of the path.
- */
- localPath(path: string): string;
- /**
- * Normalize a global path. Reduces '..' and '.' parts, and removes
- * leading slashes from the local part of the path, while retaining
- * the drive name if it exists.
- *
- * @param path: the path.
- *
- * @returns The normalized path.
- */
- normalize(path: string): string;
- /**
- * Given a path of the form `drive:local/portion/of/it.txt`
- * get the name of the drive. If the path is missing
- * a drive portion, returns an empty string.
- *
- * @param path: the path.
- *
- * @returns The drive name for the path, or the empty string.
- */
- driveName(path: string): string;
- /**
- * Given a path, get a ModelDB.IFactory from the
- * relevant backend. Returns `null` if the backend
- * does not provide one.
- */
- getModelDBFactory(path: string): ModelDB.IFactory | null;
- /**
- * Get a file or directory.
- *
- * @param path: The path to the file.
- *
- * @param options: The options used to fetch the file.
- *
- * @returns A promise which resolves with the file content.
- */
- get(path: string, options?: IFetchOptions): Promise<IModel>;
- /**
- * Get an encoded download url given a file path.
- *
- * @param A promise which resolves with the absolute POSIX
- * file path on the server.
- */
- getDownloadUrl(path: string): Promise<string>;
- /**
- * Create a new untitled file or directory in the specified directory path.
- *
- * @param options: The options used to create the file.
- *
- * @returns A promise which resolves with the created file content when the
- * file is created.
- */
- newUntitled(options?: ICreateOptions): Promise<IModel>;
- /**
- * Delete a file.
- *
- * @param path - The path to the file.
- *
- * @returns A promise which resolves when the file is deleted.
- */
- delete(path: string): Promise<void>;
- /**
- * Rename a file or directory.
- *
- * @param path - The original file path.
- *
- * @param newPath - The new file path.
- *
- * @returns A promise which resolves with the new file content model when the
- * file is renamed.
- */
- rename(path: string, newPath: string): Promise<IModel>;
- /**
- * Save a file.
- *
- * @param path - The desired file path.
- *
- * @param options - Optional overrides to the model.
- *
- * @returns A promise which resolves with the file content model when the
- * file is saved.
- */
- save(path: string, options?: Partial<IModel>): Promise<IModel>;
- /**
- * Copy a file into a given directory.
- *
- * @param path - The original file path.
- *
- * @param toDir - The destination directory path.
- *
- * @returns A promise which resolves with the new content model when the
- * file is copied.
- */
- copy(path: string, toDir: string): Promise<IModel>;
- /**
- * Create a checkpoint for a file.
- *
- * @param path - The path of the file.
- *
- * @returns A promise which resolves with the new checkpoint model when the
- * checkpoint is created.
- */
- createCheckpoint(path: string): Promise<ICheckpointModel>;
- /**
- * List available checkpoints for a file.
- *
- * @param path - The path of the file.
- *
- * @returns A promise which resolves with a list of checkpoint models for
- * the file.
- */
- listCheckpoints(path: string): Promise<ICheckpointModel[]>;
- /**
- * Restore a file to a known checkpoint state.
- *
- * @param path - The path of the file.
- *
- * @param checkpointID - The id of the checkpoint to restore.
- *
- * @returns A promise which resolves when the checkpoint is restored.
- */
- restoreCheckpoint(path: string, checkpointID: string): Promise<void>;
- /**
- * Delete a checkpoint for a file.
- *
- * @param path - The path of the file.
- *
- * @param checkpointID - The id of the checkpoint to delete.
- *
- * @returns A promise which resolves when the checkpoint is deleted.
- */
- deleteCheckpoint(path: string, checkpointID: string): Promise<void>;
- }
- /**
- * The interface for a network drive that can be mounted
- * in the contents manager.
- */
- export interface IDrive extends IDisposable {
- /**
- * The name of the drive, which is used at the leading
- * component of file paths.
- */
- readonly name: string;
- /**
- * The server settings of the manager.
- */
- readonly serverSettings: ServerConnection.ISettings;
- /**
- * An optional ModelDB.IFactory instance for the
- * drive.
- */
- readonly modelDBFactory?: ModelDB.IFactory;
- /**
- * A signal emitted when a file operation takes place.
- */
- fileChanged: ISignal<IDrive, IChangedArgs>;
- /**
- * Get a file or directory.
- *
- * @param localPath: The path to the file.
- *
- * @param options: The options used to fetch the file.
- *
- * @returns A promise which resolves with the file content.
- */
- get(localPath: string, options?: IFetchOptions): Promise<IModel>;
- /**
- * Get an encoded download url given a file path.
- *
- * @param A promise which resolves with the absolute POSIX
- * file path on the server.
- */
- getDownloadUrl(localPath: string): Promise<string>;
- /**
- * Create a new untitled file or directory in the specified directory path.
- *
- * @param options: The options used to create the file.
- *
- * @returns A promise which resolves with the created file content when the
- * file is created.
- */
- newUntitled(options?: ICreateOptions): Promise<IModel>;
- /**
- * Delete a file.
- *
- * @param localPath - The path to the file.
- *
- * @returns A promise which resolves when the file is deleted.
- */
- delete(localPath: string): Promise<void>;
- /**
- * Rename a file or directory.
- *
- * @param oldLocalPath - The original file path.
- *
- * @param newLocalPath - The new file path.
- *
- * @returns A promise which resolves with the new file content model when the
- * file is renamed.
- */
- rename(oldLocalPath: string, newLocalPath: string): Promise<IModel>;
- /**
- * Save a file.
- *
- * @param localPath - The desired file path.
- *
- * @param options - Optional overrides to the model.
- *
- * @returns A promise which resolves with the file content model when the
- * file is saved.
- */
- save(localPath: string, options?: Partial<IModel>): Promise<IModel>;
- /**
- * Copy a file into a given directory.
- *
- * @param localPath - The original file path.
- *
- * @param toLocalDir - The destination directory path.
- *
- * @returns A promise which resolves with the new content model when the
- * file is copied.
- */
- copy(localPath: string, toLocalDir: string): Promise<IModel>;
- /**
- * Create a checkpoint for a file.
- *
- * @param localPath - The path of the file.
- *
- * @returns A promise which resolves with the new checkpoint model when the
- * checkpoint is created.
- */
- createCheckpoint(localPath: string): Promise<ICheckpointModel>;
- /**
- * List available checkpoints for a file.
- *
- * @param localPath - The path of the file.
- *
- * @returns A promise which resolves with a list of checkpoint models for
- * the file.
- */
- listCheckpoints(localPath: string): Promise<ICheckpointModel[]>;
- /**
- * Restore a file to a known checkpoint state.
- *
- * @param localPath - The path of the file.
- *
- * @param checkpointID - The id of the checkpoint to restore.
- *
- * @returns A promise which resolves when the checkpoint is restored.
- */
- restoreCheckpoint(localPath: string, checkpointID: string): Promise<void>;
- /**
- * Delete a checkpoint for a file.
- *
- * @param localPath - The path of the file.
- *
- * @param checkpointID - The id of the checkpoint to delete.
- *
- * @returns A promise which resolves when the checkpoint is deleted.
- */
- deleteCheckpoint(localPath: string, checkpointID: string): Promise<void>;
- }
- }
- /**
- * A contents manager that passes file operations to the server.
- * Multiple servers implementing the `IDrive` interface can be
- * attached to the contents manager, so that the same session can
- * perform file operations on multiple backends.
- *
- * This includes checkpointing with the normal file operations.
- */
- export class ContentsManager implements Contents.IManager {
- /**
- * Construct a new contents manager object.
- *
- * @param options - The options used to initialize the object.
- */
- constructor(options: ContentsManager.IOptions = {}) {
- let serverSettings = (this.serverSettings =
- options.serverSettings || ServerConnection.makeSettings());
- this._defaultDrive = options.defaultDrive || new Drive({ serverSettings });
- this._defaultDrive.fileChanged.connect(this._onFileChanged, this);
- }
- /**
- * The server settings associated with the manager.
- */
- readonly serverSettings: ServerConnection.ISettings;
- /**
- * A signal emitted when a file operation takes place.
- */
- get fileChanged(): ISignal<this, Contents.IChangedArgs> {
- return this._fileChanged;
- }
- /**
- * Test whether the manager has been disposed.
- */
- get isDisposed(): boolean {
- return this._isDisposed;
- }
- /**
- * Dispose of the resources held by the manager.
- */
- dispose(): void {
- if (this.isDisposed) {
- return;
- }
- this._isDisposed = true;
- Signal.clearData(this);
- }
- /**
- * Add an `IDrive` to the manager.
- */
- addDrive(drive: Contents.IDrive): void {
- this._additionalDrives.set(drive.name, drive);
- drive.fileChanged.connect(this._onFileChanged, this);
- }
- /**
- * Given a path, get a ModelDB.IFactory from the
- * relevant backend. Returns `null` if the backend
- * does not provide one.
- */
- getModelDBFactory(path: string): ModelDB.IFactory | null {
- let [drive] = this._driveForPath(path);
- return (drive && drive.modelDBFactory) || null;
- }
- /**
- * Given a path of the form `drive:local/portion/of/it.txt`
- * get the local part of it.
- *
- * @param path: the path.
- *
- * @returns The local part of the path.
- */
- localPath(path: string): string {
- const parts = path.split('/');
- const firstParts = parts[0].split(':');
- if (firstParts.length === 1 || !this._additionalDrives.has(firstParts[0])) {
- return PathExt.removeSlash(path);
- }
- return PathExt.join(firstParts.slice(1).join(':'), ...parts.slice(1));
- }
- /**
- * Normalize a global path. Reduces '..' and '.' parts, and removes
- * leading slashes from the local part of the path, while retaining
- * the drive name if it exists.
- *
- * @param path: the path.
- *
- * @returns The normalized path.
- */
- normalize(path: string): string {
- const parts = path.split(':');
- if (parts.length === 1) {
- return PathExt.normalize(path);
- }
- return `${parts[0]}:${PathExt.normalize(parts.slice(1).join(':'))}`;
- }
- /**
- * Given a path of the form `drive:local/portion/of/it.txt`
- * get the name of the drive. If the path is missing
- * a drive portion, returns an empty string.
- *
- * @param path: the path.
- *
- * @returns The drive name for the path, or the empty string.
- */
- driveName(path: string): string {
- const parts = path.split('/');
- const firstParts = parts[0].split(':');
- if (firstParts.length === 1) {
- return '';
- }
- if (this._additionalDrives.has(firstParts[0])) {
- return firstParts[0];
- }
- return '';
- }
- /**
- * Get a file or directory.
- *
- * @param path: The path to the file.
- *
- * @param options: The options used to fetch the file.
- *
- * @returns A promise which resolves with the file content.
- */
- get(
- path: string,
- options?: Contents.IFetchOptions
- ): Promise<Contents.IModel> {
- let [drive, localPath] = this._driveForPath(path);
- return drive.get(localPath, options).then(contentsModel => {
- let listing: Contents.IModel[] = [];
- if (contentsModel.type === 'directory' && contentsModel.content) {
- each(contentsModel.content, (item: Contents.IModel) => {
- listing.push({
- ...item,
- path: this._toGlobalPath(drive, item.path)
- } as Contents.IModel);
- });
- return {
- ...contentsModel,
- path: this._toGlobalPath(drive, localPath),
- content: listing
- } as Contents.IModel;
- } else {
- return {
- ...contentsModel,
- path: this._toGlobalPath(drive, localPath)
- } as Contents.IModel;
- }
- });
- }
- /**
- * Get an encoded download url given a file path.
- *
- * @param path - An absolute POSIX file path on the server.
- *
- * #### Notes
- * It is expected that the path contains no relative paths.
- */
- getDownloadUrl(path: string): Promise<string> {
- let [drive, localPath] = this._driveForPath(path);
- return drive.getDownloadUrl(localPath);
- }
- /**
- * Create a new untitled file or directory in the specified directory path.
- *
- * @param options: The options used to create the file.
- *
- * @returns A promise which resolves with the created file content when the
- * file is created.
- */
- newUntitled(options: Contents.ICreateOptions = {}): Promise<Contents.IModel> {
- if (options.path) {
- let globalPath = this.normalize(options.path);
- let [drive, localPath] = this._driveForPath(globalPath);
- return drive
- .newUntitled({ ...options, path: localPath })
- .then(contentsModel => {
- return {
- ...contentsModel,
- path: PathExt.join(globalPath, contentsModel.name)
- } as Contents.IModel;
- });
- } else {
- return this._defaultDrive.newUntitled(options);
- }
- }
- /**
- * Delete a file.
- *
- * @param path - The path to the file.
- *
- * @returns A promise which resolves when the file is deleted.
- */
- delete(path: string): Promise<void> {
- let [drive, localPath] = this._driveForPath(path);
- return drive.delete(localPath);
- }
- /**
- * Rename a file or directory.
- *
- * @param path - The original file path.
- *
- * @param newPath - The new file path.
- *
- * @returns A promise which resolves with the new file contents model when
- * the file is renamed.
- */
- rename(path: string, newPath: string): Promise<Contents.IModel> {
- let [drive1, path1] = this._driveForPath(path);
- let [drive2, path2] = this._driveForPath(newPath);
- if (drive1 !== drive2) {
- throw Error('ContentsManager: renaming files must occur within a Drive');
- }
- return drive1.rename(path1, path2).then(contentsModel => {
- return {
- ...contentsModel,
- path: this._toGlobalPath(drive1, path2)
- } as Contents.IModel;
- });
- }
- /**
- * Save a file.
- *
- * @param path - The desired file path.
- *
- * @param options - Optional overrides to the model.
- *
- * @returns A promise which resolves with the file content model when the
- * file is saved.
- *
- * #### Notes
- * Ensure that `model.content` is populated for the file.
- */
- save(
- path: string,
- options: Partial<Contents.IModel> = {}
- ): Promise<Contents.IModel> {
- const globalPath = this.normalize(path);
- const [drive, localPath] = this._driveForPath(path);
- return drive
- .save(localPath, { ...options, path: localPath })
- .then(contentsModel => {
- return { ...contentsModel, path: globalPath } as Contents.IModel;
- });
- }
- /**
- * Copy a file into a given directory.
- *
- * @param path - The original file path.
- *
- * @param toDir - The destination directory path.
- *
- * @returns A promise which resolves with the new contents model when the
- * file is copied.
- *
- * #### Notes
- * The server will select the name of the copied file.
- */
- copy(fromFile: string, toDir: string): Promise<Contents.IModel> {
- let [drive1, path1] = this._driveForPath(fromFile);
- let [drive2, path2] = this._driveForPath(toDir);
- if (drive1 === drive2) {
- return drive1.copy(path1, path2).then(contentsModel => {
- return {
- ...contentsModel,
- path: this._toGlobalPath(drive1, contentsModel.path)
- } as Contents.IModel;
- });
- } else {
- throw Error('Copying files between drives is not currently implemented');
- }
- }
- /**
- * Create a checkpoint for a file.
- *
- * @param path - The path of the file.
- *
- * @returns A promise which resolves with the new checkpoint model when the
- * checkpoint is created.
- */
- createCheckpoint(path: string): Promise<Contents.ICheckpointModel> {
- let [drive, localPath] = this._driveForPath(path);
- return drive.createCheckpoint(localPath);
- }
- /**
- * List available checkpoints for a file.
- *
- * @param path - The path of the file.
- *
- * @returns A promise which resolves with a list of checkpoint models for
- * the file.
- */
- listCheckpoints(path: string): Promise<Contents.ICheckpointModel[]> {
- let [drive, localPath] = this._driveForPath(path);
- return drive.listCheckpoints(localPath);
- }
- /**
- * Restore a file to a known checkpoint state.
- *
- * @param path - The path of the file.
- *
- * @param checkpointID - The id of the checkpoint to restore.
- *
- * @returns A promise which resolves when the checkpoint is restored.
- */
- restoreCheckpoint(path: string, checkpointID: string): Promise<void> {
- let [drive, localPath] = this._driveForPath(path);
- return drive.restoreCheckpoint(localPath, checkpointID);
- }
- /**
- * Delete a checkpoint for a file.
- *
- * @param path - The path of the file.
- *
- * @param checkpointID - The id of the checkpoint to delete.
- *
- * @returns A promise which resolves when the checkpoint is deleted.
- */
- deleteCheckpoint(path: string, checkpointID: string): Promise<void> {
- let [drive, localPath] = this._driveForPath(path);
- return drive.deleteCheckpoint(localPath, checkpointID);
- }
- /**
- * Given a drive and a local path, construct a fully qualified
- * path. The inverse of `_driveForPath`.
- *
- * @param drive: an `IDrive`.
- *
- * @param localPath: the local path on the drive.
- *
- * @returns the fully qualified path.
- */
- private _toGlobalPath(drive: Contents.IDrive, localPath: string): string {
- if (drive === this._defaultDrive) {
- return PathExt.removeSlash(localPath);
- } else {
- return `${drive.name}:${PathExt.removeSlash(localPath)}`;
- }
- }
- /**
- * Given a path, get the `IDrive to which it refers,
- * where the path satisfies the pattern
- * `'driveName:path/to/file'`. If there is no `driveName`
- * prepended to the path, it returns the default drive.
- *
- * @param path: a path to a file.
- *
- * @returns A tuple containing an `IDrive` object for the path,
- * and a local path for that drive.
- */
- private _driveForPath(path: string): [Contents.IDrive, string] {
- const driveName = this.driveName(path);
- const localPath = this.localPath(path);
- if (driveName) {
- return [this._additionalDrives.get(driveName), localPath];
- } else {
- return [this._defaultDrive, localPath];
- }
- }
- /**
- * Respond to fileChanged signals from the drives attached to
- * the manager. This prepends the drive name to the path if necessary,
- * and then forwards the signal.
- */
- private _onFileChanged(sender: Contents.IDrive, args: Contents.IChangedArgs) {
- if (sender === this._defaultDrive) {
- this._fileChanged.emit(args);
- } else {
- let newValue: Partial<Contents.IModel> | null = null;
- let oldValue: Partial<Contents.IModel> | null = null;
- if (args.newValue && args.newValue.path) {
- newValue = {
- ...args.newValue,
- path: this._toGlobalPath(sender, args.newValue.path)
- };
- }
- if (args.oldValue && args.oldValue.path) {
- oldValue = {
- ...args.oldValue,
- path: this._toGlobalPath(sender, args.oldValue.path)
- };
- }
- this._fileChanged.emit({
- type: args.type,
- newValue,
- oldValue
- });
- }
- }
- private _isDisposed = false;
- private _additionalDrives = new Map<string, Contents.IDrive>();
- private _defaultDrive: Contents.IDrive;
- private _fileChanged = new Signal<this, Contents.IChangedArgs>(this);
- }
- /**
- * A default implementation for an `IDrive`, talking to the
- * server using the Jupyter REST API.
- */
- export class Drive implements Contents.IDrive {
- /**
- * Construct a new contents manager object.
- *
- * @param options - The options used to initialize the object.
- */
- constructor(options: Drive.IOptions = {}) {
- this.name = options.name || 'Default';
- this._apiEndpoint = options.apiEndpoint || SERVICE_DRIVE_URL;
- this.serverSettings =
- options.serverSettings || ServerConnection.makeSettings();
- }
- /**
- * The name of the drive, which is used at the leading
- * component of file paths.
- */
- readonly name: string;
- /**
- * A signal emitted when a file operation takes place.
- */
- get fileChanged(): ISignal<this, Contents.IChangedArgs> {
- return this._fileChanged;
- }
- /**
- * The server settings of the drive.
- */
- readonly serverSettings: ServerConnection.ISettings;
- /**
- * Test whether the manager has been disposed.
- */
- get isDisposed(): boolean {
- return this._isDisposed;
- }
- /**
- * Dispose of the resources held by the manager.
- */
- dispose(): void {
- if (this.isDisposed) {
- return;
- }
- this._isDisposed = true;
- Signal.clearData(this);
- }
- /**
- * Get a file or directory.
- *
- * @param localPath: The path to the file.
- *
- * @param options: The options used to fetch the file.
- *
- * @returns A promise which resolves with the file content.
- *
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
- */
- get(
- localPath: string,
- options?: Contents.IFetchOptions
- ): Promise<Contents.IModel> {
- let url = this._getUrl(localPath);
- if (options) {
- // The notebook type cannot take an format option.
- if (options.type === 'notebook') {
- delete options['format'];
- }
- let content = options.content ? '1' : '0';
- let params: JSONObject = { ...options, content };
- url += URLExt.objectToQueryString(params);
- }
- let settings = this.serverSettings;
- return ServerConnection.makeRequest(url, {}, settings)
- .then(response => {
- if (response.status !== 200) {
- throw new ServerConnection.ResponseError(response);
- }
- return response.json();
- })
- .then(data => {
- validate.validateContentsModel(data);
- return data;
- });
- }
- /**
- * Get an encoded download url given a file path.
- *
- * @param localPath - An absolute POSIX file path on the server.
- *
- * #### Notes
- * It is expected that the path contains no relative paths.
- */
- getDownloadUrl(localPath: string): Promise<string> {
- let baseUrl = this.serverSettings.baseUrl;
- return Promise.resolve(
- URLExt.join(baseUrl, FILES_URL, URLExt.encodeParts(localPath))
- );
- }
- /**
- * Create a new untitled file or directory in the specified directory path.
- *
- * @param options: The options used to create the file.
- *
- * @returns A promise which resolves with the created file content when the
- * file is created.
- *
- * #### Notes
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
- */
- newUntitled(options: Contents.ICreateOptions = {}): Promise<Contents.IModel> {
- let body = '{}';
- if (options) {
- if (options.ext) {
- options.ext = Private.normalizeExtension(options.ext);
- }
- body = JSON.stringify(options);
- }
- let settings = this.serverSettings;
- let url = this._getUrl(options.path || '');
- let init = {
- method: 'POST',
- body
- };
- return ServerConnection.makeRequest(url, init, settings)
- .then(response => {
- if (response.status !== 201) {
- throw new ServerConnection.ResponseError(response);
- }
- return response.json();
- })
- .then(data => {
- validate.validateContentsModel(data);
- this._fileChanged.emit({
- type: 'new',
- oldValue: null,
- newValue: data
- });
- return data;
- });
- }
- /**
- * Delete a file.
- *
- * @param localPath - The path to the file.
- *
- * @returns A promise which resolves when the file is deleted.
- *
- * #### Notes
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
- */
- delete(localPath: string): Promise<void> {
- let url = this._getUrl(localPath);
- let settings = this.serverSettings;
- let init = { method: 'DELETE' };
- return ServerConnection.makeRequest(url, init, settings).then(response => {
- // Translate certain errors to more specific ones.
- // TODO: update IPEP27 to specify errors more precisely, so
- // that error types can be detected here with certainty.
- if (response.status === 400) {
- return response.json().then(data => {
- throw new ServerConnection.ResponseError(response, data['message']);
- });
- }
- if (response.status !== 204) {
- throw new ServerConnection.ResponseError(response);
- }
- this._fileChanged.emit({
- type: 'delete',
- oldValue: { path: localPath },
- newValue: null
- });
- });
- }
- /**
- * Rename a file or directory.
- *
- * @param oldLocalPath - The original file path.
- *
- * @param newLocalPath - The new file path.
- *
- * @returns A promise which resolves with the new file contents model when
- * the file is renamed.
- *
- * #### Notes
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
- */
- rename(oldLocalPath: string, newLocalPath: string): Promise<Contents.IModel> {
- let settings = this.serverSettings;
- let url = this._getUrl(oldLocalPath);
- let init = {
- method: 'PATCH',
- body: JSON.stringify({ path: newLocalPath })
- };
- return ServerConnection.makeRequest(url, init, settings)
- .then(response => {
- if (response.status !== 200) {
- throw new ServerConnection.ResponseError(response);
- }
- return response.json();
- })
- .then(data => {
- validate.validateContentsModel(data);
- this._fileChanged.emit({
- type: 'rename',
- oldValue: { path: oldLocalPath },
- newValue: data
- });
- return data;
- });
- }
- /**
- * Save a file.
- *
- * @param localPath - The desired file path.
- *
- * @param options - Optional overrides to the model.
- *
- * @returns A promise which resolves with the file content model when the
- * file is saved.
- *
- * #### Notes
- * Ensure that `model.content` is populated for the file.
- *
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
- */
- save(
- localPath: string,
- options: Partial<Contents.IModel> = {}
- ): Promise<Contents.IModel> {
- let settings = this.serverSettings;
- let url = this._getUrl(localPath);
- let init = {
- method: 'PUT',
- body: JSON.stringify(options)
- };
- return ServerConnection.makeRequest(url, init, settings)
- .then(response => {
- // will return 200 for an existing file and 201 for a new file
- if (response.status !== 200 && response.status !== 201) {
- throw new ServerConnection.ResponseError(response);
- }
- return response.json();
- })
- .then(data => {
- validate.validateContentsModel(data);
- this._fileChanged.emit({
- type: 'save',
- oldValue: null,
- newValue: data
- });
- return data;
- });
- }
- /**
- * Copy a file into a given directory.
- *
- * @param localPath - The original file path.
- *
- * @param toDir - The destination directory path.
- *
- * @returns A promise which resolves with the new contents model when the
- * file is copied.
- *
- * #### Notes
- * The server will select the name of the copied file.
- *
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
- */
- copy(fromFile: string, toDir: string): Promise<Contents.IModel> {
- let settings = this.serverSettings;
- let url = this._getUrl(toDir);
- let init = {
- method: 'POST',
- body: JSON.stringify({ copy_from: fromFile })
- };
- return ServerConnection.makeRequest(url, init, settings)
- .then(response => {
- if (response.status !== 201) {
- throw new ServerConnection.ResponseError(response);
- }
- return response.json();
- })
- .then(data => {
- validate.validateContentsModel(data);
- this._fileChanged.emit({
- type: 'new',
- oldValue: null,
- newValue: data
- });
- return data;
- });
- }
- /**
- * Create a checkpoint for a file.
- *
- * @param localPath - The path of the file.
- *
- * @returns A promise which resolves with the new checkpoint model when the
- * checkpoint is created.
- *
- * #### Notes
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
- */
- createCheckpoint(localPath: string): Promise<Contents.ICheckpointModel> {
- let url = this._getUrl(localPath, 'checkpoints');
- let init = { method: 'POST' };
- return ServerConnection.makeRequest(url, init, this.serverSettings)
- .then(response => {
- if (response.status !== 201) {
- throw new ServerConnection.ResponseError(response);
- }
- return response.json();
- })
- .then(data => {
- validate.validateCheckpointModel(data);
- return data;
- });
- }
- /**
- * List available checkpoints for a file.
- *
- * @param localPath - The path of the file.
- *
- * @returns A promise which resolves with a list of checkpoint models for
- * the file.
- *
- * #### Notes
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
- */
- listCheckpoints(localPath: string): Promise<Contents.ICheckpointModel[]> {
- let url = this._getUrl(localPath, 'checkpoints');
- return ServerConnection.makeRequest(url, {}, this.serverSettings)
- .then(response => {
- if (response.status !== 200) {
- throw new ServerConnection.ResponseError(response);
- }
- return response.json();
- })
- .then(data => {
- if (!Array.isArray(data)) {
- throw new Error('Invalid Checkpoint list');
- }
- for (let i = 0; i < data.length; i++) {
- validate.validateCheckpointModel(data[i]);
- }
- return data;
- });
- }
- /**
- * Restore a file to a known checkpoint state.
- *
- * @param localPath - The path of the file.
- *
- * @param checkpointID - The id of the checkpoint to restore.
- *
- * @returns A promise which resolves when the checkpoint is restored.
- *
- * #### Notes
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
- */
- restoreCheckpoint(localPath: string, checkpointID: string): Promise<void> {
- let url = this._getUrl(localPath, 'checkpoints', checkpointID);
- let init = { method: 'POST' };
- return ServerConnection.makeRequest(url, init, this.serverSettings).then(
- response => {
- if (response.status !== 204) {
- throw new ServerConnection.ResponseError(response);
- }
- }
- );
- }
- /**
- * Delete a checkpoint for a file.
- *
- * @param localPath - The path of the file.
- *
- * @param checkpointID - The id of the checkpoint to delete.
- *
- * @returns A promise which resolves when the checkpoint is deleted.
- *
- * #### Notes
- * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
- */
- deleteCheckpoint(localPath: string, checkpointID: string): Promise<void> {
- let url = this._getUrl(localPath, 'checkpoints', checkpointID);
- let init = { method: 'DELETE' };
- return ServerConnection.makeRequest(url, init, this.serverSettings).then(
- response => {
- if (response.status !== 204) {
- throw new ServerConnection.ResponseError(response);
- }
- }
- );
- }
- /**
- * Get a REST url for a file given a path.
- */
- private _getUrl(...args: string[]): string {
- let parts = args.map(path => URLExt.encodeParts(path));
- let baseUrl = this.serverSettings.baseUrl;
- return URLExt.join(baseUrl, this._apiEndpoint, ...parts);
- }
- private _apiEndpoint: string;
- private _isDisposed = false;
- private _fileChanged = new Signal<this, Contents.IChangedArgs>(this);
- }
- /**
- * A namespace for ContentsManager statics.
- */
- export namespace ContentsManager {
- /**
- * The options used to initialize a contents manager.
- */
- export interface IOptions {
- /**
- * The default drive backend for the contents manager.
- */
- defaultDrive?: Contents.IDrive;
- /**
- * The server settings associated with the manager.
- */
- serverSettings?: ServerConnection.ISettings;
- }
- }
- /**
- * A namespace for Drive statics.
- */
- export namespace Drive {
- /**
- * The options used to initialize a `Drive`.
- */
- export interface IOptions {
- /**
- * The name for the `Drive`, which is used in file
- * paths to disambiguate it from other drives.
- */
- name?: string;
- /**
- * The server settings for the server.
- */
- serverSettings?: ServerConnection.ISettings;
- /**
- * A REST endpoint for drive requests.
- * If not given, defaults to the Jupyter
- * REST API given by [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
- */
- apiEndpoint?: string;
- }
- }
- /**
- * A namespace for module private data.
- */
- namespace Private {
- /**
- * Normalize a file extension to be of the type `'.foo'`.
- *
- * Adds a leading dot if not present and converts to lower case.
- */
- export function normalizeExtension(extension: string): string {
- if (extension.length > 0 && extension.indexOf('.') !== 0) {
- extension = `.${extension}`;
- }
- return extension;
- }
- }
|