Source: managers/files.ts

  1. /**
  2. * @fileoverview Manager for the Box Files Resource
  3. */
  4. // ------------------------------------------------------------------------------
  5. // Requirements
  6. // ------------------------------------------------------------------------------
  7. import Promise from 'bluebird';
  8. import crypto from 'crypto';
  9. import httpStatusCodes from 'http-status';
  10. import { Readable, Writable } from 'stream';
  11. import urlTemplate from 'url-template';
  12. import BoxClient from '../box-client';
  13. import * as schemas from "../schemas";
  14. import errors from '../util/errors';
  15. import urlPath from '../util/url-path';
  16. const ChunkedUploader = require('../chunked-uploader');
  17. // -----------------------------------------------------------------------------
  18. // Typedefs
  19. // -----------------------------------------------------------------------------
  20. /**
  21. * Enum of valid x-rep- hint values for generating representation info
  22. *
  23. * @readonly
  24. * @enum {FileRepresentationType}
  25. */
  26. enum FileRepresentationType {
  27. PDF = '[pdf]',
  28. THUMBNAIL = '[jpg?dimensions=320x320]',
  29. IMAGE_MEDIUM = '[jpg?dimensions=1024x1024][png?dimensions=1024x1024]',
  30. IMAGE_LARGE = '[jpg?dimensions=2048x2048][png?dimensions=2048x2048]',
  31. EXTRACTED_TEXT = '[extracted_text]',
  32. }
  33. /**
  34. * @typedef {Object} UploadPart
  35. * @property {string} part_id An 8-character hexadecimal string identifying the part
  36. * @property {int} offset The byte offset of the part within the whole file
  37. * @property {int} size The size of the part in bytes
  38. */
  39. type UploadPart = {
  40. part_id: string;
  41. offset: number;
  42. size: number;
  43. };
  44. /**
  45. * Enum of valid lock types
  46. *
  47. * @readonly
  48. * @enum {LockType}
  49. */
  50. enum LockType {
  51. LOCK = 'lock',
  52. UNLOCK = 'unlock',
  53. }
  54. type FileSharedLinkAccess = 'open' | 'company' | 'collaborators' | null;
  55. type FileSharedLinkPermissions = {
  56. /**
  57. * If the shared link allows only to view files. This can only be set when access is set to open or company.
  58. */
  59. can_view?: true,
  60. /**
  61. * If the shared link allows only to download files. This can only be set when access is set to open or company.
  62. */
  63. can_download?: boolean,
  64. /**
  65. * If the shared link allows only to edit files. This can only be set when access is set to open or company.
  66. */
  67. can_edit?: boolean,
  68. }
  69. type FileSharedLink = {
  70. /**
  71. * The level of access for the shared link. This can be restricted to anyone with the link (open),
  72. * only people within the company (company) and only those who have been invited to the file (collaborators).
  73. *
  74. * If not set, this field defaults to the access level specified by the enterprise admin.
  75. * To create a shared link with this default setting pass the shared_link object with no access field.
  76. * To remove access and change its value to default one pass the shared_link object with null value access field.
  77. */
  78. access?: FileSharedLinkAccess,
  79. /**
  80. * The password required to access the shared link. Set the password to null to remove it.
  81. * A password can only be set when access is set to open.
  82. */
  83. password?: string | null,
  84. /**
  85. * The timestamp at which this shared link will expire. This field can only be set by users with paid accounts.
  86. * The value must be greater than the current date and time.
  87. * Example value: '2012-12-12T10:53:43-08:00'
  88. */
  89. unshared_at?: string | null,
  90. /**
  91. * Defines a custom vanity name to use in the shared link URL, for example vanity_name: "my-shared-link" will
  92. * produce a shared link of "https://app.box.com/v/my-shared-link".
  93. *
  94. * Custom URLs should not be used when sharing sensitive content as vanity URLs are a lot easier to guess
  95. * than regular shared links.
  96. */
  97. vanity_name?: string | null,
  98. /**
  99. * Defines what actions are allowed on a shared link.
  100. */
  101. permissions?: FileSharedLinkPermissions
  102. }
  103. // -----------------------------------------------------------------------------
  104. // Private
  105. // -----------------------------------------------------------------------------
  106. // Base path for all files endpoints
  107. const BASE_PATH = '/files',
  108. VERSIONS_SUBRESOURCE = '/versions',
  109. WATERMARK_SUBRESOURCE = '/watermark',
  110. UPLOAD_SESSION_SUBRESOURCE = '/upload_sessions',
  111. ZIP_DOWNLOAD_PATH = '/zip_downloads';
  112. /**
  113. * Returns the multipart form value for file upload metadata.
  114. * @param {string} parentFolderID - the ID of the parent folder to upload to
  115. * @param {string} filename - the file name that the uploaded file should have
  116. * @param {Object} [options] - Optional metadata
  117. * @returns {Object} - the form value expected by the API for the 'metadata' key
  118. * @private
  119. */
  120. function createFileMetadataFormData(
  121. parentFolderID: string,
  122. filename: string,
  123. options?: Record<string, any>
  124. ) {
  125. // Although the filename and parent folder ID can be specified without using a
  126. // metadata form field, Platform has recommended that we use the metadata form
  127. // field to specify these parameters (one benefit is that UTF-8 characters can
  128. // be specified in the filename).
  129. var metadata = {
  130. name: filename,
  131. parent: { id: parentFolderID },
  132. };
  133. Object.assign(metadata, options);
  134. return JSON.stringify(metadata);
  135. }
  136. /**
  137. * Returns the multipart form value for file upload content.
  138. * @param {string|Buffer|Stream} content - the content of the file being uploaded
  139. * @param {Object} options - options for the content
  140. * @returns {Object} - the form value expected by the API for the 'content' key
  141. * @private
  142. */
  143. function createFileContentFormData(
  144. content: string | Buffer | Readable,
  145. options?: Record<string, any>
  146. ) {
  147. // The upload API appears to look for a form field that contains a filename
  148. // property and assume that this form field contains the file content. Thus,
  149. // the value of name does not actually matter (as long as it does not conflict
  150. // with other field names). Similarly, the value of options.filename does not
  151. // matter either (as long as it exists), since the upload API will use the
  152. // filename specified in the metadata form field instead.
  153. return {
  154. value: content,
  155. options: Object.assign({ filename: 'unused' }, options),
  156. };
  157. }
  158. /**
  159. * Poll the representation info URL until representation is generated,
  160. * then return content URL template.
  161. * @param {BoxClient} client The client to use for making API calls
  162. * @param {string} infoURL The URL to use for getting representation info
  163. * @returns {Promise<string>} A promise resolving to the content URL template
  164. */
  165. function pollRepresentationInfo(client: BoxClient, infoURL: string) {
  166. return client.get(infoURL).then((response: any /* FIXME */) => {
  167. if (response.statusCode !== 200) {
  168. throw errors.buildUnexpectedResponseError(response);
  169. }
  170. var info = response.body;
  171. switch (info.status.state) {
  172. case 'success':
  173. case 'viewable':
  174. case 'error':
  175. return info;
  176. case 'none':
  177. case 'pending':
  178. return Promise.delay(1000).then(() =>
  179. pollRepresentationInfo(client, infoURL)
  180. );
  181. default:
  182. throw new Error(`Unknown representation status: ${info.status.state}`);
  183. }
  184. });
  185. }
  186. // ------------------------------------------------------------------------------
  187. // Public
  188. // ------------------------------------------------------------------------------
  189. /**
  190. * Simple manager for interacting with all 'File' endpoints and actions.
  191. *
  192. * @param {BoxClient} client The Box API Client that is responsible for making calls to the API
  193. * @constructor
  194. */
  195. class Files {
  196. client: BoxClient;
  197. representation!: typeof FileRepresentationType;
  198. constructor(client: BoxClient) {
  199. // Attach the client, for making API calls
  200. this.client = client;
  201. }
  202. /**
  203. * Requests a file object with the given ID.
  204. *
  205. * API Endpoint: '/files/:fileID'
  206. * Method: GET
  207. *
  208. * @param {string} fileID - Box ID of the file being requested
  209. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  210. * @param {Function} [callback] - Passed the file information if it was acquired successfully
  211. * @returns {Promise<Object>} A promise resolving to the file object
  212. */
  213. get(fileID: string, options?: Record<string, any>, callback?: Function) {
  214. var params = {
  215. qs: options,
  216. };
  217. var apiPath = urlPath(BASE_PATH, fileID);
  218. return this.client.wrapWithDefaultHandler(this.client.get)(
  219. apiPath,
  220. params,
  221. callback
  222. );
  223. }
  224. /**
  225. * Requests a download URL for a given file.
  226. *
  227. * API Endpoint: '/files/:fileID/content'
  228. * Method: GET
  229. * Special Expected Responses:
  230. * 202 ACCEPTED - Download isn't available yet. Returns an error.
  231. * 302 FOUND - Download is available. A Download URL is returned.
  232. *
  233. * @param {string} fileID - Box ID of the file being requested
  234. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  235. * @param {Function} [callback] - Passed the download URL if request was successful.
  236. * @returns {Promise<string>} A promise resolving to the file's download URL
  237. */
  238. getDownloadURL(
  239. fileID: string,
  240. options?: Record<string, any>,
  241. callback?: Function
  242. ) {
  243. var params = {
  244. qs: options,
  245. };
  246. var apiPath = urlPath(BASE_PATH, fileID, '/content');
  247. // Handle Special API Response
  248. return this.client
  249. .get(apiPath, params)
  250. .then((response: any /* FIXME */) => {
  251. switch (response.statusCode) {
  252. // 302 - Found
  253. // No data returned, but the location header points to a download link for that file.
  254. case httpStatusCodes.FOUND:
  255. return response.headers.location;
  256. // 202 - Download isn't ready yet.
  257. case httpStatusCodes.ACCEPTED:
  258. throw errors.buildResponseError(
  259. response,
  260. 'Download not ready at this time'
  261. );
  262. // Unexpected Response
  263. default:
  264. throw errors.buildUnexpectedResponseError(response);
  265. }
  266. })
  267. .asCallback(callback);
  268. }
  269. /**
  270. * Requests a Readable Stream for the given file ID.
  271. *
  272. * API Endpoint: '/files/:fileID/content'
  273. * Method: GET
  274. * Special Expected Responses:
  275. * 202 ACCEPTED - Download isn't available yet. Returns an error.
  276. * 302 FOUND - Download is available. A Download stream is returned.
  277. *
  278. * @param {string} fileID - Box ID of the file being requested
  279. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  280. * @param {string} [options.version] - ID of the version of this file to download
  281. * @param {int[]} [options.byteRange] - starting and ending bytes of the file to read, e.g. [0, 99] to read the first 100 bytes
  282. * @param {Function} [callback] - passed the readable stream if request was successful
  283. * @returns {Promise<Readable>} A promise resolving for the file stream
  284. */
  285. getReadStream(
  286. fileID: string,
  287. options?: {
  288. version?: string;
  289. byteRange?: number[];
  290. },
  291. callback?: Function
  292. ) {
  293. options = options || {};
  294. var downloadStreamOptions = {
  295. streaming: true,
  296. headers: {} as Record<string, any>,
  297. };
  298. if (options.byteRange) {
  299. var range = options.byteRange;
  300. delete options.byteRange;
  301. downloadStreamOptions.headers.Range = `bytes=${range[0]}-${range[1]}`;
  302. }
  303. // Get the download URL to download from
  304. return (
  305. this.getDownloadURL(fileID, options)
  306. // Return a read stream to download the file
  307. .then((url: string) => this.client.get(url, downloadStreamOptions))
  308. .asCallback(callback)
  309. );
  310. }
  311. /**
  312. * Gets the comments on a file.
  313. *
  314. * API Endpoint: '/files/:fileID/comments'
  315. * Method: GET
  316. *
  317. * @param {string} fileID - Box file id of the file
  318. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  319. * @param {Function} [callback] - passed the file comments if they were successfully acquired
  320. * @returns {Promise<Object>} A promise resolving to the collection of comments
  321. */
  322. getComments(
  323. fileID: string,
  324. options?: Record<string, any>,
  325. callback?: Function
  326. ) {
  327. var params = {
  328. qs: options,
  329. };
  330. var apiPath = urlPath(BASE_PATH, fileID, '/comments');
  331. return this.client.wrapWithDefaultHandler(this.client.get)(
  332. apiPath,
  333. params,
  334. callback
  335. );
  336. }
  337. /**
  338. * Update some information about a given file.
  339. *
  340. * API Endpoint: '/files/:fileID'
  341. * Method: PUT
  342. *
  343. * @param {string} fileID - Box ID of the file being requested
  344. * @param {Object} updates - File fields to update
  345. * @param {string} [updates.etag] Only apply the updates if the file etag matches
  346. * @param {string} [updates.fields] Comma-separated list of fields to return
  347. * @param {Function} [callback] - Passed the updated file information if it was acquired successfully
  348. * @returns {Promise<Object>} A promise resolving to the update file object
  349. */
  350. update(
  351. fileID: string,
  352. updates: {
  353. [key: string]: any;
  354. etag?: string;
  355. shared_link?: FileSharedLink;
  356. fields?: string;
  357. },
  358. callback?: Function
  359. ) {
  360. var params: Record<string, any> = {
  361. body: updates,
  362. };
  363. if (updates && updates.etag) {
  364. params.headers = {
  365. 'If-Match': updates.etag,
  366. };
  367. delete updates.etag;
  368. }
  369. if (updates && updates.fields) {
  370. params.qs = {
  371. fields: updates.fields,
  372. };
  373. delete updates.fields;
  374. }
  375. var apiPath = urlPath(BASE_PATH, fileID);
  376. return this.client.wrapWithDefaultHandler(this.client.put)(
  377. apiPath,
  378. params,
  379. callback
  380. );
  381. }
  382. /**
  383. * Add a file to a given collection
  384. *
  385. * API Endpoint: '/files/:fileID'
  386. * Method: PUT
  387. *
  388. * @param {string} fileID - The file to add to the collection
  389. * @param {string} collectionID - The collection to add the file to
  390. * @param {Function} [callback] - Passed the updated file if successful, error otherwise
  391. * @returns {Promise<Object>} A promise resolving to the updated file object
  392. */
  393. addToCollection(fileID: string, collectionID: string, callback?: Function) {
  394. return this.get(fileID, { fields: 'collections' })
  395. .then((data: Record<string, any>) => {
  396. var collections = data.collections || [];
  397. // Convert to correct format
  398. collections = collections.map((c: any /* FIXME */) => ({ id: c.id }));
  399. if (!collections.find((c: any /* FIXME */) => c.id === collectionID)) {
  400. collections.push({ id: collectionID });
  401. }
  402. return this.update(fileID, { collections });
  403. })
  404. .asCallback(callback);
  405. }
  406. /**
  407. * Remove a file from a given collection
  408. *
  409. * API Endpoint: '/files/:fileID'
  410. * Method: PUT
  411. *
  412. * @param {string} fileID - The file to remove from the collection
  413. * @param {string} collectionID - The collection to remove the file from
  414. * @param {Function} [callback] - Passed the updated file if successful, error otherwise
  415. * @returns {Promise<Object>} A promise resolving to the updated file object
  416. */
  417. removeFromCollection(
  418. fileID: string,
  419. collectionID: string,
  420. callback?: Function
  421. ) {
  422. return this.get(fileID, { fields: 'collections' })
  423. .then((data: any /* FIXME */) => {
  424. var collections = data.collections || [];
  425. // Convert to correct object format and remove the specified collection
  426. collections = collections
  427. .map((c: any /* FIXME */) => ({ id: c.id }))
  428. .filter((c: any /* FIXME */) => c.id !== collectionID);
  429. return this.update(fileID, { collections });
  430. })
  431. .asCallback(callback);
  432. }
  433. /**
  434. * Move a file into a new parent folder.
  435. *
  436. * API Endpoint: '/files/:fileID'
  437. * Method: PUT
  438. *
  439. * @param {string} fileID - The Box ID of the file being requested
  440. * @param {string} newParentID - The Box ID for the new parent folder. '0' to move to All Files.
  441. * @param {Function} [callback] - Passed the updated file information if it was acquired successfully
  442. * @returns {Promise<Object>} A promise resolving to the updated file object
  443. */
  444. move(fileID: string, newParentID: string, callback?: Function) {
  445. var params = {
  446. body: {
  447. parent: {
  448. id: newParentID,
  449. },
  450. },
  451. };
  452. var apiPath = urlPath(BASE_PATH, fileID);
  453. return this.client.wrapWithDefaultHandler(this.client.put)(
  454. apiPath,
  455. params,
  456. callback
  457. );
  458. }
  459. /**
  460. * Copy a file into a new folder.
  461. *
  462. * API Endpoint: '/files/:fileID/copy
  463. * Method: POST
  464. *
  465. * @param {string} fileID - The Box ID of the file being requested
  466. * @param {string} newParentID - The Box ID for the new parent folder. '0' to copy to All Files.
  467. * @param {Object} [options] - Optional parameters for the copy operation, can be left null in most cases
  468. * @param {string} [options.name] - A new name to use if there is an identically-named item in the new parent folder
  469. * @param {string} [options.version] - An optional ID of the specific file version to copy
  470. * @param {Function} [callback] - passed the new file info if call was successful
  471. * @returns {Promise<Object>} A promise resolving to the new file object
  472. */
  473. copy(
  474. fileID: string,
  475. newParentID: string,
  476. options?:
  477. | {
  478. name?: string;
  479. version?: string;
  480. }
  481. | Function,
  482. callback?: Function
  483. ) {
  484. // @NOTE(mwiller) 2016-10-25: Shuffle arguments to maintain backward compatibility
  485. // This can be removed at the v2.0 update
  486. if (typeof options === 'function') {
  487. callback = options;
  488. options = {};
  489. }
  490. options = options || {};
  491. (options as Record<string, any>).parent = {
  492. id: newParentID,
  493. };
  494. var params = {
  495. body: options,
  496. };
  497. var apiPath = urlPath(BASE_PATH, fileID, '/copy');
  498. return this.client.wrapWithDefaultHandler(this.client.post)(
  499. apiPath,
  500. params,
  501. callback
  502. );
  503. }
  504. /**
  505. * Delete a given file.
  506. *
  507. * API Endpoint: '/files/:fileID'
  508. * Method: DELETE
  509. *
  510. * @param {string} fileID - Box ID of the file being requested
  511. * @param {Object} [options] Optional parameters
  512. * @param {string} [options.etag] Only delete the file if the etag value matches
  513. * @param {Function} [callback] - Empty response body passed if successful.
  514. * @returns {Promise<void>} A promise resolving to nothing
  515. */
  516. delete(
  517. fileID: string,
  518. options?:
  519. | {
  520. [key: string]: any;
  521. etag?: string;
  522. }
  523. | Function,
  524. callback?: Function
  525. ) {
  526. // Switch around arguments if necessary for backwards compatibility
  527. if (typeof options === 'function') {
  528. callback = options;
  529. options = {};
  530. }
  531. var params: Record<string, any> = {};
  532. if (options && options.etag) {
  533. params.headers = {
  534. 'If-Match': options.etag,
  535. };
  536. }
  537. var apiPath = urlPath(BASE_PATH, fileID);
  538. return this.client.wrapWithDefaultHandler(this.client.del)(
  539. apiPath,
  540. params,
  541. callback
  542. );
  543. }
  544. /**
  545. * Get preflight information for a new file upload. Without any file data,
  546. * this will return an upload URL and token to be used when uploading the file.
  547. * Using this upload URL will allow for the fastest upload, and the one-time
  548. * token can be passed to a worker or other client to actually perform the
  549. * upload with. If file data (e.g. size, parent, name) is passed, it will be
  550. * validated as if the actual file were being uploaded. This enables checking
  551. * of preconditions such as name uniqueness and available storage space before
  552. * attempting a large file upload.
  553. *
  554. * API Endpoint: '/files/content'
  555. * Method: OPTIONS
  556. *
  557. * @param {string} parentFolderID - The id of the parent folder to upload to
  558. * @param {Object} [fileData] - Optional data about the file to be uploaded
  559. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  560. * @param {Function} [callback] - Called with upload data if successful, or err if the upload would not succeed
  561. * @returns {Promise<Object>} A promise resolving to the upload data
  562. */
  563. preflightUploadFile(
  564. parentFolderID: string,
  565. fileData?: Record<string, any>,
  566. options?: Record<string, any>,
  567. callback?: Function
  568. ) {
  569. var params = {
  570. body: {
  571. parent: {
  572. id: parentFolderID,
  573. },
  574. },
  575. qs: options,
  576. };
  577. if (fileData) {
  578. Object.assign(params.body, fileData);
  579. }
  580. var apiPath = urlPath(BASE_PATH, '/content');
  581. return this.client.wrapWithDefaultHandler(this.client.options)(
  582. apiPath,
  583. params,
  584. callback
  585. );
  586. }
  587. /**
  588. * Get preflight information for a file version upload. Without any file data,
  589. * this will return an upload URL and token to be used when uploading the file.
  590. * Using this upload URL will allow for the fastest upload, and the one-time
  591. * token can be passed to a worker or other client to actually perform the
  592. * upload with. If file data (e.g. size, parent, name) is passed, it will be
  593. * validated as if the actual file were being uploaded. This enables checking
  594. * of preconditions such as name uniqueness and available storage space before
  595. * attempting a large file upload.
  596. *
  597. * API Endpoint: '/files/:fileID/content'
  598. * Method: OPTIONS
  599. *
  600. * @param {string} fileID - The file ID to which a new version will be uploaded
  601. * @param {Object} [fileData] - Optional data about the file to be uploaded
  602. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  603. * @param {Function} [callback] - Called with upload data if successful, or err if the upload would not succeed
  604. * @returns {Promise<Object>} A promise resolving to the upload data
  605. */
  606. preflightUploadNewFileVersion(
  607. fileID: string,
  608. fileData?: Record<string, any>,
  609. options?: Record<string, any>,
  610. callback?: Function
  611. ) {
  612. var params: Record<string, any> = {
  613. qs: options,
  614. };
  615. if (fileData) {
  616. params.body = fileData;
  617. }
  618. var apiPath = urlPath(BASE_PATH, fileID, '/content');
  619. return this.client.wrapWithDefaultHandler(this.client.options)(
  620. apiPath,
  621. params,
  622. callback
  623. );
  624. }
  625. /**
  626. * If there are previous versions of this file, this method can be used to promote one of the older
  627. * versions to the top of the stack. This actually mints a copy of the old version and puts it on
  628. * the top of the versions stack. The file will have the exact same contents, the same SHA1/etag,
  629. * and the same name as the original. Other properties such as comments do not get updated to their former values.
  630. *
  631. * API Endpoint: '/files/:fileID/versions/current'
  632. * Method: POST
  633. *
  634. * @param {string} fileID - The file ID which version will be promoted
  635. * @param {string} versionID - The ID of the file_version that you want to make current
  636. * @param {Function} [callback] - Passed the promoted file version information if successful, error otherwise
  637. * @returns {Promise<Object>} A promise resolving to the promoted file version
  638. */
  639. promoteVersion(fileID: string, versionID: string, callback?: Function) {
  640. var apiPath = urlPath(BASE_PATH, fileID, VERSIONS_SUBRESOURCE, '/current'),
  641. params = {
  642. body: {
  643. type: 'file_version',
  644. id: versionID,
  645. },
  646. };
  647. return this.client.wrapWithDefaultHandler(this.client.post)(
  648. apiPath,
  649. params,
  650. callback
  651. );
  652. }
  653. /**
  654. * Uploads a new file. Unlike non-upload methods, this method will not perform any retries.
  655. * This method currently does not support any optional parameters such as contentModifiedAt.
  656. *
  657. * API Endpoint: '/files/content'
  658. * Method: POST
  659. *
  660. * @param {string} parentFolderID - the id of the parent folder to upload to
  661. * @param {string} filename - the file name that the uploaded file should have
  662. * @param {string|Buffer|ReadStream} content - the content of the file. It can be a string, a Buffer, or a read stream
  663. * (like that returned by fs.createReadStream()).
  664. * @param {Object} [options] - Optional parameters
  665. * @param {string} [options.content_created_at] - RFC 3339 timestamp when the file was created
  666. * @param {string} [options.content_modified_at] - RFC 3339 timestamp when the file was last modified
  667. * @param {int} [options.content_length] - Optional length of the content. Required if content is a read stream of any type other than fs stream.
  668. * @param {string} [options.description] - Optional description of the uploaded file.
  669. * @param {Function} [callback] - called with data about the upload if successful, or an error if the
  670. * upload failed
  671. * @returns {Promise<Object>} A promise resolving to the uploaded file
  672. */
  673. uploadFile(
  674. parentFolderID: string,
  675. filename: string,
  676. content: string | Buffer | Readable,
  677. options?:
  678. | {
  679. content_created_at?: string;
  680. content_modified_at?: string;
  681. content_length?: number;
  682. description?: string;
  683. }
  684. | Function,
  685. callback?: Function
  686. ) {
  687. // Shuffle around optional parameter
  688. if (typeof options === 'function') {
  689. callback = options;
  690. options = {};
  691. }
  692. var formOptions: Record<string, any> = {};
  693. if (options && options.hasOwnProperty('content_length')) {
  694. formOptions.knownLength = options.content_length;
  695. // Delete content_length from options so it's not added to the attributes of the form
  696. delete options.content_length;
  697. }
  698. var apiPath = urlPath(BASE_PATH, '/content'),
  699. multipartFormData = {
  700. attributes: createFileMetadataFormData(
  701. parentFolderID,
  702. filename,
  703. options
  704. ),
  705. content: createFileContentFormData(content, formOptions),
  706. };
  707. return this.client.wrapWithDefaultHandler(this.client.upload)(
  708. apiPath,
  709. null,
  710. multipartFormData,
  711. callback
  712. );
  713. }
  714. /**
  715. * Uploads a new version of a file. Unlike non-upload methods, this method will not perform any retries.
  716. * This method currently does not support any optional parameters such as contentModifiedAt.
  717. *
  718. * API Endpoint: '/files/:fileID/content'
  719. * Method: POST
  720. *
  721. * @param {string} fileID - the id of the file to upload a new version of
  722. * @param {string|Buffer|Stream} content - the content of the file. It can be a string, a Buffer, or a read stream
  723. * (like that returned by fs.createReadStream()).
  724. * @param {Object} [options] - Optional parameters
  725. * @param {string} [options.content_modified_at] - RFC 3339 timestamp when the file was last modified
  726. * @param {string} [options.name] - A new name for the file
  727. * @param {int} [options.content_length] - Optional length of the content. Required if content is a read stream of any type other than fs stream.
  728. * @param {string} [options.description] - Optional description of the uploaded new file version.
  729. * @param {Function} [callback] - called with data about the upload if successful, or an error if the
  730. * upload failed
  731. * @returns {Promise<Object>} A promise resolving to the uploaded file
  732. */
  733. uploadNewFileVersion(
  734. fileID: string,
  735. content: string | Buffer | Readable,
  736. options?:
  737. | {
  738. content_modified_at?: string;
  739. name?: string;
  740. content_length?: number;
  741. description?: string;
  742. }
  743. | Function,
  744. callback?: Function
  745. ) {
  746. // Shuffle around optional parameter
  747. if (typeof options === 'function') {
  748. callback = options;
  749. options = {};
  750. }
  751. var apiPath = urlPath(BASE_PATH, fileID, '/content'),
  752. multipartFormData: Record<string, any> = {};
  753. var formOptions: Record<string, any> = {};
  754. if (options) {
  755. if (options.hasOwnProperty('content_length')) {
  756. formOptions.knownLength = options.content_length;
  757. // Delete content_length from options so it's not added to the attributes of the form
  758. delete options.content_length;
  759. }
  760. multipartFormData.attributes = JSON.stringify(options);
  761. }
  762. multipartFormData.content = createFileContentFormData(content, formOptions);
  763. return this.client.wrapWithDefaultHandler(this.client.upload)(
  764. apiPath,
  765. null,
  766. multipartFormData,
  767. callback
  768. );
  769. }
  770. /**
  771. * Retrieves all metadata associated with a file.
  772. *
  773. * API Endpoint: '/files/:fileID/metadata'
  774. * Method: GET
  775. *
  776. * @param {string} fileID - the ID of the file to get metadata for
  777. * @param {Function} [callback] - called with an array of metadata when successful
  778. * @returns {Promise<Object>} A promise resolving to a collection of metadata on the file
  779. */
  780. getAllMetadata(fileID: string, callback?: Function) {
  781. var apiPath = urlPath(BASE_PATH, fileID, 'metadata');
  782. return this.client.wrapWithDefaultHandler(this.client.get)(
  783. apiPath,
  784. null,
  785. callback
  786. );
  787. }
  788. /**
  789. * Retrieve a single metadata template instance for a file.
  790. *
  791. * API Endpoint: '/files/:fileID/metadata/:scope/:template'
  792. * Method: GET
  793. *
  794. * @param {string} fileID - The ID of the file to retrive the metadata of
  795. * @param {string} scope - The scope of the metadata template, e.g. "global"
  796. * @param {string} template - The metadata template to retrieve
  797. * @param {Function} [callback] - Passed the metadata template if successful
  798. * @returns {Promise<Object>} A promise resolving to the metadata template
  799. */
  800. getMetadata(
  801. fileID: string,
  802. scope: string,
  803. template: string,
  804. callback?: Function
  805. ) {
  806. var apiPath = urlPath(BASE_PATH, fileID, 'metadata', scope, template);
  807. return this.client.wrapWithDefaultHandler(this.client.get)(
  808. apiPath,
  809. null,
  810. callback
  811. );
  812. }
  813. /**
  814. * Adds metadata to a file. Metadata must either match a template schema or
  815. * be placed into the unstructured "properties" template in global scope.
  816. *
  817. * API Endpoint: '/files/:fileID/metadata/:scope/:template'
  818. * Method: POST
  819. *
  820. * @param {string} fileID - The ID of the file to add metadata to
  821. * @param {string} scope - The scope of the metadata template, e.g. "enterprise"
  822. * @param {string} template - The metadata template schema to add
  823. * @param {Object} data - Key/value pairs tp add as metadata
  824. * @param {Function} [callback] - Called with error if unsuccessful
  825. * @returns {Promise<Object>} A promise resolving to the new metadata
  826. */
  827. addMetadata(
  828. fileID: string,
  829. scope: string,
  830. template: string,
  831. data: Record<string, any>,
  832. callback?: Function
  833. ) {
  834. var apiPath = urlPath(BASE_PATH, fileID, 'metadata', scope, template),
  835. params = {
  836. body: data,
  837. };
  838. return this.client.wrapWithDefaultHandler(this.client.post)(
  839. apiPath,
  840. params,
  841. callback
  842. );
  843. }
  844. /**
  845. * Updates a metadata template instance with JSON Patch-formatted data.
  846. *
  847. * API Endpoint: '/files/:fileID/metadata/:scope/:template'
  848. * Method: PUT
  849. *
  850. * @param {string} fileID - The file to update metadata for
  851. * @param {string} scope - The scope of the template to update
  852. * @param {string} template - The template to update
  853. * @param {Object} patch - The patch data
  854. * @param {Function} [callback] - Called with updated metadata if successful
  855. * @returns {Promise<Object>} A promise resolving to the updated metadata
  856. */
  857. updateMetadata(
  858. fileID: string,
  859. scope: string,
  860. template: string,
  861. patch: Record<string, any>,
  862. callback?: Function
  863. ) {
  864. var apiPath = urlPath(BASE_PATH, fileID, 'metadata', scope, template),
  865. params = {
  866. body: patch,
  867. headers: {
  868. 'Content-Type': 'application/json-patch+json',
  869. },
  870. };
  871. return this.client.wrapWithDefaultHandler(this.client.put)(
  872. apiPath,
  873. params,
  874. callback
  875. );
  876. }
  877. /**
  878. * Sets metadata on a file, overwriting any metadata that exists for the provided keys.
  879. *
  880. * @param {string} fileID - The file to set metadata on
  881. * @param {string} scope - The scope of the metadata template
  882. * @param {string} template - The key of the metadata template
  883. * @param {Object} metadata - The metadata to set
  884. * @param {Function} [callback] - Called with updated metadata if successful
  885. * @returns {Promise<Object>} A promise resolving to the updated metadata
  886. */
  887. setMetadata(
  888. fileID: string,
  889. scope: string,
  890. template: string,
  891. metadata: Record<string, any>,
  892. callback?: Function
  893. ) {
  894. return this.addMetadata(fileID, scope, template, metadata)
  895. .catch((err: any /* FIXME */) => {
  896. if (err.statusCode !== 409) {
  897. throw err;
  898. }
  899. // Metadata already exists on the file; update instead
  900. var updates = Object.keys(metadata).map((key) => ({
  901. op: 'add',
  902. path: `/${key}`,
  903. value: metadata[key],
  904. }));
  905. return this.updateMetadata(fileID, scope, template, updates);
  906. })
  907. .asCallback(callback);
  908. }
  909. /**
  910. * Deletes a metadata template from a file.
  911. *
  912. * API Endpoint: '/files/:fileID/metadata/:scope/:template'
  913. * Method: DELETE
  914. *
  915. * @param {string} fileID - The ID of the file to remove metadata from
  916. * @param {string} scope - The scope of the metadata template
  917. * @param {string} template - The template to remove from the file
  918. * @param {Function} [callback] - Called with nothing if successful, error otherwise
  919. * @returns {Promise<void>} A promise resolving to nothing
  920. */
  921. deleteMetadata(
  922. fileID: string,
  923. scope: string,
  924. template: string,
  925. callback?: Function
  926. ) {
  927. var apiPath = urlPath(BASE_PATH, fileID, 'metadata', scope, template);
  928. return this.client.wrapWithDefaultHandler(this.client.del)(
  929. apiPath,
  930. null,
  931. callback
  932. );
  933. }
  934. /**
  935. * Permanently deletes an item that is in the trash. The item will no longer exist in Box. This action cannot be undone.
  936. *
  937. * API Endpoint: '/files/:fileID/trash'
  938. * Method: DELETE
  939. *
  940. * @param {string} fileID - The ID of the file to remove metadata from
  941. * @param {Object} [options] Optional parameters
  942. * @param {string} [options.etag] Only delete the file if the etag matches
  943. * @param {Function} [callback] - Called with nothing if successful, error otherwise
  944. * @returns {Promise<void>} A promise resolving to nothing
  945. */
  946. deletePermanently(
  947. fileID: string,
  948. options?:
  949. | {
  950. [key: string]: any;
  951. etag?: string;
  952. }
  953. | Function,
  954. callback?: Function
  955. ) {
  956. if (typeof options === 'function') {
  957. callback = options;
  958. options = {};
  959. }
  960. var params: Record<string, any> = {};
  961. if (options && options.etag) {
  962. params.headers = {
  963. 'If-Match': options.etag,
  964. };
  965. }
  966. var apiPath = urlPath(BASE_PATH, fileID, '/trash');
  967. return this.client.wrapWithDefaultHandler(this.client.del)(
  968. apiPath,
  969. params,
  970. callback
  971. );
  972. }
  973. /**
  974. * Retrieves a file that has been moved to the trash.
  975. *
  976. * API Endpoint: '/files/:fileID/trash'
  977. * Method: GET
  978. *
  979. * @param {string} fileID - The ID of the file being requested
  980. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  981. * @param {Function} [callback] - Passed the trashed file information if successful, error otherwise
  982. * @returns {Promise<Object>} A promise resolving to the trashed file
  983. */
  984. getTrashedFile(
  985. fileID: string,
  986. options?: Record<string, any>,
  987. callback?: Function
  988. ) {
  989. var params = {
  990. qs: options,
  991. };
  992. var apiPath = urlPath(BASE_PATH, fileID, 'trash');
  993. return this.client.wrapWithDefaultHandler(this.client.get)(
  994. apiPath,
  995. params,
  996. callback
  997. );
  998. }
  999. /**
  1000. * Retrieves all of the tasks for given file.
  1001. *
  1002. * API Endpoint: '/files/:fileID/tasks'
  1003. * Method: GET
  1004. *
  1005. * @param {string} fileID - The ID of the file to get tasks for
  1006. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  1007. * @param {Function} [callback] - Passed the file tasks if successful, error otherwise
  1008. * @returns {Promise<Object>} A promise resolving to a collections of tasks on the file
  1009. */
  1010. getTasks(fileID: string, options?: Record<string, any>, callback?: Function) {
  1011. var params = {
  1012. qs: options,
  1013. };
  1014. var apiPath = urlPath(BASE_PATH, fileID, '/tasks');
  1015. return this.client.wrapWithDefaultHandler(this.client.get)(
  1016. apiPath,
  1017. params,
  1018. callback
  1019. );
  1020. }
  1021. /**
  1022. * Used to retrieve an expiring URL for creating an embedded preview session.
  1023. * The URL will expire after 60 seconds and the preview session will expire after 60 minutes.
  1024. *
  1025. * API Endpoint: '/files/:fileID?fields=expiring_embed_link'
  1026. * Method: GET
  1027. *
  1028. * @param {string} fileID - The ID of the file to generate embed link for
  1029. * @param {Function} [callback] - Passed with the embed link if successful, error otherwise
  1030. * @returns {Promise<string>} A promise resolving to the file embed link URL
  1031. */
  1032. getEmbedLink(fileID: string, callback?: Function) {
  1033. var params = {
  1034. qs: {
  1035. fields: 'expiring_embed_link',
  1036. },
  1037. };
  1038. var apiPath = urlPath(BASE_PATH, fileID);
  1039. return this.client
  1040. .get(apiPath, params)
  1041. .then((response: any /* FIXME */) => {
  1042. if (response.statusCode !== httpStatusCodes.OK) {
  1043. throw errors.buildUnexpectedResponseError(response);
  1044. }
  1045. return response.body.expiring_embed_link.url;
  1046. })
  1047. .asCallback(callback);
  1048. }
  1049. /**
  1050. * Locks a file.
  1051. *
  1052. * API Endpoint: '/files/:fileID'
  1053. * Method: PUT
  1054. *
  1055. * @param {string} fileID - The ID of the file to lock
  1056. * @param {Object} [options] - Optional parameters, can be left null in most cases
  1057. * @param {?string} [options.expires_at] - The time the lock expires
  1058. * @param {boolean} [options.is_download_prevented] - Whether or not the file can be downloaded while locked
  1059. * @param {Function} [callback] - Passed with the locked file information if successful, error otherwise
  1060. * @returns {Promise<Object>} A promise resolving to the locked file object
  1061. */
  1062. lock(
  1063. fileID: string,
  1064. options?: {
  1065. expires_at?: string;
  1066. is_download_prevented?: boolean;
  1067. },
  1068. callback?: Function
  1069. ) {
  1070. var apiPath = urlPath(BASE_PATH, fileID),
  1071. params = {
  1072. body: {
  1073. lock: {
  1074. type: LockType.LOCK,
  1075. },
  1076. },
  1077. };
  1078. Object.assign(params.body.lock, options);
  1079. return this.client.wrapWithDefaultHandler(this.client.put)(
  1080. apiPath,
  1081. params,
  1082. callback
  1083. );
  1084. }
  1085. /**
  1086. * Unlocks a file.
  1087. *
  1088. * API Endpoint: '/files/:fileID'
  1089. * Method: PUT
  1090. *
  1091. * @param {string} fileID - The ID of the file to unlock
  1092. * @param {Function} [callback] - Passed with the unlocked file information if successful, error otherwise
  1093. * @returns {Promise<Object>} A promise resolving to the unlocked file object
  1094. */
  1095. unlock(fileID: string, callback?: Function) {
  1096. var apiPath = urlPath(BASE_PATH, fileID),
  1097. params = {
  1098. body: {
  1099. lock: null,
  1100. },
  1101. };
  1102. return this.client.wrapWithDefaultHandler(this.client.put)(
  1103. apiPath,
  1104. params,
  1105. callback
  1106. );
  1107. }
  1108. /**
  1109. * Restores an item that has been moved to the trash. Default behavior is to
  1110. * restore the item to the folder it was in before it was moved to the trash.
  1111. * If that parent folder no longer exists or if there is now an item with the
  1112. * same name in that parent folder, the new parent folder and/or new name will
  1113. * need to be included in the request.
  1114. *
  1115. * API Endpoint: '/files/:fileID'
  1116. * Method: POST
  1117. *
  1118. * @param {string} fileID - The ID of the file to restore
  1119. * @param {Object} [options] - Optional parameters, can be left null in most cases
  1120. * @param {string} [options.name] - The new name for this item
  1121. * @param {string} [options.parent_id] - The new parent folder for this item
  1122. * @param {Function} [callback] - Called with item information if successful, error otherwise
  1123. * @returns {Promise<Object>} A promise resolving to the restored file object
  1124. */
  1125. restoreFromTrash(
  1126. fileID: string,
  1127. options?: {
  1128. name?: string;
  1129. parent_id?: string;
  1130. },
  1131. callback?: Function
  1132. ) {
  1133. // Set up the parent_id parameter
  1134. if (options && options.parent_id) {
  1135. (options as Record<string, any>).parent = {
  1136. id: options.parent_id,
  1137. };
  1138. delete options.parent_id;
  1139. }
  1140. var apiPath = urlPath(BASE_PATH, fileID),
  1141. params = {
  1142. body: options || {},
  1143. };
  1144. return this.client.wrapWithDefaultHandler(this.client.post)(
  1145. apiPath,
  1146. params,
  1147. callback
  1148. );
  1149. }
  1150. /**
  1151. * If there are previous versions of this file, this method can be used to retrieve information
  1152. * about the older versions.
  1153. *
  1154. * API Endpoint: '/files/:fileID/versions'
  1155. * Method: GET
  1156. *
  1157. * @param {string} fileID - The ID of the file to view version for
  1158. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  1159. * @param {Function} [callback] - Passed a list of previous file versions if successful, error otherwise
  1160. * @returns {Promise<Object>} A promise resolving to the collection of file versions
  1161. */
  1162. getVersions(
  1163. fileID: string,
  1164. options?: Record<string, any>,
  1165. callback?: Function
  1166. ) {
  1167. var apiPath = urlPath(BASE_PATH, fileID, VERSIONS_SUBRESOURCE),
  1168. params = {
  1169. qs: options,
  1170. };
  1171. return this.client.wrapWithDefaultHandler(this.client.get)(
  1172. apiPath,
  1173. params,
  1174. callback
  1175. );
  1176. }
  1177. /**
  1178. * Used to retrieve the watermark for a corresponding Box file.
  1179. *
  1180. * API Endpoint: '/files/:fileID/watermark'
  1181. * Method: GET
  1182. *
  1183. * @param {string} fileID - The Box ID of the file to get watermark for
  1184. * @param {Object} [options] - Additional options for the request. Can be left null in most cases.
  1185. * @param {Function} [callback] - Passed the watermark information if successful, error otherwise
  1186. * @returns {Promise<Object>} A promise resolving to the watermark info
  1187. */
  1188. getWatermark(
  1189. fileID: string,
  1190. options?: Record<string, any>,
  1191. callback?: Function
  1192. ) {
  1193. var apiPath = urlPath(BASE_PATH, fileID, WATERMARK_SUBRESOURCE),
  1194. params = {
  1195. qs: options,
  1196. };
  1197. return this.client
  1198. .get(apiPath, params)
  1199. .then((response: any /* FIXME */) => {
  1200. if (response.statusCode !== 200) {
  1201. throw errors.buildUnexpectedResponseError(response);
  1202. }
  1203. return response.body.watermark;
  1204. })
  1205. .asCallback(callback);
  1206. }
  1207. /**
  1208. * Used to apply or update the watermark for a corresponding Box file.
  1209. *
  1210. * API Endpoint: '/files/:fileID/watermark'
  1211. * Method: PUT
  1212. *
  1213. * @param {string} fileID - The Box ID of the file to update watermark for
  1214. * @param {Object} [options] - Optional parameters, can be left null
  1215. * @param {Function} [callback] - Passed the watermark information if successful, error otherwise
  1216. * @returns {Promise<Object>} A promise resolving to the watermark info
  1217. */
  1218. applyWatermark(
  1219. fileID: string,
  1220. options?: Record<string, any>,
  1221. callback?: Function
  1222. ) {
  1223. var apiPath = urlPath(BASE_PATH, fileID, WATERMARK_SUBRESOURCE),
  1224. params = {
  1225. body: {
  1226. watermark: {
  1227. imprint: 'default', // Currently the API only supports default imprint
  1228. },
  1229. },
  1230. };
  1231. Object.assign(params.body.watermark, options);
  1232. return this.client.wrapWithDefaultHandler(this.client.put)(
  1233. apiPath,
  1234. params,
  1235. callback
  1236. );
  1237. }
  1238. /**
  1239. * Used to remove the watermark for a corresponding Box file.
  1240. *
  1241. * API Endpoint: '/files/:fileID/watermark'
  1242. * Method: DELETE
  1243. *
  1244. * @param {string} fileID - The Box ID of the file to remove watermark from
  1245. * @param {Function} [callback] - Empty response body passed if successful, error otherwise
  1246. * @returns {Promise<void>} A promise resolving to nothing
  1247. */
  1248. removeWatermark(fileID: string, callback: Function) {
  1249. var apiPath = urlPath(BASE_PATH, fileID, WATERMARK_SUBRESOURCE);
  1250. return this.client.wrapWithDefaultHandler(this.client.del)(
  1251. apiPath,
  1252. null,
  1253. callback
  1254. );
  1255. }
  1256. /**
  1257. * Discards a specific file version to the trash. Depending on the enterprise settings
  1258. * for this user, the item will either be actually deleted from Box or moved to the trash.
  1259. *
  1260. * API Endpoint: '/files/:fileID/version/:versionID'
  1261. * Method: DELETE
  1262. *
  1263. * @param {string} fileID - The file ID which old version will be moved to the trash or delete permanently
  1264. * @param {string} versionID - The ID of the version to move to the trash or delete permanently
  1265. * @param {Object} [options] Optional parameters
  1266. * @param {string} [options.etag] Only delete the version of the file etag matches
  1267. * @param {Function} [callback] - Empty response body, error otherwise
  1268. * @returns {Promise<void>} A promise resolving to nothing
  1269. */
  1270. deleteVersion(
  1271. fileID: string,
  1272. versionID: string,
  1273. options?:
  1274. | {
  1275. [key: string]: any;
  1276. etag?: string;
  1277. }
  1278. | Function,
  1279. callback?: Function
  1280. ) {
  1281. // Switch around arguments if necessary for backwwards compatibility
  1282. if (typeof options === 'function') {
  1283. callback = options;
  1284. options = {};
  1285. }
  1286. var params = {};
  1287. if (options && options.etag) {
  1288. (params as Record<string, any>).headers = {
  1289. 'If-Match': options.etag,
  1290. };
  1291. }
  1292. var apiPath = urlPath(BASE_PATH, fileID, VERSIONS_SUBRESOURCE, versionID);
  1293. return this.client.wrapWithDefaultHandler(this.client.del)(
  1294. apiPath,
  1295. params,
  1296. callback
  1297. );
  1298. }
  1299. /**
  1300. * Creates a session used to upload a new file in chunks.. This will first
  1301. * verify that the file can be created and then open a session for uploading
  1302. * pieces of the file.
  1303. *
  1304. * API Endpoint: '/files/upload_sessions'
  1305. * Method: POST
  1306. *
  1307. * @param {string} folderID - The ID of the folder to upload the file to
  1308. * @param {int} size - The size of the file that will be uploaded
  1309. * @param {string} name - The name of the file to be created
  1310. * @param {Function} [callback] - Passed the upload session info if successful
  1311. * @returns {Promise<Object>} A promise resolving to the new upload session object
  1312. */
  1313. createUploadSession(
  1314. folderID: string,
  1315. size: number,
  1316. name: string,
  1317. callback?: Function
  1318. ) {
  1319. var apiURL =
  1320. this.client._uploadBaseURL +
  1321. urlPath(BASE_PATH, UPLOAD_SESSION_SUBRESOURCE),
  1322. params = {
  1323. body: {
  1324. folder_id: folderID,
  1325. file_size: size,
  1326. file_name: name,
  1327. },
  1328. };
  1329. return this.client.wrapWithDefaultHandler(this.client.post)(
  1330. apiURL,
  1331. params,
  1332. callback
  1333. );
  1334. }
  1335. /**
  1336. * Creates a session used to upload a new version of a file in chunks. This
  1337. * will first verify that the version can be created and then open a session for
  1338. * uploading pieces of the file.
  1339. *
  1340. * API Endpoint: '/files/:fileID/upload_sessions'
  1341. * Method: POST
  1342. *
  1343. * @param {string} fileID - The ID of the file to upload a new version of
  1344. * @param {int} size - The size of the file that will be uploaded
  1345. * @param {Function} [callback] - Passed the upload session info if successful
  1346. * @returns {Promise<Object>} A promise resolving to the new upload session object
  1347. */
  1348. createNewVersionUploadSession(
  1349. fileID: string,
  1350. size: number,
  1351. callback?: Function
  1352. ) {
  1353. var apiURL =
  1354. this.client._uploadBaseURL +
  1355. urlPath(BASE_PATH, fileID, UPLOAD_SESSION_SUBRESOURCE),
  1356. params = {
  1357. body: {
  1358. file_size: size,
  1359. },
  1360. };
  1361. return this.client.wrapWithDefaultHandler(this.client.post)(
  1362. apiURL,
  1363. params,
  1364. callback
  1365. );
  1366. }
  1367. /**
  1368. * Uploads a chunk of a file to an open upload session
  1369. *
  1370. * API Endpoint: '/files/upload_sessions/:sessionID'
  1371. * Method: PUT
  1372. *
  1373. * @param {string} sessionID - The ID of the upload session to upload to
  1374. * @param {Buffer|string} part - The chunk of the file to upload
  1375. * @param {int} offset - The byte position where the chunk begins in the file
  1376. * @param {int} totalSize - The total size of the file being uploaded
  1377. * @param {Function} [callback] - Passed the part definition if successful
  1378. * @returns {Promise<Object>} A promise resolving to the part object
  1379. */
  1380. uploadPart(
  1381. sessionID: string,
  1382. part: Buffer | string,
  1383. offset: number,
  1384. totalSize: number,
  1385. callback?: Function
  1386. ) {
  1387. var apiURL =
  1388. this.client._uploadBaseURL +
  1389. urlPath(BASE_PATH, UPLOAD_SESSION_SUBRESOURCE, sessionID);
  1390. var hash = crypto.createHash('sha1').update(part).digest('base64');
  1391. var params = {
  1392. headers: {
  1393. 'Content-Type': 'application/octet-stream',
  1394. Digest: `SHA=${hash}`,
  1395. 'Content-Range': `bytes ${offset}-${
  1396. offset + part.length - 1
  1397. }/${totalSize}`,
  1398. },
  1399. json: false,
  1400. body: part,
  1401. };
  1402. return this.client
  1403. .put(apiURL, params)
  1404. .then((response: any /* FIXME */) => {
  1405. if (response.statusCode !== 200) {
  1406. throw errors.buildUnexpectedResponseError(response);
  1407. }
  1408. return JSON.parse(response.body);
  1409. })
  1410. .asCallback(callback);
  1411. }
  1412. /**
  1413. * Commit an upload session after all parts have been uploaded, creating the new file
  1414. *
  1415. * API Endpoint: '/files/upload_sessions/:sessionID/commit'
  1416. * Method: POST
  1417. *
  1418. * @param {string} sessionID - The ID of the upload session to commit
  1419. * @param {string} fileHash - The base64-encoded SHA-1 hash of the file being uploaded
  1420. * @param {Object} [options] - Optional parameters set on the created file, can be left null
  1421. * @param {UploadPart[]} [options.parts] The list of uploaded parts to be committed, will be fetched from the API otherwise
  1422. * @param {string} [options.description] - Optional description of the uploaded file.
  1423. * @param {Function} [callback] - Passed the new file information if successful
  1424. * @returns {Promise<Object>} A promise resolving to the uploaded file object
  1425. */
  1426. commitUploadSession(
  1427. sessionID: string,
  1428. fileHash: string,
  1429. options?: {
  1430. parts?: UploadPart[];
  1431. description?: string;
  1432. },
  1433. callback?: Function
  1434. ) {
  1435. options = options || {};
  1436. var userParts;
  1437. if (options.parts) {
  1438. userParts = options.parts;
  1439. delete options.parts;
  1440. }
  1441. var apiURL =
  1442. this.client._uploadBaseURL +
  1443. urlPath(BASE_PATH, UPLOAD_SESSION_SUBRESOURCE, sessionID, 'commit'),
  1444. params = {
  1445. headers: {
  1446. Digest: `SHA=${fileHash}`,
  1447. },
  1448. body: {
  1449. attributes: options,
  1450. } as Record<string, any>,
  1451. };
  1452. var fetchParts = (
  1453. offset: any /* FIXME */,
  1454. fetchedParts: any /* FIXME */
  1455. ) => {
  1456. let pagingOptions = {
  1457. limit: 1000,
  1458. offset,
  1459. };
  1460. return this.getUploadSessionParts(sessionID, pagingOptions).then((
  1461. data: any /* FIXME */
  1462. ) => {
  1463. fetchedParts = fetchedParts.concat(data.entries);
  1464. if (data.offset + data.entries.length >= data.total_count) {
  1465. return Promise.resolve(fetchedParts);
  1466. }
  1467. return fetchParts(offset + data.limit, fetchedParts);
  1468. });
  1469. };
  1470. return (userParts ? Promise.resolve(userParts) : fetchParts(0, []))
  1471. .then((parts: any[] /* FIXME */) => {
  1472. // Commit the upload with the list of parts
  1473. params.body.parts = parts;
  1474. return this.client.post(apiURL, params);
  1475. })
  1476. .then((response: any /* FIXME */) => {
  1477. if (response.statusCode === 201) {
  1478. return response.body;
  1479. }
  1480. if (response.statusCode === 202) {
  1481. var retryInterval = response.headers['retry-after'] || 1;
  1482. return Promise.delay(retryInterval * 1000).then(() => {
  1483. // Ensure we don't have to fetch parts from the API again on retry
  1484. options = Object.assign({}, options, { parts: params.body.parts });
  1485. return this.commitUploadSession(sessionID, fileHash, options);
  1486. });
  1487. }
  1488. throw errors.buildUnexpectedResponseError(response);
  1489. })
  1490. .asCallback(callback);
  1491. }
  1492. /**
  1493. * Abort an upload session, discarding any chunks that were uploaded to it
  1494. *
  1495. * API Endpoint: '/files/upload_sessions/:sessionID'
  1496. * Method: DELETE
  1497. *
  1498. * @param {string} sessionID - The ID of the upload session to commit
  1499. * @param {Function} [callback] - Passed nothing if successful, error otherwise
  1500. * @returns {Promise<void>} A promise resolving to nothing
  1501. */
  1502. abortUploadSession(sessionID: string, callback?: Function) {
  1503. var apiURL =
  1504. this.client._uploadBaseURL +
  1505. urlPath(BASE_PATH, UPLOAD_SESSION_SUBRESOURCE, sessionID);
  1506. return this.client.wrapWithDefaultHandler(this.client.del)(
  1507. apiURL,
  1508. null,
  1509. callback
  1510. );
  1511. }
  1512. /**
  1513. * Get a list of all parts that have been uploaded to an upload session
  1514. *
  1515. * API Endpoint: '/files/upload_sessions/:sessionID/parts'
  1516. * Method: GET
  1517. *
  1518. * @param {string} sessionID - The ID of the session to get a list of parts from
  1519. * @param {Object} [options] - Optional parameters, can be left null
  1520. * @param {string} [options.offset] - Paging offset for the list of parts
  1521. * @param {int} [options.limit] - Maximum number of parts to return
  1522. * @param {Function} [callback] - Passed the list of parts if successful
  1523. * @returns {Promise<Object>} A promise resolving to the collection of uploaded parts
  1524. */
  1525. getUploadSessionParts(
  1526. sessionID: string,
  1527. options?: {
  1528. offset?: string;
  1529. limit?: number;
  1530. },
  1531. callback?: Function
  1532. ) {
  1533. var apiURL =
  1534. this.client._uploadBaseURL +
  1535. urlPath(BASE_PATH, UPLOAD_SESSION_SUBRESOURCE, sessionID, 'parts'),
  1536. params = {
  1537. qs: options,
  1538. };
  1539. return this.client.wrapWithDefaultHandler(this.client.get)(
  1540. apiURL,
  1541. params,
  1542. callback
  1543. );
  1544. }
  1545. /**
  1546. * Get the status of an upload session, e.g. whether or not is has started or
  1547. * finished committing
  1548. *
  1549. * API Endpoint: '/files/upload_sessions/:sessionID'
  1550. * Method: GET
  1551. *
  1552. * @param {string} sessionID - The ID of the upload session to get the status of
  1553. * @param {Function} [callback] - Passed the session status if successful
  1554. * @returns {Promise<Object>} A promise resolving to the upload session object
  1555. */
  1556. getUploadSession(sessionID: string, callback?: Function) {
  1557. var apiURL =
  1558. this.client._uploadBaseURL +
  1559. urlPath(BASE_PATH, UPLOAD_SESSION_SUBRESOURCE, sessionID);
  1560. return this.client.wrapWithDefaultHandler(this.client.get)(
  1561. apiURL,
  1562. null,
  1563. callback
  1564. );
  1565. }
  1566. /**
  1567. * Upload a file in chunks, which is generally faster and more reliable for
  1568. * large files.
  1569. *
  1570. * API Endpoint: '/files/upload_sessions'
  1571. * Method: POST
  1572. *
  1573. * @param {string} folderID - The ID of the folder to upload the file to
  1574. * @param {int} size - The size of the file that will be uploaded
  1575. * @param {string} name - The name of the file to be created
  1576. * @param {Buffer|string|Readable} file - The file to upload
  1577. * @param {Object} [options] - Optional parameters for the upload
  1578. * @param {int} [options.parallelism] The number of chunks to upload concurrently
  1579. * @param {int} [options.retryInterval] The amount of time to wait before retrying a failed chunk upload, in ms
  1580. * @param {Object} [options.fileAttributes] Attributes to set on the newly-uploaded file
  1581. * @param {Function} [callback] - Passed the uploader if successful
  1582. * @returns {Promise<ChunkedUploader>} A promise resolving to the chunked uploader
  1583. */
  1584. getChunkedUploader(
  1585. folderID: string,
  1586. size: number,
  1587. name: string,
  1588. file: Buffer | string | Readable,
  1589. options?: {
  1590. parallelism?: number;
  1591. retryInterval?: number;
  1592. fileAttributes?: Record<string, any>;
  1593. },
  1594. callback?: Function
  1595. ) {
  1596. if (file instanceof Readable) {
  1597. // Need to pause the stream immediately to prevent certain libraries,
  1598. // e.g. request from placing the stream into flowing mode and consuming bytes
  1599. file.pause();
  1600. }
  1601. return this.createUploadSession(folderID, size, name)
  1602. .then(
  1603. (sessionInfo: any /* FIXME */) =>
  1604. new ChunkedUploader(this.client, sessionInfo, file, size, options)
  1605. )
  1606. .asCallback(callback);
  1607. }
  1608. /**
  1609. * Upload a new file version in chunks, which is generally faster and more
  1610. * reliable for large files.
  1611. *
  1612. * API Endpoint: '/files/:fileID/upload_sessions'
  1613. * Method: POST
  1614. *
  1615. * @param {string} fileID - The ID of the file to upload a new version of
  1616. * @param {int} size - The size of the file that will be uploaded
  1617. * @param {Buffer|string|Readable} file - The file to upload
  1618. * @param {Object} [options] - Optional parameters for the upload
  1619. * @param {int} [options.parallelism] The number of chunks to upload concurrently
  1620. * @param {int} [options.retryInterval] The amount of time to wait before retrying a failed chunk upload, in ms
  1621. * @param {Object} [options.fileAttributes] Attributes to set on the updated file object
  1622. * @param {Function} [callback] - Passed the uploader if successful
  1623. * @returns {Promise<ChunkedUploader>} A promise resolving to the chunked uploader
  1624. */
  1625. getNewVersionChunkedUploader(
  1626. fileID: string,
  1627. size: number,
  1628. file: Buffer | string | Readable,
  1629. options?: {
  1630. parallelism?: number;
  1631. retryInterval?: number;
  1632. fileAttributes?: Record<string, any>;
  1633. },
  1634. callback?: Function
  1635. ) {
  1636. if (file instanceof Readable) {
  1637. // Need to pause the stream immediately to prevent certain libraries,
  1638. // e.g. request from placing the stream into flowing mode and consuming bytes
  1639. file.pause();
  1640. }
  1641. return this.createNewVersionUploadSession(fileID, size)
  1642. .then(
  1643. (sessionInfo: any /* FIXME */) =>
  1644. new ChunkedUploader(this.client, sessionInfo, file, size, options)
  1645. )
  1646. .asCallback(callback);
  1647. }
  1648. /**
  1649. * Requests collaborations on a given file.
  1650. *
  1651. * API Endpoint: '/files/:fileID/collaborations'
  1652. * Method: GET
  1653. *
  1654. * @param {string} fileID - Box ID of the file being requested
  1655. * @param {Object} [options] - Additional options. Can be left null in most cases.
  1656. * @param {int} [options.limit] - The maximum number of collaborations to return
  1657. * @param {int} [options.offset] - Paging parameter for the collaborations collection
  1658. * @param {string} [options.fields] - Comma-separated list of fields to return on the collaboration objects
  1659. * @param {Function} [callback] - Passed the collaborations if successful, error otherwise
  1660. * @returns {Promise<schemas.Collaborations>} A promise resolving to the collection of collaborations on the file
  1661. */
  1662. getCollaborations(
  1663. fileID: string,
  1664. options?: {
  1665. limit?: number;
  1666. offset?: number;
  1667. fields?: string;
  1668. },
  1669. callback?: Function
  1670. ): Promise<schemas.Collaborations> {
  1671. var params = {
  1672. qs: options,
  1673. };
  1674. var apiPath = urlPath(BASE_PATH, fileID, '/collaborations');
  1675. return this.client.wrapWithDefaultHandler(this.client.get)(
  1676. apiPath,
  1677. params,
  1678. callback
  1679. );
  1680. }
  1681. /**
  1682. * Requests information for all representation objects generated for a specific Box file
  1683. *
  1684. * API Endpoint: '/files/:fileID?fields=representations'
  1685. * Method : GET
  1686. *
  1687. * @param {string} fileID - Box ID of the file being requested
  1688. * @param {client.files.representation} representationType - The x-rep-hints value the application should create a
  1689. * representation for. This value can either come from FileRepresentationType enum or manually created
  1690. * @param {Object} [options] - Additional options. Can be left empty
  1691. * @param {boolean} [options.generateRepresentations = false] - Set to true to return representation info where all states resolve to success.
  1692. * @param {Function} [callback] - Passed an array of representaton objects if successful
  1693. * @returns {Promise<Object>} A promise resolving to the representation response objects
  1694. */
  1695. getRepresentationInfo(
  1696. fileID: string,
  1697. representationType: FileRepresentationType | string,
  1698. options?:
  1699. | {
  1700. generateRepresentations?: boolean;
  1701. }
  1702. | Function,
  1703. callback?: Function
  1704. ) {
  1705. if (typeof options === 'function') {
  1706. callback = options;
  1707. options = {};
  1708. }
  1709. if (!representationType && options && options.generateRepresentations) {
  1710. throw new Error(
  1711. 'Must provide a valid X-Rep-Hints string to get representations with a success status'
  1712. );
  1713. }
  1714. var params = {
  1715. qs: {
  1716. fields: 'representations',
  1717. },
  1718. headers: {
  1719. 'x-rep-hints': representationType,
  1720. },
  1721. };
  1722. var apiPath = urlPath(BASE_PATH, fileID);
  1723. return this.client
  1724. .get(apiPath, params)
  1725. .then((response: any /* FIXME */) => {
  1726. switch (response.statusCode) {
  1727. // 202 - A Box file representation will be generated, but is not ready yet
  1728. case httpStatusCodes.ACCEPTED:
  1729. throw errors.buildResponseError(
  1730. response,
  1731. 'Representation not ready at this time'
  1732. );
  1733. // 200 - A Boxfile representation generated successfully
  1734. // return the representation object
  1735. case httpStatusCodes.OK:
  1736. if (options && (options as any).generateRepresentations) {
  1737. var data = response.body.representations.entries;
  1738. var promiseArray = data.map((entry: any /* FIXME */) => {
  1739. switch (entry.status.state) {
  1740. case 'success':
  1741. case 'viewable':
  1742. case 'error':
  1743. return Promise.resolve(entry);
  1744. default:
  1745. return pollRepresentationInfo(this.client, entry.info.url);
  1746. }
  1747. });
  1748. return Promise.all(promiseArray).then((entries) => ({ entries }));
  1749. }
  1750. return response.body.representations;
  1751. // Unexpected Response
  1752. default:
  1753. throw errors.buildUnexpectedResponseError(response);
  1754. }
  1755. })
  1756. .asCallback(callback);
  1757. }
  1758. /**
  1759. * Get the contents of a representation of a file, e.g, the binary content of an image or pdf.
  1760. *
  1761. * API Endpoint: '/files/:fileID?fields=representations'
  1762. * Method : GET
  1763. *
  1764. * @param {string} fileID The file ID to get the representation of
  1765. * @param {string} representationType The X-Rep-Hints type to request
  1766. * @param {Object} [options] Optional parameters
  1767. * @param {string} [options.assetPath] Asset path for representations with multiple files
  1768. * @param {Function} [callback] Passed a stream over the representation contents if successful
  1769. * @returns {Promise<Readable>} A promise resolving to a stream over the representation contents
  1770. */
  1771. getRepresentationContent(
  1772. fileID: string,
  1773. representationType: FileRepresentationType | string,
  1774. options?: {
  1775. assetPath?: string;
  1776. },
  1777. callback?: Function
  1778. ) {
  1779. if (!representationType) {
  1780. throw new Error('Must provide a valid X-Rep-Hints string');
  1781. }
  1782. options = Object.assign({ assetPath: '' }, options);
  1783. return this.getRepresentationInfo(fileID, representationType)
  1784. .then((reps: any /* FIXME */) => {
  1785. var repInfo = reps.entries.pop();
  1786. if (!repInfo) {
  1787. throw new Error(
  1788. 'Could not get information for requested representation'
  1789. );
  1790. }
  1791. // If the representation is paged, we need to specify which page to get the content for
  1792. // If the assetPath is not specified, we default to the first pages
  1793. if (!options?.assetPath && repInfo.properties?.paged == 'true') {
  1794. options!.assetPath = `1.${repInfo.representation}`;
  1795. }
  1796. switch (repInfo.status.state) {
  1797. case 'success':
  1798. case 'viewable':
  1799. return repInfo.content.url_template;
  1800. case 'error':
  1801. throw new Error('Representation had error status');
  1802. case 'none':
  1803. case 'pending':
  1804. return pollRepresentationInfo(this.client, repInfo.info.url).then((
  1805. info: any /* FIXME */
  1806. ) => {
  1807. if (info.status.state === 'error') {
  1808. throw new Error('Representation had error status');
  1809. }
  1810. return info.content.url_template;
  1811. });
  1812. default:
  1813. throw new Error(
  1814. `Unknown representation status: ${repInfo.status.state}`
  1815. );
  1816. }
  1817. })
  1818. .then((assetURLTemplate: string) => {
  1819. var url = urlTemplate
  1820. .parse(assetURLTemplate)
  1821. .expand({ asset_path: options!.assetPath });
  1822. return this.client.get(url, { streaming: true });
  1823. })
  1824. .asCallback(callback);
  1825. }
  1826. /**
  1827. * Creates a zip of multiple files and folders.
  1828. *
  1829. * API Endpoint: '/zip_downloads'
  1830. * Method: POST
  1831. *
  1832. * @param {name} name - The name of the zip file to be created
  1833. * @param {Array} items - Array of files or folders to be part of the created zip
  1834. * @param {Function} [callback] Passed a zip information object
  1835. * @returns {Promise<string>} A promise resolving to a zip information object
  1836. */
  1837. createZip(name: string, items: any[] /* FIXME */, callback?: Function) {
  1838. var params = {
  1839. body: {
  1840. download_file_name: name,
  1841. items,
  1842. },
  1843. };
  1844. return this.client.wrapWithDefaultHandler(this.client.post)(
  1845. ZIP_DOWNLOAD_PATH,
  1846. params,
  1847. callback
  1848. );
  1849. }
  1850. /**
  1851. * Creates a zip of multiple files and folders and downloads it.
  1852. *
  1853. * API Endpoint: '/zip_downloads'
  1854. * Method: GET
  1855. *
  1856. * @param {name} name - The name of the zip file to be created
  1857. * @param {Array} items - Array of files or folders to be part of the created zip
  1858. * @param {Stream} stream - Stream to pipe the readable stream of the zip file
  1859. * @param {Function} [callback] - Passed a zip download status object
  1860. * @returns {Promise<Readable>} A promise resolving to a zip download status object
  1861. */
  1862. downloadZip(
  1863. name: string,
  1864. items: any[] /* FIXME */,
  1865. stream: Writable,
  1866. callback?: Function
  1867. ) {
  1868. var downloadStreamOptions = {
  1869. streaming: true,
  1870. headers: {},
  1871. };
  1872. var params = {
  1873. body: {
  1874. download_file_name: name,
  1875. items,
  1876. },
  1877. };
  1878. return this.client
  1879. .post(ZIP_DOWNLOAD_PATH, params)
  1880. .then((response: any /* FIXME */) =>
  1881. this.client
  1882. .get(response.body.download_url, downloadStreamOptions)
  1883. .then((responseStream: Readable) => {
  1884. responseStream.pipe(stream);
  1885. // eslint-disable-next-line promise/avoid-new
  1886. return new Promise((resolve, reject) => {
  1887. responseStream.on('end', () => resolve('Done downloading'));
  1888. responseStream.on('error', (error) => reject(error));
  1889. }).then(() =>
  1890. this.client
  1891. .get(response.body.status_url)
  1892. .then((responseStatus: any /* FIXME */) => responseStatus.body)
  1893. );
  1894. })
  1895. )
  1896. .asCallback(callback);
  1897. }
  1898. }
  1899. /**
  1900. * Enum of valid x-rep- hint values for generating representation info
  1901. *
  1902. * @readonly
  1903. * @enum {FileRepresentationType}
  1904. */
  1905. Files.prototype.representation = FileRepresentationType;
  1906. /**
  1907. * @module box-node-sdk/lib/managers/files
  1908. * @see {@Link Files}
  1909. */
  1910. export = Files;