index.ts 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. URLExt, PathExt, ModelDB
  5. } from '@jupyterlab/coreutils';
  6. import {
  7. JSONExt, JSONObject
  8. } from '@phosphor/coreutils';
  9. import {
  10. each
  11. } from '@phosphor/algorithm';
  12. import {
  13. IDisposable
  14. } from '@phosphor/disposable';
  15. import {
  16. ISignal, Signal
  17. } from '@phosphor/signaling';
  18. import {
  19. ServerConnection
  20. } from '..';
  21. import * as validate
  22. from './validate';
  23. /**
  24. * The url for the default drive service.
  25. */
  26. const SERVICE_DRIVE_URL = 'api/contents';
  27. /**
  28. * The url for the file access.
  29. */
  30. const FILES_URL = 'files';
  31. /**
  32. * A namespace for contents interfaces.
  33. */
  34. export
  35. namespace Contents {
  36. /**
  37. * A contents model.
  38. */
  39. export
  40. interface IModel {
  41. /**
  42. * Name of the contents file.
  43. *
  44. * #### Notes
  45. * Equivalent to the last part of the `path` field.
  46. */
  47. readonly name?: string;
  48. /**
  49. * The full file path.
  50. *
  51. * #### Notes
  52. * It will *not* start with `/`, and it will be `/`-delimited.
  53. */
  54. readonly path?: string;
  55. /**
  56. * The type of file.
  57. */
  58. readonly type?: ContentType;
  59. /**
  60. * Whether the requester has permission to edit the file.
  61. */
  62. readonly writable?: boolean;
  63. /**
  64. * File creation timestamp.
  65. */
  66. readonly created?: string;
  67. /**
  68. * Last modified timestamp.
  69. */
  70. readonly last_modified?: string;
  71. /**
  72. * Specify the mime-type of file contents.
  73. *
  74. * #### Notes
  75. * Only non-`null` when `content` is present and `type` is `"file"`.
  76. */
  77. readonly mimetype?: string;
  78. /**
  79. * The optional file content.
  80. */
  81. readonly content?: any;
  82. /**
  83. * The format of the file `content`.
  84. *
  85. * #### Notes
  86. * Only relevant for type: 'file'
  87. */
  88. readonly format?: FileFormat;
  89. }
  90. /**
  91. * A contents file type.
  92. */
  93. export
  94. type ContentType = 'notebook' | 'file' | 'directory';
  95. /**
  96. * A contents file format.
  97. */
  98. export
  99. type FileFormat = 'json' | 'text' | 'base64';
  100. /**
  101. * The options used to fetch a file.
  102. */
  103. export
  104. interface IFetchOptions extends JSONObject {
  105. /**
  106. * The override file type for the request.
  107. */
  108. type?: ContentType;
  109. /**
  110. * The override file format for the request.
  111. */
  112. format?: FileFormat;
  113. /**
  114. * Whether to include the file content.
  115. *
  116. * The default is `true`.
  117. */
  118. content?: boolean;
  119. }
  120. /**
  121. * The options used to create a file.
  122. */
  123. export
  124. interface ICreateOptions extends JSONObject {
  125. /**
  126. * The directory in which to create the file.
  127. */
  128. path?: string;
  129. /**
  130. * The optional file extension for the new file (e.g. `".txt"`).
  131. *
  132. * #### Notes
  133. * This ignored if `type` is `'notebook'`.
  134. */
  135. ext?: string;
  136. /**
  137. * The file type.
  138. */
  139. type?: ContentType;
  140. }
  141. /**
  142. * Checkpoint model.
  143. */
  144. export
  145. interface ICheckpointModel {
  146. /**
  147. * The unique identifier for the checkpoint.
  148. */
  149. readonly id: string;
  150. /**
  151. * Last modified timestamp.
  152. */
  153. readonly last_modified: string;
  154. }
  155. /**
  156. * The change args for a file change.
  157. */
  158. export
  159. interface IChangedArgs {
  160. /**
  161. * The type of change.
  162. */
  163. type: 'new' | 'delete' | 'rename' | 'save';
  164. /**
  165. * The new contents.
  166. */
  167. oldValue: IModel | null;
  168. /**
  169. * The old contents.
  170. */
  171. newValue: IModel | null;
  172. }
  173. /**
  174. * The interface for a contents manager.
  175. */
  176. export
  177. interface IManager extends IDisposable {
  178. /**
  179. * A signal emitted when a file operation takes place.
  180. */
  181. fileChanged: ISignal<IManager, IChangedArgs>;
  182. /**
  183. * Add an `IDrive` to the manager.
  184. */
  185. addDrive(drive: IDrive): void;
  186. /**
  187. * Given a path, get a ModelDB.IFactory from the
  188. * relevant backend. Returns `null` if the backend
  189. * does not provide one.
  190. */
  191. getModelDBFactory(path: string): ModelDB.IFactory;
  192. /**
  193. * Get a file or directory.
  194. *
  195. * @param path: The path to the file.
  196. *
  197. * @param options: The options used to fetch the file.
  198. *
  199. * @returns A promise which resolves with the file content.
  200. */
  201. get(path: string, options?: IFetchOptions): Promise<IModel>;
  202. /**
  203. * Get an encoded download url given a file path.
  204. *
  205. * @param A promise which resolves with the absolute POSIX
  206. * file path on the server.
  207. */
  208. getDownloadUrl(path: string): Promise<string>;
  209. /**
  210. * Create a new untitled file or directory in the specified directory path.
  211. *
  212. * @param options: The options used to create the file.
  213. *
  214. * @returns A promise which resolves with the created file content when the
  215. * file is created.
  216. */
  217. newUntitled(options?: ICreateOptions): Promise<IModel>;
  218. /**
  219. * Delete a file.
  220. *
  221. * @param path - The path to the file.
  222. *
  223. * @returns A promise which resolves when the file is deleted.
  224. */
  225. delete(path: string): Promise<void>;
  226. /**
  227. * Rename a file or directory.
  228. *
  229. * @param path - The original file path.
  230. *
  231. * @param newPath - The new file path.
  232. *
  233. * @returns A promise which resolves with the new file content model when the
  234. * file is renamed.
  235. */
  236. rename(path: string, newPath: string): Promise<IModel>;
  237. /**
  238. * Save a file.
  239. *
  240. * @param path - The desired file path.
  241. *
  242. * @param options - Optional overrrides to the model.
  243. *
  244. * @returns A promise which resolves with the file content model when the
  245. * file is saved.
  246. */
  247. save(path: string, options?: IModel): Promise<IModel>;
  248. /**
  249. * Copy a file into a given directory.
  250. *
  251. * @param path - The original file path.
  252. *
  253. * @param toDir - The destination directory path.
  254. *
  255. * @returns A promise which resolves with the new content model when the
  256. * file is copied.
  257. */
  258. copy(path: string, toDir: string): Promise<IModel>;
  259. /**
  260. * Create a checkpoint for a file.
  261. *
  262. * @param path - The path of the file.
  263. *
  264. * @returns A promise which resolves with the new checkpoint model when the
  265. * checkpoint is created.
  266. */
  267. createCheckpoint(path: string): Promise<IModel>;
  268. /**
  269. * List available checkpoints for a file.
  270. *
  271. * @param path - The path of the file.
  272. *
  273. * @returns A promise which resolves with a list of checkpoint models for
  274. * the file.
  275. */
  276. listCheckpoints(path: string): Promise<ICheckpointModel[]>;
  277. /**
  278. * Restore a file to a known checkpoint state.
  279. *
  280. * @param path - The path of the file.
  281. *
  282. * @param checkpointID - The id of the checkpoint to restore.
  283. *
  284. * @returns A promise which resolves when the checkpoint is restored.
  285. */
  286. restoreCheckpoint(path: string, checkpointID: string): Promise<void>;
  287. /**
  288. * Delete a checkpoint for a file.
  289. *
  290. * @param path - The path of the file.
  291. *
  292. * @param checkpointID - The id of the checkpoint to delete.
  293. *
  294. * @returns A promise which resolves when the checkpoint is deleted.
  295. */
  296. deleteCheckpoint(path: string, checkpointID: string): Promise<void>;
  297. }
  298. /**
  299. * The interface for a network drive that can be mounted
  300. * in the contents manager.
  301. */
  302. export
  303. interface IDrive extends IDisposable {
  304. /**
  305. * The name of the drive, which is used at the leading
  306. * component of file paths.
  307. */
  308. readonly name: string;
  309. /**
  310. * The server settings of the manager.
  311. */
  312. readonly serverSettings: ServerConnection.ISettings;
  313. /**
  314. * An optional ModelDB.IFactory instance for the
  315. * drive.
  316. */
  317. readonly modelDBFactory?: ModelDB.IFactory;
  318. /**
  319. * A signal emitted when a file operation takes place.
  320. */
  321. fileChanged: ISignal<IDrive, IChangedArgs>;
  322. /**
  323. * Get a file or directory.
  324. *
  325. * @param localPath: The path to the file.
  326. *
  327. * @param options: The options used to fetch the file.
  328. *
  329. * @returns A promise which resolves with the file content.
  330. */
  331. get(localPath: string, options?: IFetchOptions): Promise<IModel>;
  332. /**
  333. * Get an encoded download url given a file path.
  334. *
  335. * @param A promise which resolves with the absolute POSIX
  336. * file path on the server.
  337. */
  338. getDownloadUrl(localPath: string): Promise<string>;
  339. /**
  340. * Create a new untitled file or directory in the specified directory path.
  341. *
  342. * @param options: The options used to create the file.
  343. *
  344. * @returns A promise which resolves with the created file content when the
  345. * file is created.
  346. */
  347. newUntitled(options?: ICreateOptions): Promise<IModel>;
  348. /**
  349. * Delete a file.
  350. *
  351. * @param localPath - The path to the file.
  352. *
  353. * @returns A promise which resolves when the file is deleted.
  354. */
  355. delete(localPath: string): Promise<void>;
  356. /**
  357. * Rename a file or directory.
  358. *
  359. * @param oldLocalPath - The original file path.
  360. *
  361. * @param newLocalPath - The new file path.
  362. *
  363. * @returns A promise which resolves with the new file content model when the
  364. * file is renamed.
  365. */
  366. rename(oldLocalPath: string, newLocalPath: string): Promise<IModel>;
  367. /**
  368. * Save a file.
  369. *
  370. * @param localPath - The desired file path.
  371. *
  372. * @param options - Optional overrrides to the model.
  373. *
  374. * @returns A promise which resolves with the file content model when the
  375. * file is saved.
  376. */
  377. save(localPath: string, options?: IModel): Promise<IModel>;
  378. /**
  379. * Copy a file into a given directory.
  380. *
  381. * @param localPath - The original file path.
  382. *
  383. * @param toLocalDir - The destination directory path.
  384. *
  385. * @returns A promise which resolves with the new content model when the
  386. * file is copied.
  387. */
  388. copy(localPath: string, toLocalDir: string): Promise<IModel>;
  389. /**
  390. * Create a checkpoint for a file.
  391. *
  392. * @param localPath - The path of the file.
  393. *
  394. * @returns A promise which resolves with the new checkpoint model when the
  395. * checkpoint is created.
  396. */
  397. createCheckpoint(localPath: string): Promise<IModel>;
  398. /**
  399. * List available checkpoints for a file.
  400. *
  401. * @param localPath - The path of the file.
  402. *
  403. * @returns A promise which resolves with a list of checkpoint models for
  404. * the file.
  405. */
  406. listCheckpoints(localPath: string): Promise<ICheckpointModel[]>;
  407. /**
  408. * Restore a file to a known checkpoint state.
  409. *
  410. * @param localPath - The path of the file.
  411. *
  412. * @param checkpointID - The id of the checkpoint to restore.
  413. *
  414. * @returns A promise which resolves when the checkpoint is restored.
  415. */
  416. restoreCheckpoint(localPath: string, checkpointID: string): Promise<void>;
  417. /**
  418. * Delete a checkpoint for a file.
  419. *
  420. * @param localPath - The path of the file.
  421. *
  422. * @param checkpointID - The id of the checkpoint to delete.
  423. *
  424. * @returns A promise which resolves when the checkpoint is deleted.
  425. */
  426. deleteCheckpoint(localPath: string, checkpointID: string): Promise<void>;
  427. }
  428. }
  429. /**
  430. * A contents manager that passes file operations to the server.
  431. * Multiple servers implementing the `IDrive` interface can be
  432. * attached to the contents manager, so that the same session can
  433. * perform file operations on multiple backends.
  434. *
  435. * This includes checkpointing with the normal file operations.
  436. */
  437. export
  438. class ContentsManager implements Contents.IManager {
  439. /**
  440. * Construct a new contents manager object.
  441. *
  442. * @param options - The options used to initialize the object.
  443. */
  444. constructor(options: ContentsManager.IOptions = {}) {
  445. this._defaultDrive = options.defaultDrive || new Drive();
  446. this._defaultDrive.fileChanged.connect(this._onFileChanged, this);
  447. }
  448. /**
  449. * A signal emitted when a file operation takes place.
  450. */
  451. get fileChanged(): ISignal<this, Contents.IChangedArgs> {
  452. return this._fileChanged;
  453. }
  454. /**
  455. * Test whether the manager has been disposed.
  456. */
  457. get isDisposed(): boolean {
  458. return this._isDisposed;
  459. }
  460. /**
  461. * Dispose of the resources held by the manager.
  462. */
  463. dispose(): void {
  464. if (this.isDisposed) {
  465. return;
  466. }
  467. this._isDisposed = true;
  468. Signal.clearData(this);
  469. }
  470. /**
  471. * Add an `IDrive` to the manager.
  472. */
  473. addDrive(drive: Contents.IDrive): void {
  474. this._additionalDrives.set(drive.name, drive);
  475. drive.fileChanged.connect(this._onFileChanged, this);
  476. }
  477. /**
  478. * Given a path, get a ModelDB.IFactory from the
  479. * relevant backend. Returns `null` if the backend
  480. * does not provide one.
  481. */
  482. getModelDBFactory(path: string): ModelDB.IFactory {
  483. let [drive,] = this._driveForPath(path);
  484. return drive.modelDBFactory || null;
  485. }
  486. /**
  487. * Get a file or directory.
  488. *
  489. * @param path: The path to the file.
  490. *
  491. * @param options: The options used to fetch the file.
  492. *
  493. * @returns A promise which resolves with the file content.
  494. */
  495. get(path: string, options?: Contents.IFetchOptions): Promise<Contents.IModel> {
  496. let [drive, localPath] = this._driveForPath(path);
  497. return drive.get(localPath, options).then( contentsModel => {
  498. let listing: Contents.IModel[] = [];
  499. if (contentsModel.type === 'directory') {
  500. each(contentsModel.content, (item: Contents.IModel) => {
  501. listing.push({
  502. ...item,
  503. path: this._toGlobalPath(drive, item.path)
  504. } as Contents.IModel);
  505. });
  506. return {
  507. ...contentsModel,
  508. path: Private.normalize(path),
  509. content: listing
  510. } as Contents.IModel;
  511. } else {
  512. return {
  513. ...contentsModel,
  514. path: Private.normalize(path)
  515. } as Contents.IModel;
  516. }
  517. });
  518. }
  519. /**
  520. * Get an encoded download url given a file path.
  521. *
  522. * @param path - An absolute POSIX file path on the server.
  523. *
  524. * #### Notes
  525. * It is expected that the path contains no relative paths.
  526. */
  527. getDownloadUrl(path: string): Promise<string> {
  528. let [drive, localPath] = this._driveForPath(path);
  529. return drive.getDownloadUrl(localPath);
  530. }
  531. /**
  532. * Create a new untitled file or directory in the specified directory path.
  533. *
  534. * @param options: The options used to create the file.
  535. *
  536. * @returns A promise which resolves with the created file content when the
  537. * file is created.
  538. */
  539. newUntitled(options: Contents.ICreateOptions = {}): Promise<Contents.IModel> {
  540. if (options.path) {
  541. let globalPath = Private.normalize(options.path);
  542. let [drive, localPath] = this._driveForPath(globalPath);
  543. return drive.newUntitled({ ...options, path: localPath }).then( contentsModel => {
  544. return {
  545. ...contentsModel,
  546. path: PathExt.join(globalPath, contentsModel.name)
  547. } as Contents.IModel;
  548. });
  549. } else {
  550. return this._defaultDrive.newUntitled(options);
  551. }
  552. }
  553. /**
  554. * Delete a file.
  555. *
  556. * @param path - The path to the file.
  557. *
  558. * @returns A promise which resolves when the file is deleted.
  559. */
  560. delete(path: string): Promise<void> {
  561. let [drive, localPath] = this._driveForPath(path);
  562. return drive.delete(localPath);
  563. }
  564. /**
  565. * Rename a file or directory.
  566. *
  567. * @param path - The original file path.
  568. *
  569. * @param newPath - The new file path.
  570. *
  571. * @returns A promise which resolves with the new file contents model when
  572. * the file is renamed.
  573. */
  574. rename(path: string, newPath: string): Promise<Contents.IModel> {
  575. let [drive1, path1] = this._driveForPath(path);
  576. let [drive2, path2] = this._driveForPath(newPath);
  577. if (drive1 !== drive2) {
  578. throw Error('ContentsManager: renaming files must occur within a Drive');
  579. }
  580. return drive1.rename(path1, path2).then(contentsModel => {
  581. return { ...contentsModel, path: path } as Contents.IModel;
  582. });
  583. }
  584. /**
  585. * Save a file.
  586. *
  587. * @param path - The desired file path.
  588. *
  589. * @param options - Optional overrides to the model.
  590. *
  591. * @returns A promise which resolves with the file content model when the
  592. * file is saved.
  593. *
  594. * #### Notes
  595. * Ensure that `model.content` is populated for the file.
  596. */
  597. save(path: string, options: Contents.IModel = {}): Promise<Contents.IModel> {
  598. let [drive, localPath] = this._driveForPath(path);
  599. return drive.save(localPath, { ...options, path: localPath }).then(contentsModel => {
  600. return { ...contentsModel, path: Private.normalize(path) } as Contents.IModel;
  601. });
  602. }
  603. /**
  604. * Copy a file into a given directory.
  605. *
  606. * @param path - The original file path.
  607. *
  608. * @param toDir - The destination directory path.
  609. *
  610. * @returns A promise which resolves with the new contents model when the
  611. * file is copied.
  612. *
  613. * #### Notes
  614. * The server will select the name of the copied file.
  615. */
  616. copy(fromFile: string, toDir: string): Promise<Contents.IModel> {
  617. let [drive1, path1] = this._driveForPath(fromFile);
  618. let [drive2, path2] = this._driveForPath(toDir);
  619. if (drive1 === drive2) {
  620. return drive1.copy(path1, path2).then(contentsModel => {
  621. return {
  622. ...contentsModel,
  623. path: this._toGlobalPath(drive1, contentsModel.path)
  624. } as Contents.IModel;
  625. });
  626. } else {
  627. throw Error('Copying files between drives is not currently implemented');
  628. }
  629. }
  630. /**
  631. * Create a checkpoint for a file.
  632. *
  633. * @param path - The path of the file.
  634. *
  635. * @returns A promise which resolves with the new checkpoint model when the
  636. * checkpoint is created.
  637. */
  638. createCheckpoint(path: string): Promise<Contents.ICheckpointModel> {
  639. let [drive, localPath] = this._driveForPath(path);
  640. return drive.createCheckpoint(localPath);
  641. }
  642. /**
  643. * List available checkpoints for a file.
  644. *
  645. * @param path - The path of the file.
  646. *
  647. * @returns A promise which resolves with a list of checkpoint models for
  648. * the file.
  649. */
  650. listCheckpoints(path: string): Promise<Contents.ICheckpointModel[]> {
  651. let [drive, localPath] = this._driveForPath(path);
  652. return drive.listCheckpoints(localPath);
  653. }
  654. /**
  655. * Restore a file to a known checkpoint state.
  656. *
  657. * @param path - The path of the file.
  658. *
  659. * @param checkpointID - The id of the checkpoint to restore.
  660. *
  661. * @returns A promise which resolves when the checkpoint is restored.
  662. */
  663. restoreCheckpoint(path: string, checkpointID: string): Promise<void> {
  664. let [drive, localPath] = this._driveForPath(path);
  665. return drive.restoreCheckpoint(localPath, checkpointID);
  666. }
  667. /**
  668. * Delete a checkpoint for a file.
  669. *
  670. * @param path - The path of the file.
  671. *
  672. * @param checkpointID - The id of the checkpoint to delete.
  673. *
  674. * @returns A promise which resolves when the checkpoint is deleted.
  675. */
  676. deleteCheckpoint(path: string, checkpointID: string): Promise<void> {
  677. let [drive, localPath] = this._driveForPath(path);
  678. return drive.deleteCheckpoint(localPath, checkpointID);
  679. }
  680. /**
  681. * Given a drive and a local path, construct a fully qualified
  682. * path. The inverse of `_driveForPath`.
  683. *
  684. * @param drive: an `IDrive`.
  685. *
  686. * @param localPath: the local path on the drive.
  687. *
  688. * @returns the fully qualified path.
  689. */
  690. private _toGlobalPath(drive: Contents.IDrive, localPath: string): string {
  691. if (drive === this._defaultDrive) {
  692. return PathExt.normalize(localPath);
  693. } else {
  694. return drive.name + ':' + PathExt.normalize(localPath);
  695. }
  696. }
  697. /**
  698. * Given a path, get the `IDrive to which it refers,
  699. * where the path satisfies the pattern
  700. * `'driveName:path/to/file'`. If there is no `driveName`
  701. * prepended to the path, it returns the default drive.
  702. *
  703. * @param path: a path to a file.
  704. *
  705. * @returns A tuple containing an `IDrive` object for the path,
  706. * and a local path for that drive.
  707. */
  708. private _driveForPath(path: string): [Contents.IDrive, string] {
  709. // Split the path at ':'
  710. let parts = path.split(':')
  711. if (parts.length === 1) {
  712. return [this._defaultDrive, path]
  713. } else {
  714. let drive = this._additionalDrives.get(parts[0]);
  715. if (!drive) {
  716. throw Error('ContentsManager: cannot find requested drive');
  717. }
  718. return [drive, Private.normalize(parts[1])];
  719. }
  720. }
  721. private _onFileChanged(sender: Contents.IDrive, args: Contents.IChangedArgs) {
  722. if (sender === this._defaultDrive) {
  723. this._fileChanged.emit(args);
  724. } else {
  725. let newValue: Contents.IModel = null;
  726. let oldValue: Contents.IModel = null;
  727. if (args.newValue) {
  728. newValue = {
  729. ...args.newValue,
  730. path: this._toGlobalPath(sender, args.newValue.path)
  731. };
  732. }
  733. if (args.oldValue) {
  734. oldValue = {
  735. ...args.oldValue,
  736. path: this._toGlobalPath(sender, args.oldValue.path)
  737. };
  738. }
  739. this._fileChanged.emit({
  740. type: args.type,
  741. newValue,
  742. oldValue
  743. });
  744. }
  745. }
  746. private _isDisposed = false;
  747. private _additionalDrives = new Map<string, Contents.IDrive>();
  748. private _defaultDrive: Contents.IDrive = null;
  749. private _fileChanged = new Signal<this, Contents.IChangedArgs>(this);
  750. }
  751. /**
  752. * A default implementation for an `IDrive`, talking to the
  753. * server using the Jupyter REST API.
  754. */
  755. export
  756. class Drive implements Contents.IDrive {
  757. /**
  758. * Construct a new contents manager object.
  759. *
  760. * @param options - The options used to initialize the object.
  761. */
  762. constructor(options: Drive.IOptions = {}) {
  763. this.name = options.name || 'Default';
  764. this._apiEndpoint = options.apiEndpoint || SERVICE_DRIVE_URL;
  765. this.serverSettings = options.serverSettings || ServerConnection.makeSettings();
  766. }
  767. /**
  768. * The name of the drive, which is used at the leading
  769. * component of file paths.
  770. */
  771. readonly name: string;
  772. /**
  773. * A signal emitted when a file operation takes place.
  774. */
  775. get fileChanged(): ISignal<this, Contents.IChangedArgs> {
  776. return this._fileChanged;
  777. }
  778. /**
  779. * The server settings of the manager.
  780. */
  781. readonly serverSettings: ServerConnection.ISettings;
  782. /**
  783. * Test whether the manager has been disposed.
  784. */
  785. get isDisposed(): boolean {
  786. return this._isDisposed;
  787. }
  788. /**
  789. * Dispose of the resources held by the manager.
  790. */
  791. dispose(): void {
  792. if (this.isDisposed) {
  793. return;
  794. }
  795. this._isDisposed = true;
  796. Signal.clearData(this);
  797. }
  798. /**
  799. * Get a file or directory.
  800. *
  801. * @param localPath: The path to the file.
  802. *
  803. * @param options: The options used to fetch the file.
  804. *
  805. * @returns A promise which resolves with the file content.
  806. *
  807. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
  808. */
  809. get(localPath: string, options?: Contents.IFetchOptions): Promise<Contents.IModel> {
  810. let url = this._getUrl(localPath);
  811. if (options) {
  812. // The notebook type cannot take an format option.
  813. if (options.type === 'notebook') {
  814. delete options['format'];
  815. }
  816. let params: any = JSONExt.deepCopy(options);
  817. params.content = options.content ? '1' : '0';
  818. url += URLExt.objectToQueryString(params);
  819. }
  820. let request = {
  821. url,
  822. method: 'GET',
  823. cache: false
  824. };
  825. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  826. if (response.xhr.status !== 200) {
  827. throw ServerConnection.makeError(response);
  828. }
  829. try {
  830. validate.validateContentsModel(response.data);
  831. } catch (err) {
  832. throw ServerConnection.makeError(response, err.message);
  833. }
  834. return response.data;
  835. });
  836. }
  837. /**
  838. * Get an encoded download url given a file path.
  839. *
  840. * @param localPath - An absolute POSIX file path on the server.
  841. *
  842. * #### Notes
  843. * It is expected that the path contains no relative paths.
  844. */
  845. getDownloadUrl(localPath: string): Promise<string> {
  846. let baseUrl = this.serverSettings.baseUrl;
  847. return Promise.resolve(URLExt.join(baseUrl, FILES_URL,
  848. URLExt.encodeParts(localPath)));
  849. }
  850. /**
  851. * Create a new untitled file or directory in the specified directory path.
  852. *
  853. * @param options: The options used to create the file.
  854. *
  855. * @returns A promise which resolves with the created file content when the
  856. * file is created.
  857. *
  858. * #### Notes
  859. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
  860. */
  861. newUntitled(options: Contents.ICreateOptions = {}): Promise<Contents.IModel> {
  862. let data = '{}';
  863. if (options) {
  864. if (options.ext) {
  865. options.ext = Private.normalizeExtension(options.ext);
  866. }
  867. data = JSON.stringify(options);
  868. }
  869. let request = {
  870. url: this._getUrl(options.path || ''),
  871. method: 'POST',
  872. data,
  873. };
  874. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  875. if (response.xhr.status !== 201) {
  876. throw ServerConnection.makeError(response);
  877. }
  878. let data = response.data as Contents.IModel;
  879. try {
  880. validate.validateContentsModel(data);
  881. } catch (err) {
  882. throw ServerConnection.makeError(response, err.message);
  883. }
  884. this._fileChanged.emit({
  885. type: 'new',
  886. oldValue: null,
  887. newValue: data
  888. });
  889. return data;
  890. });
  891. }
  892. /**
  893. * Delete a file.
  894. *
  895. * @param localPath - The path to the file.
  896. *
  897. * @returns A promise which resolves when the file is deleted.
  898. *
  899. * #### Notes
  900. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
  901. */
  902. delete(localPath: string): Promise<void> {
  903. let request = {
  904. url: this._getUrl(localPath),
  905. method: 'DELETE'
  906. };
  907. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  908. if (response.xhr.status !== 204) {
  909. throw ServerConnection.makeError(response);
  910. }
  911. this._fileChanged.emit({
  912. type: 'delete',
  913. oldValue: { path: localPath },
  914. newValue: null
  915. });
  916. }, error => {
  917. // Translate certain errors to more specific ones.
  918. // TODO: update IPEP27 to specify errors more precisely, so
  919. // that error types can be detected here with certainty.
  920. if (error.xhr.status === 400) {
  921. let err = JSON.parse(error.xhr.response);
  922. if (err.message) {
  923. error.message = err.message;
  924. }
  925. }
  926. return Promise.reject(error);
  927. }
  928. );
  929. }
  930. /**
  931. * Rename a file or directory.
  932. *
  933. * @param oldLocalPath - The original file path.
  934. *
  935. * @param newLocalPath - The new file path.
  936. *
  937. * @returns A promise which resolves with the new file contents model when
  938. * the file is renamed.
  939. *
  940. * #### Notes
  941. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
  942. */
  943. rename(oldLocalPath: string, newLocalPath: string): Promise<Contents.IModel> {
  944. let request = {
  945. url: this._getUrl(oldLocalPath),
  946. method: 'PATCH',
  947. data: JSON.stringify({ localPath: newLocalPath })
  948. };
  949. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  950. if (response.xhr.status !== 200) {
  951. throw ServerConnection.makeError(response);
  952. }
  953. let data = response.data as Contents.IModel;
  954. try {
  955. validate.validateContentsModel(data);
  956. } catch (err) {
  957. throw ServerConnection.makeError(response, err.message);
  958. }
  959. this._fileChanged.emit({
  960. type: 'rename',
  961. oldValue: { path: oldLocalPath },
  962. newValue: data
  963. });
  964. return data;
  965. });
  966. }
  967. /**
  968. * Save a file.
  969. *
  970. * @param localPath - The desired file path.
  971. *
  972. * @param options - Optional overrides to the model.
  973. *
  974. * @returns A promise which resolves with the file content model when the
  975. * file is saved.
  976. *
  977. * #### Notes
  978. * Ensure that `model.content` is populated for the file.
  979. *
  980. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
  981. */
  982. save(localPath: string, options: Contents.IModel = {}): Promise<Contents.IModel> {
  983. let request = {
  984. url: this._getUrl(localPath),
  985. method: 'PUT',
  986. cache: false,
  987. data: JSON.stringify(options)
  988. };
  989. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  990. // will return 200 for an existing file and 201 for a new file
  991. if (response.xhr.status !== 200 && response.xhr.status !== 201) {
  992. throw ServerConnection.makeError(response);
  993. }
  994. let data = response.data as Contents.IModel;
  995. try {
  996. validate.validateContentsModel(data);
  997. } catch (err) {
  998. throw ServerConnection.makeError(response, err.message);
  999. }
  1000. this._fileChanged.emit({
  1001. type: 'save',
  1002. oldValue: null,
  1003. newValue: data
  1004. });
  1005. return data;
  1006. });
  1007. }
  1008. /**
  1009. * Copy a file into a given directory.
  1010. *
  1011. * @param localPath - The original file path.
  1012. *
  1013. * @param toDir - The destination directory path.
  1014. *
  1015. * @returns A promise which resolves with the new contents model when the
  1016. * file is copied.
  1017. *
  1018. * #### Notes
  1019. * The server will select the name of the copied file.
  1020. *
  1021. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
  1022. */
  1023. copy(fromFile: string, toDir: string): Promise<Contents.IModel> {
  1024. let request = {
  1025. url: this._getUrl(toDir),
  1026. method: 'POST',
  1027. data: JSON.stringify({ copy_from: fromFile })
  1028. };
  1029. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  1030. if (response.xhr.status !== 201) {
  1031. throw ServerConnection.makeError(response);
  1032. }
  1033. let data = response.data as Contents.IModel;
  1034. try {
  1035. validate.validateContentsModel(data);
  1036. } catch (err) {
  1037. throw ServerConnection.makeError(response, err.message);
  1038. }
  1039. this._fileChanged.emit({
  1040. type: 'new',
  1041. oldValue: null,
  1042. newValue: data
  1043. });
  1044. return data;
  1045. });
  1046. }
  1047. /**
  1048. * Create a checkpoint for a file.
  1049. *
  1050. * @param localPath - The path of the file.
  1051. *
  1052. * @returns A promise which resolves with the new checkpoint model when the
  1053. * checkpoint is created.
  1054. *
  1055. * #### Notes
  1056. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
  1057. */
  1058. createCheckpoint(localPath: string): Promise<Contents.ICheckpointModel> {
  1059. let request = {
  1060. url: this._getUrl(localPath, 'checkpoints'),
  1061. method: 'POST'
  1062. };
  1063. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  1064. if (response.xhr.status !== 201) {
  1065. throw ServerConnection.makeError(response);
  1066. }
  1067. let data = response.data as Contents.ICheckpointModel;
  1068. try {
  1069. validate.validateCheckpointModel(data);
  1070. } catch (err) {
  1071. throw ServerConnection.makeError(response, err.message);
  1072. }
  1073. return data;
  1074. });
  1075. }
  1076. /**
  1077. * List available checkpoints for a file.
  1078. *
  1079. * @param localPath - The path of the file.
  1080. *
  1081. * @returns A promise which resolves with a list of checkpoint models for
  1082. * the file.
  1083. *
  1084. * #### Notes
  1085. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model.
  1086. */
  1087. listCheckpoints(localPath: string): Promise<Contents.ICheckpointModel[]> {
  1088. let request = {
  1089. url: this._getUrl(localPath, 'checkpoints'),
  1090. method: 'GET',
  1091. cache: false
  1092. };
  1093. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  1094. if (response.xhr.status !== 200) {
  1095. throw ServerConnection.makeError(response);
  1096. }
  1097. if (!Array.isArray(response.data)) {
  1098. throw ServerConnection.makeError(response, 'Invalid Checkpoint list');
  1099. }
  1100. for (let i = 0; i < response.data.length; i++) {
  1101. try {
  1102. validate.validateCheckpointModel(response.data[i]);
  1103. } catch (err) {
  1104. throw ServerConnection.makeError(response, err.message);
  1105. }
  1106. }
  1107. return response.data;
  1108. });
  1109. }
  1110. /**
  1111. * Restore a file to a known checkpoint state.
  1112. *
  1113. * @param localPath - The path of the file.
  1114. *
  1115. * @param checkpointID - The id of the checkpoint to restore.
  1116. *
  1117. * @returns A promise which resolves when the checkpoint is restored.
  1118. *
  1119. * #### Notes
  1120. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
  1121. */
  1122. restoreCheckpoint(localPath: string, checkpointID: string): Promise<void> {
  1123. let request = {
  1124. url: this._getUrl(localPath, 'checkpoints', checkpointID),
  1125. method: 'POST'
  1126. };
  1127. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  1128. if (response.xhr.status !== 204) {
  1129. throw ServerConnection.makeError(response);
  1130. }
  1131. });
  1132. }
  1133. /**
  1134. * Delete a checkpoint for a file.
  1135. *
  1136. * @param localPath - The path of the file.
  1137. *
  1138. * @param checkpointID - The id of the checkpoint to delete.
  1139. *
  1140. * @returns A promise which resolves when the checkpoint is deleted.
  1141. *
  1142. * #### Notes
  1143. * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
  1144. */
  1145. deleteCheckpoint(localPath: string, checkpointID: string): Promise<void> {
  1146. let request = {
  1147. url: this._getUrl(localPath, 'checkpoints', checkpointID),
  1148. method: 'DELETE'
  1149. };
  1150. return ServerConnection.makeRequest(request, this.serverSettings).then(response => {
  1151. if (response.xhr.status !== 204) {
  1152. throw ServerConnection.makeError(response);
  1153. }
  1154. });
  1155. }
  1156. /**
  1157. * Get a REST url for a file given a path.
  1158. */
  1159. private _getUrl(...args: string[]): string {
  1160. let parts = args.map(path => URLExt.encodeParts(path));
  1161. let baseUrl = this.serverSettings.baseUrl;
  1162. return URLExt.join(baseUrl, this._apiEndpoint,
  1163. ...parts);
  1164. }
  1165. private _apiEndpoint: string;
  1166. private _isDisposed = false;
  1167. private _fileChanged = new Signal<this, Contents.IChangedArgs>(this);
  1168. }
  1169. /**
  1170. * A namespace for ContentsManager statics.
  1171. */
  1172. export
  1173. namespace ContentsManager {
  1174. /**
  1175. * The options used to intialize a contents manager.
  1176. */
  1177. export
  1178. interface IOptions {
  1179. /**
  1180. * The default drive backend for the contents manager.
  1181. */
  1182. defaultDrive?: Contents.IDrive;
  1183. }
  1184. }
  1185. /**
  1186. * A namespace for Drive statics.
  1187. */
  1188. export
  1189. namespace Drive {
  1190. /**
  1191. * The options used to intialize a `Drive`.
  1192. */
  1193. export
  1194. interface IOptions {
  1195. /**
  1196. * The name for the `Drive`, which is used in file
  1197. * paths to disambiguate it from other drives.
  1198. */
  1199. name?: string;
  1200. /**
  1201. * The server settings for the server.
  1202. */
  1203. serverSettings?: ServerConnection.ISettings;
  1204. /**
  1205. * A REST endpoint for drive requests.
  1206. * If not given, defaults to the Jupyter
  1207. * REST API given by [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents).
  1208. */
  1209. apiEndpoint?: string;
  1210. }
  1211. }
  1212. /**
  1213. * A namespace for module private data.
  1214. */
  1215. namespace Private {
  1216. /**
  1217. * Normalize a file extension to be of the type `'.foo'`.
  1218. *
  1219. * Adds a leading dot if not present and converts to lower case.
  1220. */
  1221. export
  1222. function normalizeExtension(extension: string): string {
  1223. if (extension.length > 0 && extension.indexOf('.') !== 0) {
  1224. extension = `.${extension}`;
  1225. }
  1226. return extension;
  1227. }
  1228. /**
  1229. * Normalize a global path. Reduces '..' and '.' parts, and removes
  1230. * leading slashes from the local part of the path, while retaining
  1231. * the drive name if it exists.
  1232. */
  1233. export
  1234. function normalize(path: string): string {
  1235. let parts = path.split(':');
  1236. if (parts.length === 1) {
  1237. return PathExt.normalize(path);
  1238. } else if (parts.length === 2) {
  1239. return parts[0] + ':' + PathExt.normalize(parts[1]);
  1240. } else {
  1241. throw Error('Malformed path: '+path);
  1242. }
  1243. }
  1244. }