jupyterlab-emrys
Version:
A computational environment for Jupyter. Powered by Emrys
482 lines (481 loc) • 15.9 kB
JavaScript
// 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 = {}));