model.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. Contents, ContentsManager, Kernel, IServiceManager, Session
  5. } from '@jupyterlab/services';
  6. import {
  7. ISequence
  8. } from 'phosphor/lib/algorithm/sequence';
  9. import {
  10. Vector
  11. } from 'phosphor/lib/collections/vector';
  12. import {
  13. IDisposable
  14. } from 'phosphor/lib/core/disposable';
  15. import {
  16. clearSignalData, defineSignal, ISignal
  17. } from 'phosphor/lib/core/signaling';
  18. import {
  19. IChangedArgs
  20. } from '../common/interfaces';
  21. /**
  22. * An implementation of a file browser model.
  23. *
  24. * #### Notes
  25. * All paths parameters without a leading `'/'` are interpreted as relative to
  26. * the current directory. Supports `'../'` syntax.
  27. */
  28. export
  29. class FileBrowserModel implements IDisposable {
  30. /**
  31. * Construct a new file browser model.
  32. */
  33. constructor(options: FileBrowserModel.IOptions) {
  34. this._manager = options.manager;
  35. this._model = { path: '', name: '/', type: 'directory' };
  36. this.cd();
  37. this._manager.sessions.runningChanged.connect(this._onRunningChanged, this);
  38. }
  39. /**
  40. * A signal emitted when the path changes.
  41. */
  42. pathChanged: ISignal<FileBrowserModel, IChangedArgs<string>>;
  43. /**
  44. * Get the refreshed signal.
  45. */
  46. refreshed: ISignal<FileBrowserModel, void>;
  47. /**
  48. * Get the file path changed signal.
  49. */
  50. fileChanged: ISignal<FileBrowserModel, IChangedArgs<Contents.IModel>>;
  51. /**
  52. * Get the current path.
  53. */
  54. get path(): string {
  55. return this._model ? this._model.path : '';
  56. }
  57. /**
  58. * Get a read-only list of the items in the current path.
  59. */
  60. get items(): ISequence<Contents.IModel> {
  61. return this._items;
  62. }
  63. /**
  64. * Get whether the model is disposed.
  65. */
  66. get isDisposed(): boolean {
  67. return this._model === null;
  68. }
  69. /**
  70. * Get the session models for active notebooks in the current directory.
  71. */
  72. get sessions(): ISequence<Session.IModel> {
  73. return this._sessions;
  74. }
  75. /**
  76. * Get the kernel specs.
  77. */
  78. get kernelspecs(): Kernel.ISpecModels {
  79. return this._manager.kernelspecs;
  80. }
  81. /**
  82. * Dispose of the resources held by the model.
  83. */
  84. dispose(): void {
  85. if (this.isDisposed) {
  86. return;
  87. }
  88. this._model = null;
  89. this._sessions.clear();
  90. this._items.clear();
  91. this._manager = null;
  92. clearSignalData(this);
  93. }
  94. /**
  95. * Change directory.
  96. *
  97. * @param path - The path to the file or directory.
  98. *
  99. * @returns A promise with the contents of the directory.
  100. */
  101. cd(newValue = '.'): Promise<void> {
  102. if (newValue !== '.') {
  103. newValue = Private.normalizePath(this._model.path, newValue);
  104. }
  105. // Collapse requests to the same directory.
  106. if (newValue === this._pendingPath) {
  107. return this._pending;
  108. }
  109. let oldValue = this.path;
  110. let options: Contents.IFetchOptions = { content: true };
  111. this._pendingPath = newValue;
  112. if (newValue === '.') {
  113. newValue = this.path;
  114. }
  115. if (oldValue !== newValue) {
  116. this._sessions.clear();
  117. }
  118. let manager = this._manager;
  119. this._pending = manager.contents.get(newValue, options).then(contents => {
  120. this._handleContents(contents);
  121. this._pendingPath = null;
  122. return manager.sessions.listRunning();
  123. }).then(models => {
  124. if (this.isDisposed) {
  125. return;
  126. }
  127. this._onRunningChanged(manager.sessions, models);
  128. if (oldValue !== newValue) {
  129. this.pathChanged.emit({
  130. name: 'path',
  131. oldValue,
  132. newValue
  133. });
  134. }
  135. this.refreshed.emit(void 0);
  136. });
  137. return this._pending;
  138. }
  139. /**
  140. * Refresh the current directory.
  141. */
  142. refresh(): Promise<void> {
  143. return this.cd('.').catch(error => {
  144. console.error(error);
  145. let msg = 'Unable to refresh the directory listing due to ';
  146. msg += 'lost server connection.';
  147. error.message = msg;
  148. throw error;
  149. });
  150. }
  151. /**
  152. * Copy a file.
  153. *
  154. * @param fromFile - The path of the original file.
  155. *
  156. * @param toDir - The path to the target directory.
  157. *
  158. * @returns A promise which resolves to the contents of the file.
  159. */
  160. copy(fromFile: string, toDir: string): Promise<Contents.IModel> {
  161. let normalizePath = Private.normalizePath;
  162. fromFile = normalizePath(this._model.path, fromFile);
  163. toDir = normalizePath(this._model.path, toDir);
  164. return this._manager.contents.copy(fromFile, toDir).then(contents => {
  165. this.fileChanged.emit({
  166. name: 'file',
  167. oldValue: void 0,
  168. newValue: contents
  169. });
  170. return contents;
  171. });
  172. }
  173. /**
  174. * Delete a file.
  175. *
  176. * @param: path - The path to the file to be deleted.
  177. *
  178. * @returns A promise which resolves when the file is deleted.
  179. */
  180. deleteFile(path: string): Promise<void> {
  181. let normalizePath = Private.normalizePath;
  182. path = normalizePath(this._model.path, path);
  183. return this._manager.contents.delete(path).then(() => {
  184. this.fileChanged.emit({
  185. name: 'file',
  186. oldValue: { path: path },
  187. newValue: void 0
  188. });
  189. });
  190. }
  191. /**
  192. * Download a file.
  193. *
  194. * @param - path - The path of the file to be downloaded.
  195. */
  196. download(path: string): void {
  197. let url = this._manager.contents.getDownloadUrl(path);
  198. let element = document.createElement('a');
  199. element.setAttribute('href', url);
  200. element.setAttribute('download', '');
  201. element.click();
  202. }
  203. /**
  204. * Create a new untitled file or directory in the current directory.
  205. *
  206. * @param type - The type of file object to create. One of
  207. * `['file', 'notebook', 'directory']`.
  208. *
  209. * @param ext - Optional extension for `'file'` types (defaults to `'.txt'`).
  210. *
  211. * @returns A promise containing the new file contents model.
  212. */
  213. newUntitled(options: Contents.ICreateOptions): Promise<Contents.IModel> {
  214. if (options.type === 'file') {
  215. options.ext = options.ext || '.txt';
  216. }
  217. options.path = options.path || this._model.path;
  218. let promise = this._manager.contents.newUntitled(options);
  219. return promise.then((contents: Contents.IModel) => {
  220. this.fileChanged.emit({
  221. name: 'file',
  222. oldValue: void 0,
  223. newValue: contents
  224. });
  225. return contents;
  226. });
  227. }
  228. /**
  229. * Rename a file or directory.
  230. *
  231. * @param path - The path to the original file.
  232. *
  233. * @param newPath - The path to the new file.
  234. *
  235. * @returns A promise containing the new file contents model.
  236. */
  237. rename(path: string, newPath: string): Promise<Contents.IModel> {
  238. // Handle relative paths.
  239. path = Private.normalizePath(this._model.path, path);
  240. newPath = Private.normalizePath(this._model.path, newPath);
  241. let promise = this._manager.contents.rename(path, newPath);
  242. return promise.then((contents: Contents.IModel) => {
  243. this.fileChanged.emit({
  244. name: 'file',
  245. oldValue: {type: contents.type , path: path},
  246. newValue: contents
  247. });
  248. return contents;
  249. });
  250. }
  251. /**
  252. * Upload a `File` object.
  253. *
  254. * @param file - The `File` object to upload.
  255. *
  256. * @param overwrite - Whether to overwrite an existing file.
  257. *
  258. * @returns A promise containing the new file contents model.
  259. *
  260. * #### Notes
  261. * This will fail to upload files that are too big to be sent in one
  262. * request to the server.
  263. */
  264. upload(file: File, overwrite?: boolean): Promise<Contents.IModel> {
  265. // Skip large files with a warning.
  266. if (file.size > this._maxUploadSizeMb * 1024 * 1024) {
  267. let msg = `Cannot upload file (>${this._maxUploadSizeMb} MB) `;
  268. msg += `"${file.name}"`;
  269. console.warn(msg);
  270. return Promise.reject<Contents.IModel>(new Error(msg));
  271. }
  272. if (overwrite) {
  273. return this._upload(file);
  274. }
  275. let path = this._model.path;
  276. path = path ? path + '/' + file.name : file.name;
  277. return this._manager.contents.get(path, {}).then(() => {
  278. let msg = `"${file.name}" already exists`;
  279. throw new Error(msg);
  280. }, () => {
  281. return this._upload(file);
  282. });
  283. }
  284. /**
  285. * Shut down a session by session id.
  286. */
  287. shutdown(id: string): Promise<void> {
  288. return this._manager.sessions.shutdown(id);
  289. }
  290. /**
  291. * Perform the actual upload.
  292. */
  293. private _upload(file: File): Promise<Contents.IModel> {
  294. // Gather the file model parameters.
  295. let path = this._model.path;
  296. path = path ? path + '/' + file.name : file.name;
  297. let name = file.name;
  298. let isNotebook = file.name.indexOf('.ipynb') !== -1;
  299. let type: Contents.ContentType = isNotebook ? 'notebook' : 'file';
  300. let format: Contents.FileFormat = isNotebook ? 'json' : 'base64';
  301. // Get the file content.
  302. let reader = new FileReader();
  303. if (isNotebook) {
  304. reader.readAsText(file);
  305. } else {
  306. reader.readAsArrayBuffer(file);
  307. }
  308. return new Promise<Contents.IModel>((resolve, reject) => {
  309. reader.onload = (event: Event) => {
  310. let model: Contents.IModel = {
  311. type: type,
  312. format,
  313. name,
  314. content: Private.getContent(reader)
  315. };
  316. let promise = this._manager.contents.save(path, model);
  317. promise.then((contents: Contents.IModel) => {
  318. this.fileChanged.emit({
  319. name: 'file',
  320. oldValue: void 0,
  321. newValue: contents
  322. });
  323. resolve(contents);
  324. });
  325. };
  326. reader.onerror = (event: Event) => {
  327. reject(Error(`Failed to upload "${file.name}":` + event));
  328. };
  329. });
  330. }
  331. /**
  332. * Handle an updated contents model.
  333. */
  334. private _handleContents(contents: Contents.IModel): void {
  335. // Update our internal data.
  336. this._model = contents;
  337. this._items.clear();
  338. this._paths = contents.content.map((model: Contents.IModel) => {
  339. return model.path;
  340. });
  341. this._items = new Vector<Contents.IModel>(contents.content);
  342. this._model.content = null;
  343. }
  344. /**
  345. * Handle a change to the running sessions.
  346. */
  347. private _onRunningChanged(sender: Session.IManager, models: Session.IModel[]): void {
  348. this._sessions.clear();
  349. for (let model of models) {
  350. let index = this._paths.indexOf(model.notebook.path);
  351. if (index !== -1) {
  352. this._sessions.pushBack(model);
  353. }
  354. }
  355. this.refreshed.emit(void 0);
  356. }
  357. private _maxUploadSizeMb = 15;
  358. private _manager: IServiceManager = null;
  359. private _sessions = new Vector<Session.IModel>();
  360. private _items = new Vector<Contents.IModel>();
  361. private _paths: string[] = [];
  362. private _model: Contents.IModel;
  363. private _pendingPath: string = null;
  364. private _pending: Promise<void> = null;
  365. }
  366. // Define the signals for the `FileBrowserModel` class.
  367. defineSignal(FileBrowserModel.prototype, 'pathChanged');
  368. defineSignal(FileBrowserModel.prototype, 'refreshed');
  369. defineSignal(FileBrowserModel.prototype, 'fileChanged');
  370. /**
  371. * The namespace for the `FileBrowserModel` class statics.
  372. */
  373. export
  374. namespace FileBrowserModel {
  375. /**
  376. * An options object for initializing a file browser.
  377. */
  378. export
  379. interface IOptions {
  380. /**
  381. * A service manager instance.
  382. */
  383. manager: IServiceManager;
  384. }
  385. }
  386. /**
  387. * The namespace for the file browser model private data.
  388. */
  389. namespace Private {
  390. /**
  391. * Parse the content of a `FileReader`.
  392. *
  393. * If the result is an `ArrayBuffer`, return a Base64-encoded string.
  394. * Otherwise, return the JSON parsed result.
  395. */
  396. export
  397. function getContent(reader: FileReader): any {
  398. if (reader.result instanceof ArrayBuffer) {
  399. // Base64-encode binary file data.
  400. let bytes = '';
  401. let buf = new Uint8Array(reader.result);
  402. let nbytes = buf.byteLength;
  403. for (let i = 0; i < nbytes; i++) {
  404. bytes += String.fromCharCode(buf[i]);
  405. }
  406. return btoa(bytes);
  407. } else {
  408. return JSON.parse(reader.result);
  409. }
  410. }
  411. /**
  412. * Normalize a path based on a root directory, accounting for relative paths.
  413. */
  414. export
  415. function normalizePath(root: string, path: string): string {
  416. return ContentsManager.getAbsolutePath(path, root);
  417. }
  418. }