UNPKG

jupyterlab-emrys

Version:

A computational environment for Jupyter. Powered by Emrys

482 lines (481 loc) 15.9 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. "use strict"; var phosphor_signaling_1 = require('phosphor-signaling'); var json_1 = require('../notebook/common/json'); /** * An implementation of a file browser model. * * #### Notes * All paths parameters without a leading `'/'` are interpreted as relative to * the current directory. Supports `'../'` syntax. */ var FileBrowserModel = (function () { /** * Construct a new file browser model. */ function FileBrowserModel(options) { this._maxUploadSizeMb = 15; this._manager = null; this._sessions = []; this._pendingPath = null; this._pending = null; this._manager = options.manager; this._model = { path: '', name: '/', type: 'directory', content: [] }; this.cd(); this._manager.sessions.runningChanged.connect(this._onRunningChanged, this); } Object.defineProperty(FileBrowserModel.prototype, "pathChanged", { /** * A signal emitted when the path changes. */ get: function () { return Private.pathChangedSignal.bind(this); }, enumerable: true, configurable: true }); Object.defineProperty(FileBrowserModel.prototype, "refreshed", { /** * Get the refreshed signal. */ get: function () { return Private.refreshedSignal.bind(this); }, enumerable: true, configurable: true }); Object.defineProperty(FileBrowserModel.prototype, "fileChanged", { /** * Get the file path changed signal. */ get: function () { return Private.fileChangedSignal.bind(this); }, enumerable: true, configurable: true }); Object.defineProperty(FileBrowserModel.prototype, "path", { /** * Get the current path. * * #### Notes * This is a read-only property. */ get: function () { return this._model.path; }, enumerable: true, configurable: true }); Object.defineProperty(FileBrowserModel.prototype, "items", { /** * Get a read-only list of the items in the current path. */ get: function () { return this._model.content ? this._model.content.slice() : []; }, enumerable: true, configurable: true }); Object.defineProperty(FileBrowserModel.prototype, "isDisposed", { /** * Get whether the view model is disposed. */ get: function () { return this._model === null; }, enumerable: true, configurable: true }); Object.defineProperty(FileBrowserModel.prototype, "sessions", { /** * Get the session models for active notebooks. * * #### Notes * This is a read-only property. */ get: function () { return this._sessions.slice(); }, enumerable: true, configurable: true }); Object.defineProperty(FileBrowserModel.prototype, "kernelspecs", { /** * Get the kernel specs. */ get: function () { return this._manager.kernelspecs; }, enumerable: true, configurable: true }); /** * Dispose of the resources held by the view model. */ FileBrowserModel.prototype.dispose = function () { this._model = null; this._manager = null; phosphor_signaling_1.clearSignalData(this); }; /** * Change directory. * * @param path - The path to the file or directory. * * @returns A promise with the contents of the directory. */ FileBrowserModel.prototype.cd = function (newValue) { var _this = this; if (newValue === void 0) { newValue = '.'; } if (newValue !== '.') { newValue = Private.normalizePath(this._model.path, newValue); } // Collapse requests to the same directory. if (newValue === this._pendingPath) { return Promise.resolve(void 0); } var oldValue = this.path; var options = { content: true }; this._pendingPath = newValue; if (newValue === '.') { newValue = this.path; } if (oldValue !== newValue) { this._sessions = []; } this._pending = this._manager.contents.get(newValue, options).then(function (contents) { _this._model = contents; return _this._manager.sessions.listRunning(); }).then(function (models) { _this._onRunningChanged(_this._manager.sessions, models); if (oldValue !== newValue) { _this.pathChanged.emit({ name: 'path', oldValue: oldValue, newValue: newValue }); } _this.refreshed.emit(void 0); _this._pendingPath = null; }); return this._pending; }; /** * Refresh the current directory. */ FileBrowserModel.prototype.refresh = function () { return this.cd('.').catch(function (error) { console.error(error); var msg = 'Unable to refresh the directory listing due to '; msg += 'lost server connection.'; error.message = msg; throw error; }); }; /** * Copy a file. * * @param fromFile - The path of the original file. * * @param toDir - The path to the target directory. * * @returns A promise which resolves to the contents of the file. */ FileBrowserModel.prototype.copy = function (fromFile, toDir) { var _this = this; var normalizePath = Private.normalizePath; fromFile = normalizePath(this._model.path, fromFile); toDir = normalizePath(this._model.path, toDir); return this._manager.contents.copy(fromFile, toDir).then(function (contents) { _this.fileChanged.emit({ name: 'file', oldValue: void 0, newValue: contents.path }); return contents; }); }; /** * Delete a file. * * @param: path - The path to the file to be deleted. * * @returns A promise which resolves when the file is deleted. */ FileBrowserModel.prototype.deleteFile = function (path) { var _this = this; var normalizePath = Private.normalizePath; path = normalizePath(this._model.path, path); return this._manager.contents.delete(path).then(function () { _this.fileChanged.emit({ name: 'file', oldValue: path, newValue: void 0 }); }); }; /** * Download a file. * * @param - path - The path of the file to be downloaded. * * @returns - A promise which resolves to the file contents. */ FileBrowserModel.prototype.download = function (path) { var normalizePath = Private.normalizePath; path = normalizePath(this._model.path, path); return this._manager.contents.get(path, { content: true }).then(function (contents) { var element = document.createElement('a'); element.setAttribute('href', 'data:text/text;charset=utf-8,' + encodeURI(contents.content)); element.setAttribute('download', contents.name); element.click(); return contents; }); }; /** * Create a new untitled file or directory in the current directory. * * @param type - The type of file object to create. One of * `['file', 'notebook', 'directory']`. * * @param ext - Optional extension for `'file'` types (defaults to `'.txt'`). * * @returns A promise containing the new file contents model. */ FileBrowserModel.prototype.newUntitled = function (options) { var _this = this; if (options.type === 'file') { options.ext = options.ext || '.txt'; } options.path = options.path || this._model.path; return this._manager.contents.newUntitled(options).then(function (contents) { _this.fileChanged.emit({ name: 'file', oldValue: void 0, newValue: contents.path }); return contents; }); }; /** * Rename a file or directory. * * @param path - The path to the original file. * * @param newPath - The path to the new file. * * @returns A promise containing the new file contents model. */ FileBrowserModel.prototype.rename = function (path, newPath) { var _this = this; // Handle relative paths. var normalizePath = Private.normalizePath; path = normalizePath(this._model.path, path); newPath = normalizePath(this._model.path, newPath); return this._manager.contents.rename(path, newPath).then(function (contents) { _this.fileChanged.emit({ name: 'file', oldValue: path, newValue: newPath }); return contents; }); }; /** * Upload a `File` object. * * @param file - The `File` object to upload. * * @param overwrite - Whether to overwrite an existing file. * * @returns A promise containing the new file contents model. * * #### Notes * This will fail to upload files that are too big to be sent in one * request to the server. */ FileBrowserModel.prototype.upload = function (file, overwrite) { var _this = this; // Skip large files with a warning. if (file.size > this._maxUploadSizeMb * 1024 * 1024) { var msg = "Cannot upload file (>" + this._maxUploadSizeMb + " MB) "; msg += "\"" + file.name + "\""; console.warn(msg); return Promise.reject(new Error(msg)); } if (overwrite) { return this._upload(file); } var path = this._model.path; path = path ? path + '/' + file.name : file.name; return this._manager.contents.get(path, {}).then(function () { return Private.typedThrow("\"" + file.name + "\" already exists"); }, function () { return _this._upload(file); }); }; /** * Shut down a session by session id. */ FileBrowserModel.prototype.shutdown = function (id) { return this._manager.sessions.shutdown(id); }; /** * Perform the actual upload. */ FileBrowserModel.prototype._upload = function (file) { var _this = this; // Gather the file model parameters. var path = this._model.path; path = path ? path + '/' + file.name : file.name; var name = file.name; var isNotebook = file.name.indexOf('.ipynb') !== -1; var type = isNotebook ? 'notebook' : 'file'; var format = isNotebook ? 'json' : 'base64'; // Get the file content. var reader = new FileReader(); if (isNotebook) { reader.readAsText(file); } else { reader.readAsArrayBuffer(file); } return new Promise(function (resolve, reject) { reader.onload = function (event) { var model = { type: type, format: format, name: name, content: Private.getContent(reader) }; _this._manager.contents.save(path, model).then(function (contents) { _this.fileChanged.emit({ name: 'file', oldValue: void 0, newValue: contents.path }); resolve(contents); }); }; reader.onerror = function (event) { reject(Error(("Failed to upload \"" + file.name + "\":") + event)); }; }); }; /** * Handle a change to the running sessions. */ FileBrowserModel.prototype._onRunningChanged = function (sender, models) { if (json_1.deepEqual(models, this._sessions)) { return; } this._sessions = []; if (!models.length) { this.refreshed.emit(void 0); return; } var paths = this._model.content.map(function (contents) { return contents.path; }); for (var _i = 0, models_1 = models; _i < models_1.length; _i++) { var model = models_1[_i]; var index = paths.indexOf(model.notebook.path); if (index !== -1) { this._sessions.push(model); } } this.refreshed.emit(void 0); }; return FileBrowserModel; }()); exports.FileBrowserModel = FileBrowserModel; /** * The namespace for the file browser model private data. */ var Private; (function (Private) { /** * A signal emitted when a model refresh occurs. */ Private.refreshedSignal = new phosphor_signaling_1.Signal(); /** * A signal emitted when the a file changes path. */ Private.fileChangedSignal = new phosphor_signaling_1.Signal(); /** * A signal emitted when the path changes. */ Private.pathChangedSignal = new phosphor_signaling_1.Signal(); /** * Parse the content of a `FileReader`. * * If the result is an `ArrayBuffer`, return a Base64-encoded string. * Otherwise, return the JSON parsed result. */ function getContent(reader) { if (reader.result instanceof ArrayBuffer) { // Base64-encode binary file data. var bytes = ''; var buf = new Uint8Array(reader.result); var nbytes = buf.byteLength; for (var i = 0; i < nbytes; i++) { bytes += String.fromCharCode(buf[i]); } return btoa(bytes); } else { return JSON.parse(reader.result); } } Private.getContent = getContent; /** * Normalize a path based on a root directory, accounting for relative paths. */ function normalizePath(root, path) { // Current directory if (path === '.') { return root; } // Root path. if (path.indexOf('/') === 0) { path = path.slice(1, path.length); root = ''; } else if (path.indexOf('./') === 0) { path = path.slice(2, path.length); } else if (path.indexOf('../../') === 0) { var parts = root.split('/'); root = parts.splice(0, parts.length - 2).join('/'); path = path.slice(6, path.length); } else if (path.indexOf('../') === 0) { var parts = root.split('/'); root = parts.splice(0, parts.length - 1).join('/'); path = path.slice(3, path.length); } else { } if (path[path.length - 1] === '/') { path = path.slice(0, path.length - 1); } // Combine the root and the path if necessary. if (root && path) { path = root + '/' + path; } else if (root) { path = root; } return path; } Private.normalizePath = normalizePath; /** * Work around TS 1.8 type inferencing in promises which only throw. */ function typedThrow(msg) { throw new Error(msg); } Private.typedThrow = typedThrow; })(Private || (Private = {}));