index.ts 39 KB

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