123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import {
- Contents, ContentsManager, Kernel, IServiceManager, Session
- } from '@jupyterlab/services';
- import {
- ISequence
- } from 'phosphor/lib/algorithm/sequence';
- import {
- Vector
- } from 'phosphor/lib/collections/vector';
- import {
- IDisposable
- } from 'phosphor/lib/core/disposable';
- import {
- clearSignalData, defineSignal, ISignal
- } from 'phosphor/lib/core/signaling';
- import {
- IChangedArgs
- } from '../common/interfaces';
- /**
- * An implementation of a file browser model.
- *
- * #### Notes
- * All paths parameters without a leading `'/'` are interpreted as relative to
- * the current directory. Supports `'../'` syntax.
- */
- export
- class FileBrowserModel implements IDisposable {
- /**
- * Construct a new file browser model.
- */
- constructor(options: FileBrowserModel.IOptions) {
- this._manager = options.manager;
- this._model = { path: '', name: '/', type: 'directory' };
- this.cd();
- this._manager.sessions.runningChanged.connect(this._onRunningChanged, this);
- }
- /**
- * A signal emitted when the path changes.
- */
- pathChanged: ISignal<FileBrowserModel, IChangedArgs<string>>;
- /**
- * Get the refreshed signal.
- */
- refreshed: ISignal<FileBrowserModel, void>;
- /**
- * Get the file path changed signal.
- */
- fileChanged: ISignal<FileBrowserModel, IChangedArgs<Contents.IModel>>;
- /**
- * Get the current path.
- */
- get path(): string {
- return this._model ? this._model.path : '';
- }
- /**
- * Get a read-only list of the items in the current path.
- */
- get items(): ISequence<Contents.IModel> {
- return this._items;
- }
- /**
- * Get whether the model is disposed.
- */
- get isDisposed(): boolean {
- return this._model === null;
- }
- /**
- * Get the session models for active notebooks in the current directory.
- */
- get sessions(): ISequence<Session.IModel> {
- return this._sessions;
- }
- /**
- * Get the kernel specs.
- */
- get kernelspecs(): Kernel.ISpecModels {
- return this._manager.kernelspecs;
- }
- /**
- * Dispose of the resources held by the model.
- */
- dispose(): void {
- if (this.isDisposed) {
- return;
- }
- this._model = null;
- this._sessions.clear();
- this._items.clear();
- this._manager = null;
- clearSignalData(this);
- }
- /**
- * Change directory.
- *
- * @param path - The path to the file or directory.
- *
- * @returns A promise with the contents of the directory.
- */
- cd(newValue = '.'): Promise<void> {
- if (newValue !== '.') {
- newValue = Private.normalizePath(this._model.path, newValue);
- }
- // Collapse requests to the same directory.
- if (newValue === this._pendingPath) {
- return this._pending;
- }
- let oldValue = this.path;
- let options: Contents.IFetchOptions = { content: true };
- this._pendingPath = newValue;
- if (newValue === '.') {
- newValue = this.path;
- }
- if (oldValue !== newValue) {
- this._sessions.clear();
- }
- let manager = this._manager;
- this._pending = manager.contents.get(newValue, options).then(contents => {
- this._handleContents(contents);
- this._pendingPath = null;
- return manager.sessions.listRunning();
- }).then(models => {
- if (this.isDisposed) {
- return;
- }
- this._onRunningChanged(manager.sessions, models);
- if (oldValue !== newValue) {
- this.pathChanged.emit({
- name: 'path',
- oldValue,
- newValue
- });
- }
- this.refreshed.emit(void 0);
- });
- return this._pending;
- }
- /**
- * Refresh the current directory.
- */
- refresh(): Promise<void> {
- return this.cd('.').catch(error => {
- console.error(error);
- let 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.
- */
- copy(fromFile: string, toDir: string): Promise<Contents.IModel> {
- let normalizePath = Private.normalizePath;
- fromFile = normalizePath(this._model.path, fromFile);
- toDir = normalizePath(this._model.path, toDir);
- return this._manager.contents.copy(fromFile, toDir).then(contents => {
- this.fileChanged.emit({
- name: 'file',
- oldValue: void 0,
- newValue: contents
- });
- 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.
- */
- deleteFile(path: string): Promise<void> {
- let normalizePath = Private.normalizePath;
- path = normalizePath(this._model.path, path);
- return this._manager.contents.delete(path).then(() => {
- this.fileChanged.emit({
- name: 'file',
- oldValue: { path: path },
- newValue: void 0
- });
- });
- }
- /**
- * Download a file.
- *
- * @param - path - The path of the file to be downloaded.
- */
- download(path: string): void {
- let url = this._manager.contents.getDownloadUrl(path);
- let element = document.createElement('a');
- element.setAttribute('href', url);
- element.setAttribute('download', '');
- element.click();
- }
- /**
- * 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.
- */
- newUntitled(options: Contents.ICreateOptions): Promise<Contents.IModel> {
- if (options.type === 'file') {
- options.ext = options.ext || '.txt';
- }
- options.path = options.path || this._model.path;
- let promise = this._manager.contents.newUntitled(options);
- return promise.then((contents: Contents.IModel) => {
- this.fileChanged.emit({
- name: 'file',
- oldValue: void 0,
- newValue: contents
- });
- 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.
- */
- rename(path: string, newPath: string): Promise<Contents.IModel> {
- // Handle relative paths.
- path = Private.normalizePath(this._model.path, path);
- newPath = Private.normalizePath(this._model.path, newPath);
- let promise = this._manager.contents.rename(path, newPath);
- return promise.then((contents: Contents.IModel) => {
- this.fileChanged.emit({
- name: 'file',
- oldValue: {type: contents.type , path: path},
- newValue: contents
- });
- 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.
- */
- upload(file: File, overwrite?: boolean): Promise<Contents.IModel> {
- // Skip large files with a warning.
- if (file.size > this._maxUploadSizeMb * 1024 * 1024) {
- let msg = `Cannot upload file (>${this._maxUploadSizeMb} MB) `;
- msg += `"${file.name}"`;
- console.warn(msg);
- return Promise.reject<Contents.IModel>(new Error(msg));
- }
- if (overwrite) {
- return this._upload(file);
- }
- let path = this._model.path;
- path = path ? path + '/' + file.name : file.name;
- return this._manager.contents.get(path, {}).then(() => {
- let msg = `"${file.name}" already exists`;
- throw new Error(msg);
- }, () => {
- return this._upload(file);
- });
- }
- /**
- * Shut down a session by session id.
- */
- shutdown(id: string): Promise<void> {
- return this._manager.sessions.shutdown(id);
- }
- /**
- * Perform the actual upload.
- */
- private _upload(file: File): Promise<Contents.IModel> {
- // Gather the file model parameters.
- let path = this._model.path;
- path = path ? path + '/' + file.name : file.name;
- let name = file.name;
- let isNotebook = file.name.indexOf('.ipynb') !== -1;
- let type: Contents.ContentType = isNotebook ? 'notebook' : 'file';
- let format: Contents.FileFormat = isNotebook ? 'json' : 'base64';
- // Get the file content.
- let reader = new FileReader();
- if (isNotebook) {
- reader.readAsText(file);
- } else {
- reader.readAsArrayBuffer(file);
- }
- return new Promise<Contents.IModel>((resolve, reject) => {
- reader.onload = (event: Event) => {
- let model: Contents.IModel = {
- type: type,
- format,
- name,
- content: Private.getContent(reader)
- };
- let promise = this._manager.contents.save(path, model);
- promise.then((contents: Contents.IModel) => {
- this.fileChanged.emit({
- name: 'file',
- oldValue: void 0,
- newValue: contents
- });
- resolve(contents);
- });
- };
- reader.onerror = (event: Event) => {
- reject(Error(`Failed to upload "${file.name}":` + event));
- };
- });
- }
- /**
- * Handle an updated contents model.
- */
- private _handleContents(contents: Contents.IModel): void {
- // Update our internal data.
- this._model = contents;
- this._items.clear();
- this._paths = contents.content.map((model: Contents.IModel) => {
- return model.path;
- });
- this._items = new Vector<Contents.IModel>(contents.content);
- this._model.content = null;
- }
- /**
- * Handle a change to the running sessions.
- */
- private _onRunningChanged(sender: Session.IManager, models: Session.IModel[]): void {
- this._sessions.clear();
- for (let model of models) {
- let index = this._paths.indexOf(model.notebook.path);
- if (index !== -1) {
- this._sessions.pushBack(model);
- }
- }
- this.refreshed.emit(void 0);
- }
- private _maxUploadSizeMb = 15;
- private _manager: IServiceManager = null;
- private _sessions = new Vector<Session.IModel>();
- private _items = new Vector<Contents.IModel>();
- private _paths: string[] = [];
- private _model: Contents.IModel;
- private _pendingPath: string = null;
- private _pending: Promise<void> = null;
- }
- // Define the signals for the `FileBrowserModel` class.
- defineSignal(FileBrowserModel.prototype, 'pathChanged');
- defineSignal(FileBrowserModel.prototype, 'refreshed');
- defineSignal(FileBrowserModel.prototype, 'fileChanged');
- /**
- * The namespace for the `FileBrowserModel` class statics.
- */
- export
- namespace FileBrowserModel {
- /**
- * An options object for initializing a file browser.
- */
- export
- interface IOptions {
- /**
- * A service manager instance.
- */
- manager: IServiceManager;
- }
- }
- /**
- * The namespace for the file browser model private data.
- */
- namespace Private {
- /**
- * Parse the content of a `FileReader`.
- *
- * If the result is an `ArrayBuffer`, return a Base64-encoded string.
- * Otherwise, return the JSON parsed result.
- */
- export
- function getContent(reader: FileReader): any {
- if (reader.result instanceof ArrayBuffer) {
- // Base64-encode binary file data.
- let bytes = '';
- let buf = new Uint8Array(reader.result);
- let nbytes = buf.byteLength;
- for (let i = 0; i < nbytes; i++) {
- bytes += String.fromCharCode(buf[i]);
- }
- return btoa(bytes);
- } else {
- return JSON.parse(reader.result);
- }
- }
- /**
- * Normalize a path based on a root directory, accounting for relative paths.
- */
- export
- function normalizePath(root: string, path: string): string {
- return ContentsManager.getAbsolutePath(path, root);
- }
- }
|