UNPKG

@jupyterlab/services

Version:

Client APIs for the Jupyter services REST APIs

875 lines 31.1 kB
"use strict"; // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Drive = exports.ContentsManager = exports.Contents = void 0; const coreutils_1 = require("@jupyterlab/coreutils"); const signaling_1 = require("@lumino/signaling"); const __1 = require(".."); const validate = __importStar(require("./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. */ var Contents; (function (Contents) { /** * Validates an IModel, throwing an error if it does not pass. */ function validateContentsModel(contents) { validate.validateContentsModel(contents); } Contents.validateContentsModel = validateContentsModel; /** * Validates an ICheckpointModel, throwing an error if it does not pass. */ function validateCheckpointModel(checkpoint) { validate.validateCheckpointModel(checkpoint); } Contents.validateCheckpointModel = validateCheckpointModel; })(Contents || (exports.Contents = Contents = {})); /** * 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. */ class ContentsManager { /** * Construct a new contents manager object. * * @param options - The options used to initialize the object. */ constructor(options = {}) { var _a, _b; this._isDisposed = false; this._additionalDrives = new Map(); this._fileChanged = new signaling_1.Signal(this); const serverSettings = (this.serverSettings = (_a = options.serverSettings) !== null && _a !== void 0 ? _a : __1.ServerConnection.makeSettings()); this._defaultDrive = (_b = options.defaultDrive) !== null && _b !== void 0 ? _b : new Drive({ serverSettings }); this._defaultDrive.fileChanged.connect(this._onFileChanged, this); } /** * A signal emitted when a file operation takes place. */ get fileChanged() { return this._fileChanged; } /** * Test whether the manager has been disposed. */ get isDisposed() { return this._isDisposed; } /** * Dispose of the resources held by the manager. */ dispose() { if (this.isDisposed) { return; } this._isDisposed = true; signaling_1.Signal.clearData(this); } /** * Add an `IDrive` to the manager. */ addDrive(drive) { this._additionalDrives.set(drive.name, drive); drive.fileChanged.connect(this._onFileChanged, this); } /** * Given a path, get a shared model factory from the * relevant backend. Returns `null` if the backend * does not provide one. */ getSharedModelFactory(path) { var _a; const [drive] = this._driveForPath(path); return (_a = drive === null || drive === void 0 ? void 0 : drive.sharedModelFactory) !== null && _a !== void 0 ? _a : 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) { const parts = path.split('/'); const firstParts = parts[0].split(':'); if (firstParts.length === 1 || !this._additionalDrives.has(firstParts[0])) { return coreutils_1.PathExt.removeSlash(path); } return coreutils_1.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) { const parts = path.split(':'); if (parts.length === 1) { return coreutils_1.PathExt.normalize(path); } return `${parts[0]}:${coreutils_1.PathExt.normalize(parts.slice(1).join(':'))}`; } /** * Resolve a global path, starting from the root path. Behaves like * posix-path.resolve, with 3 differences: * - will never prepend cwd * - if root has a drive name, the result is prefixed with "<drive>:" * - before adding drive name, leading slashes are removed * * @param path the path. * * @returns The normalized path. */ resolvePath(root, path) { const driveName = this.driveName(root); const localPath = this.localPath(root); const resolved = coreutils_1.PathExt.resolve('/', localPath, path); return driveName ? `${driveName}:${resolved}` : resolved; } /** * 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) { 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, options) { const [drive, localPath] = this._driveForPath(path); return drive.get(localPath, options).then(contentsModel => { const listing = []; if (contentsModel.type === 'directory' && contentsModel.content) { for (const item of contentsModel.content) { listing.push({ ...item, path: this._toGlobalPath(drive, item.path) }); } return { ...contentsModel, path: this._toGlobalPath(drive, localPath), content: listing, serverPath: contentsModel.path }; } else { return { ...contentsModel, path: this._toGlobalPath(drive, localPath), serverPath: contentsModel.path }; } }); } /** * 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. * * The returned URL may include a query parameter. */ getDownloadUrl(path) { const [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 = {}) { if (options.path) { const globalPath = this.normalize(options.path); const [drive, localPath] = this._driveForPath(globalPath); return drive .newUntitled({ ...options, path: localPath }) .then(contentsModel => { return { ...contentsModel, path: coreutils_1.PathExt.join(globalPath, contentsModel.name), serverPath: contentsModel.path }; }); } 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) { const [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, newPath) { const [drive1, path1] = this._driveForPath(path); const [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), serverPath: contentsModel.path }; }); } /** * 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, options = {}) { const globalPath = this.normalize(path); const [drive, localPath] = this._driveForPath(path); return drive .save(localPath, { ...options, path: localPath }) .then(contentsModel => { return { ...contentsModel, path: globalPath, serverPath: contentsModel.path }; }); } /** * 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, toDir) { const [drive1, path1] = this._driveForPath(fromFile); const [drive2, path2] = this._driveForPath(toDir); if (drive1 === drive2) { return drive1.copy(path1, path2).then(contentsModel => { return { ...contentsModel, path: this._toGlobalPath(drive1, contentsModel.path), serverPath: contentsModel.path }; }); } 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) { const [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) { const [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, checkpointID) { const [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, checkpointID) { const [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. */ _toGlobalPath(drive, localPath) { if (drive === this._defaultDrive) { return coreutils_1.PathExt.removeSlash(localPath); } else { return `${drive.name}:${coreutils_1.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. */ _driveForPath(path) { 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. */ _onFileChanged(sender, args) { var _a, _b; if (sender === this._defaultDrive) { this._fileChanged.emit(args); } else { let newValue = null; let oldValue = null; if ((_a = args.newValue) === null || _a === void 0 ? void 0 : _a.path) { newValue = { ...args.newValue, path: this._toGlobalPath(sender, args.newValue.path) }; } if ((_b = args.oldValue) === null || _b === void 0 ? void 0 : _b.path) { oldValue = { ...args.oldValue, path: this._toGlobalPath(sender, args.oldValue.path) }; } this._fileChanged.emit({ type: args.type, newValue, oldValue }); } } } exports.ContentsManager = ContentsManager; /** * A default implementation for an `IDrive`, talking to the * server using the Jupyter REST API. */ class Drive { /** * Construct a new contents manager object. * * @param options - The options used to initialize the object. */ constructor(options = {}) { var _a, _b, _c; this._isDisposed = false; this._fileChanged = new signaling_1.Signal(this); this.name = (_a = options.name) !== null && _a !== void 0 ? _a : 'Default'; this._apiEndpoint = (_b = options.apiEndpoint) !== null && _b !== void 0 ? _b : SERVICE_DRIVE_URL; this.serverSettings = (_c = options.serverSettings) !== null && _c !== void 0 ? _c : __1.ServerConnection.makeSettings(); } /** * A signal emitted when a file operation takes place. */ get fileChanged() { return this._fileChanged; } /** * Test whether the manager has been disposed. */ get isDisposed() { return this._isDisposed; } /** * Dispose of the resources held by the manager. */ dispose() { if (this.isDisposed) { return; } this._isDisposed = true; signaling_1.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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. */ async get(localPath, options) { let url = this._getUrl(localPath); if (options) { // The notebook type cannot take an format option. if (options.type === 'notebook') { delete options['format']; } const content = options.content ? '1' : '0'; const hash = options.hash ? '1' : '0'; const params = { ...options, content, hash }; url += coreutils_1.URLExt.objectToQueryString(params); } const settings = this.serverSettings; const response = await __1.ServerConnection.makeRequest(url, {}, settings); if (response.status !== 200) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } const data = await response.json(); 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. * * The returned URL may include a query parameter. */ getDownloadUrl(localPath) { const baseUrl = this.serverSettings.baseUrl; let url = coreutils_1.URLExt.join(baseUrl, FILES_URL, coreutils_1.URLExt.encodeParts(localPath)); let cookie = ''; try { cookie = document.cookie; } catch (e) { // e.g. SecurityError in case of CSP Sandbox } const xsrfTokenMatch = cookie.match('\\b_xsrf=([^;]*)\\b'); if (xsrfTokenMatch) { const fullUrl = new URL(url); fullUrl.searchParams.append('_xsrf', xsrfTokenMatch[1]); url = fullUrl.toString(); } return Promise.resolve(url); } /** * 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. */ async newUntitled(options = {}) { var _a; let body = '{}'; if (options) { if (options.ext) { options.ext = Private.normalizeExtension(options.ext); } body = JSON.stringify(options); } const settings = this.serverSettings; const url = this._getUrl((_a = options.path) !== null && _a !== void 0 ? _a : ''); const init = { method: 'POST', body }; const response = await __1.ServerConnection.makeRequest(url, init, settings); if (response.status !== 201) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } const data = await response.json(); 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents). */ async delete(localPath) { const url = this._getUrl(localPath); const settings = this.serverSettings; const init = { method: 'DELETE' }; const response = await __1.ServerConnection.makeRequest(url, init, settings); // TODO: update IPEP27 to specify errors more precisely, so // that error types can be detected here with certainty. if (response.status !== 204) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. */ async rename(oldLocalPath, newLocalPath) { const settings = this.serverSettings; const url = this._getUrl(oldLocalPath); const init = { method: 'PATCH', body: JSON.stringify({ path: newLocalPath }) }; const response = await __1.ServerConnection.makeRequest(url, init, settings); if (response.status !== 200) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } const data = await response.json(); 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. */ async save(localPath, options = {}) { const settings = this.serverSettings; const url = this._getUrl(localPath); const init = { method: 'PUT', body: JSON.stringify(options) }; const response = await __1.ServerConnection.makeRequest(url, init, settings); // will return 200 for an existing file and 201 for a new file if (response.status !== 200 && response.status !== 201) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } const data = await response.json(); 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. */ async copy(fromFile, toDir) { const settings = this.serverSettings; const url = this._getUrl(toDir); const init = { method: 'POST', body: JSON.stringify({ copy_from: fromFile }) }; const response = await __1.ServerConnection.makeRequest(url, init, settings); if (response.status !== 201) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } const data = await response.json(); 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. */ async createCheckpoint(localPath) { const url = this._getUrl(localPath, 'checkpoints'); const init = { method: 'POST' }; const response = await __1.ServerConnection.makeRequest(url, init, this.serverSettings); if (response.status !== 201) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } const data = await response.json(); 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents) and validates the response model. */ async listCheckpoints(localPath) { const url = this._getUrl(localPath, 'checkpoints'); const response = await __1.ServerConnection.makeRequest(url, {}, this.serverSettings); if (response.status !== 200) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } const data = await response.json(); 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents). */ async restoreCheckpoint(localPath, checkpointID) { const url = this._getUrl(localPath, 'checkpoints', checkpointID); const init = { method: 'POST' }; const response = await __1.ServerConnection.makeRequest(url, init, this.serverSettings); if (response.status !== 204) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } } /** * 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 Server API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter-server/jupyter_server/main/jupyter_server/services/api/api.yaml#!/contents). */ async deleteCheckpoint(localPath, checkpointID) { const url = this._getUrl(localPath, 'checkpoints', checkpointID); const init = { method: 'DELETE' }; const response = await __1.ServerConnection.makeRequest(url, init, this.serverSettings); if (response.status !== 204) { const err = await __1.ServerConnection.ResponseError.create(response); throw err; } } /** * Get a REST url for a file given a path. */ _getUrl(...args) { const parts = args.map(path => coreutils_1.URLExt.encodeParts(path)); const baseUrl = this.serverSettings.baseUrl; return coreutils_1.URLExt.join(baseUrl, this._apiEndpoint, ...parts); } } exports.Drive = Drive; /** * A namespace for module private data. */ var Private; (function (Private) { /** * Normalize a file extension to be of the type `'.foo'`. * * Adds a leading dot if not present and converts to lower case. */ function normalizeExtension(extension) { if (extension.length > 0 && extension.indexOf('.') !== 0) { extension = `.${extension}`; } return extension; } Private.normalizeExtension = normalizeExtension; })(Private || (Private = {})); //# sourceMappingURL=index.js.map