ParseFile.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _CoreManager = _interopRequireDefault(require("./CoreManager"));
  7. function _interopRequireDefault(obj) {
  8. return obj && obj.__esModule ? obj : {
  9. default: obj
  10. };
  11. }
  12. function ownKeys(object, enumerableOnly) {
  13. var keys = Object.keys(object);
  14. if (Object.getOwnPropertySymbols) {
  15. var symbols = Object.getOwnPropertySymbols(object);
  16. enumerableOnly && (symbols = symbols.filter(function (sym) {
  17. return Object.getOwnPropertyDescriptor(object, sym).enumerable;
  18. })), keys.push.apply(keys, symbols);
  19. }
  20. return keys;
  21. }
  22. function _objectSpread(target) {
  23. for (var i = 1; i < arguments.length; i++) {
  24. var source = null != arguments[i] ? arguments[i] : {};
  25. i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
  26. _defineProperty(target, key, source[key]);
  27. }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
  28. Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
  29. });
  30. }
  31. return target;
  32. }
  33. function _defineProperty(obj, key, value) {
  34. if (key in obj) {
  35. Object.defineProperty(obj, key, {
  36. value: value,
  37. enumerable: true,
  38. configurable: true,
  39. writable: true
  40. });
  41. } else {
  42. obj[key] = value;
  43. }
  44. return obj;
  45. }
  46. const ParseError = require('./ParseError').default;
  47. let XHR = null;
  48. if (typeof XMLHttpRequest !== 'undefined') {
  49. XHR = XMLHttpRequest;
  50. }
  51. /*:: type Base64 = { base64: string };*/
  52. /*:: type Uri = { uri: string };*/
  53. /*:: type FileData = Array<number> | Base64 | Blob | Uri;*/
  54. /*:: export type FileSource =
  55. | {
  56. format: 'file',
  57. file: Blob,
  58. type: string,
  59. }
  60. | {
  61. format: 'base64',
  62. base64: string,
  63. type: string,
  64. }
  65. | {
  66. format: 'uri',
  67. uri: string,
  68. type: string,
  69. };*/
  70. function b64Digit(number
  71. /*: number*/
  72. )
  73. /*: string*/
  74. {
  75. if (number < 26) {
  76. return String.fromCharCode(65 + number);
  77. }
  78. if (number < 52) {
  79. return String.fromCharCode(97 + (number - 26));
  80. }
  81. if (number < 62) {
  82. return String.fromCharCode(48 + (number - 52));
  83. }
  84. if (number === 62) {
  85. return '+';
  86. }
  87. if (number === 63) {
  88. return '/';
  89. }
  90. throw new TypeError('Tried to encode large digit ' + number + ' in base64.');
  91. }
  92. /**
  93. * A Parse.File is a local representation of a file that is saved to the Parse
  94. * cloud.
  95. *
  96. * @alias Parse.File
  97. */
  98. class ParseFile {
  99. /*:: _name: string;*/
  100. /*:: _url: ?string;*/
  101. /*:: _source: FileSource;*/
  102. /*:: _previousSave: ?Promise<ParseFile>;*/
  103. /*:: _data: ?string;*/
  104. /*:: _requestTask: ?any;*/
  105. /*:: _metadata: ?Object;*/
  106. /*:: _tags: ?Object;*/
  107. /**
  108. * @param name {String} The file's name. This will be prefixed by a unique
  109. * value once the file has finished saving. The file name must begin with
  110. * an alphanumeric character, and consist of alphanumeric characters,
  111. * periods, spaces, underscores, or dashes.
  112. * @param data {Array} The data for the file, as either:
  113. * 1. an Array of byte value Numbers, or
  114. * 2. an Object like { base64: "..." } with a base64-encoded String.
  115. * 3. an Object like { uri: "..." } with a uri String.
  116. * 4. a File object selected with a file upload control. (3) only works
  117. * in Firefox 3.6+, Safari 6.0.2+, Chrome 7+, and IE 10+.
  118. * For example:
  119. * <pre>
  120. * var fileUploadControl = $("#profilePhotoFileUpload")[0];
  121. * if (fileUploadControl.files.length > 0) {
  122. * var file = fileUploadControl.files[0];
  123. * var name = "photo.jpg";
  124. * var parseFile = new Parse.File(name, file);
  125. * parseFile.save().then(function() {
  126. * // The file has been saved to Parse.
  127. * }, function(error) {
  128. * // The file either could not be read, or could not be saved to Parse.
  129. * });
  130. * }</pre>
  131. * @param type {String} Optional Content-Type header to use for the file. If
  132. * this is omitted, the content type will be inferred from the name's
  133. * extension.
  134. * @param metadata {Object} Optional key value pairs to be stored with file object
  135. * @param tags {Object} Optional key value pairs to be stored with file object
  136. */
  137. constructor(name
  138. /*: string*/
  139. , data
  140. /*:: ?: FileData*/
  141. , type
  142. /*:: ?: string*/
  143. , metadata
  144. /*:: ?: Object*/
  145. , tags
  146. /*:: ?: Object*/
  147. ) {
  148. const specifiedType = type || '';
  149. this._name = name;
  150. this._metadata = metadata || {};
  151. this._tags = tags || {};
  152. if (data !== undefined) {
  153. if (Array.isArray(data)) {
  154. this._data = ParseFile.encodeBase64(data);
  155. this._source = {
  156. format: 'base64',
  157. base64: this._data,
  158. type: specifiedType
  159. };
  160. } else if (typeof Blob !== 'undefined' && data instanceof Blob) {
  161. this._source = {
  162. format: 'file',
  163. file: data,
  164. type: specifiedType
  165. };
  166. } else if (data && typeof data.uri === 'string' && data.uri !== undefined) {
  167. this._source = {
  168. format: 'uri',
  169. uri: data.uri,
  170. type: specifiedType
  171. };
  172. } else if (data && typeof data.base64 === 'string') {
  173. const base64 = data.base64.split(',').slice(-1)[0];
  174. const dataType = specifiedType || data.base64.split(';').slice(0, 1)[0].split(':').slice(1, 2)[0] || 'text/plain';
  175. this._data = base64;
  176. this._source = {
  177. format: 'base64',
  178. base64,
  179. type: dataType
  180. };
  181. } else {
  182. throw new TypeError('Cannot create a Parse.File with that data.');
  183. }
  184. }
  185. }
  186. /**
  187. * Return the data for the file, downloading it if not already present.
  188. * Data is present if initialized with Byte Array, Base64 or Saved with Uri.
  189. * Data is cleared if saved with File object selected with a file upload control
  190. *
  191. * @returns {Promise} Promise that is resolve with base64 data
  192. */
  193. async getData()
  194. /*: Promise<String>*/
  195. {
  196. if (this._data) {
  197. return this._data;
  198. }
  199. if (!this._url) {
  200. throw new Error('Cannot retrieve data for unsaved ParseFile.');
  201. }
  202. const controller = _CoreManager.default.getFileController();
  203. const result = await controller.download(this._url, {
  204. requestTask: task => this._requestTask = task
  205. });
  206. this._data = result.base64;
  207. return this._data;
  208. }
  209. /**
  210. * Gets the name of the file. Before save is called, this is the filename
  211. * given by the user. After save is called, that name gets prefixed with a
  212. * unique identifier.
  213. *
  214. * @returns {string}
  215. */
  216. name()
  217. /*: string*/
  218. {
  219. return this._name;
  220. }
  221. /**
  222. * Gets the url of the file. It is only available after you save the file or
  223. * after you get the file from a Parse.Object.
  224. *
  225. * @param {object} options An object to specify url options
  226. * @returns {string}
  227. */
  228. url(options
  229. /*:: ?: { forceSecure?: boolean }*/
  230. )
  231. /*: ?string*/
  232. {
  233. options = options || {};
  234. if (!this._url) {
  235. return;
  236. }
  237. if (options.forceSecure) {
  238. return this._url.replace(/^http:\/\//i, 'https://');
  239. } else {
  240. return this._url;
  241. }
  242. }
  243. /**
  244. * Gets the metadata of the file.
  245. *
  246. * @returns {object}
  247. */
  248. metadata()
  249. /*: Object*/
  250. {
  251. return this._metadata;
  252. }
  253. /**
  254. * Gets the tags of the file.
  255. *
  256. * @returns {object}
  257. */
  258. tags()
  259. /*: Object*/
  260. {
  261. return this._tags;
  262. }
  263. /**
  264. * Saves the file to the Parse cloud.
  265. *
  266. * @param {object} options
  267. * * Valid options are:<ul>
  268. * <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
  269. * be used for this request.
  270. * <li>sessionToken: A valid session token, used for making a request on
  271. * behalf of a specific user.
  272. * <li>progress: In Browser only, callback for upload progress. For example:
  273. * <pre>
  274. * let parseFile = new Parse.File(name, file);
  275. * parseFile.save({
  276. * progress: (progressValue, loaded, total, { type }) => {
  277. * if (type === "upload" && progressValue !== null) {
  278. * // Update the UI using progressValue
  279. * }
  280. * }
  281. * });
  282. * </pre>
  283. * </ul>
  284. * @returns {Promise} Promise that is resolved when the save finishes.
  285. */
  286. save(options
  287. /*:: ?: FullOptions*/
  288. ) {
  289. options = options || {};
  290. options.requestTask = task => this._requestTask = task;
  291. options.metadata = this._metadata;
  292. options.tags = this._tags;
  293. const controller = _CoreManager.default.getFileController();
  294. if (!this._previousSave) {
  295. if (this._source.format === 'file') {
  296. this._previousSave = controller.saveFile(this._name, this._source, options).then(res => {
  297. this._name = res.name;
  298. this._url = res.url;
  299. this._data = null;
  300. this._requestTask = null;
  301. return this;
  302. });
  303. } else if (this._source.format === 'uri') {
  304. this._previousSave = controller.download(this._source.uri, options).then(result => {
  305. if (!(result && result.base64)) {
  306. return {};
  307. }
  308. const newSource = {
  309. format: 'base64',
  310. base64: result.base64,
  311. type: result.contentType
  312. };
  313. this._data = result.base64;
  314. this._requestTask = null;
  315. return controller.saveBase64(this._name, newSource, options);
  316. }).then(res => {
  317. this._name = res.name;
  318. this._url = res.url;
  319. this._requestTask = null;
  320. return this;
  321. });
  322. } else {
  323. this._previousSave = controller.saveBase64(this._name, this._source, options).then(res => {
  324. this._name = res.name;
  325. this._url = res.url;
  326. this._requestTask = null;
  327. return this;
  328. });
  329. }
  330. }
  331. if (this._previousSave) {
  332. return this._previousSave;
  333. }
  334. }
  335. /**
  336. * Aborts the request if it has already been sent.
  337. */
  338. cancel() {
  339. if (this._requestTask && typeof this._requestTask.abort === 'function') {
  340. this._requestTask.abort();
  341. }
  342. this._requestTask = null;
  343. }
  344. /**
  345. * Deletes the file from the Parse cloud.
  346. * In Cloud Code and Node only with Master Key.
  347. *
  348. * @param {object} options
  349. * * Valid options are:<ul>
  350. * <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
  351. * be used for this request.
  352. * <pre>
  353. * @returns {Promise} Promise that is resolved when the delete finishes.
  354. */
  355. destroy(options
  356. /*:: ?: FullOptions*/
  357. = {}) {
  358. if (!this._name) {
  359. throw new ParseError(ParseError.FILE_DELETE_UNNAMED_ERROR, 'Cannot delete an unnamed file.');
  360. }
  361. const destroyOptions = {
  362. useMasterKey: true
  363. };
  364. if (options.hasOwnProperty('useMasterKey')) {
  365. destroyOptions.useMasterKey = options.useMasterKey;
  366. }
  367. const controller = _CoreManager.default.getFileController();
  368. return controller.deleteFile(this._name, destroyOptions).then(() => {
  369. this._data = null;
  370. this._requestTask = null;
  371. return this;
  372. });
  373. }
  374. toJSON()
  375. /*: { name: ?string, url: ?string }*/
  376. {
  377. return {
  378. __type: 'File',
  379. name: this._name,
  380. url: this._url
  381. };
  382. }
  383. equals(other
  384. /*: mixed*/
  385. )
  386. /*: boolean*/
  387. {
  388. if (this === other) {
  389. return true;
  390. } // Unsaved Files are never equal, since they will be saved to different URLs
  391. return other instanceof ParseFile && this.name() === other.name() && this.url() === other.url() && typeof this.url() !== 'undefined';
  392. }
  393. /**
  394. * Sets metadata to be saved with file object. Overwrites existing metadata
  395. *
  396. * @param {object} metadata Key value pairs to be stored with file object
  397. */
  398. setMetadata(metadata
  399. /*: any*/
  400. ) {
  401. if (metadata && typeof metadata === 'object') {
  402. Object.keys(metadata).forEach(key => {
  403. this.addMetadata(key, metadata[key]);
  404. });
  405. }
  406. }
  407. /**
  408. * Sets metadata to be saved with file object. Adds to existing metadata.
  409. *
  410. * @param {string} key key to store the metadata
  411. * @param {*} value metadata
  412. */
  413. addMetadata(key
  414. /*: string*/
  415. , value
  416. /*: any*/
  417. ) {
  418. if (typeof key === 'string') {
  419. this._metadata[key] = value;
  420. }
  421. }
  422. /**
  423. * Sets tags to be saved with file object. Overwrites existing tags
  424. *
  425. * @param {object} tags Key value pairs to be stored with file object
  426. */
  427. setTags(tags
  428. /*: any*/
  429. ) {
  430. if (tags && typeof tags === 'object') {
  431. Object.keys(tags).forEach(key => {
  432. this.addTag(key, tags[key]);
  433. });
  434. }
  435. }
  436. /**
  437. * Sets tags to be saved with file object. Adds to existing tags.
  438. *
  439. * @param {string} key key to store tags
  440. * @param {*} value tag
  441. */
  442. addTag(key
  443. /*: string*/
  444. , value
  445. /*: string*/
  446. ) {
  447. if (typeof key === 'string') {
  448. this._tags[key] = value;
  449. }
  450. }
  451. static fromJSON(obj)
  452. /*: ParseFile*/
  453. {
  454. if (obj.__type !== 'File') {
  455. throw new TypeError('JSON object does not represent a ParseFile');
  456. }
  457. const file = new ParseFile(obj.name);
  458. file._url = obj.url;
  459. return file;
  460. }
  461. static encodeBase64(bytes
  462. /*: Array<number>*/
  463. )
  464. /*: string*/
  465. {
  466. const chunks = [];
  467. chunks.length = Math.ceil(bytes.length / 3);
  468. for (let i = 0; i < chunks.length; i++) {
  469. const b1 = bytes[i * 3];
  470. const b2 = bytes[i * 3 + 1] || 0;
  471. const b3 = bytes[i * 3 + 2] || 0;
  472. const has2 = i * 3 + 1 < bytes.length;
  473. const has3 = i * 3 + 2 < bytes.length;
  474. chunks[i] = [b64Digit(b1 >> 2 & 0x3f), b64Digit(b1 << 4 & 0x30 | b2 >> 4 & 0x0f), has2 ? b64Digit(b2 << 2 & 0x3c | b3 >> 6 & 0x03) : '=', has3 ? b64Digit(b3 & 0x3f) : '='].join('');
  475. }
  476. return chunks.join('');
  477. }
  478. }
  479. const DefaultController = {
  480. saveFile: async function (name
  481. /*: string*/
  482. , source
  483. /*: FileSource*/
  484. , options
  485. /*:: ?: FullOptions*/
  486. ) {
  487. if (source.format !== 'file') {
  488. throw new Error('saveFile can only be used with File-type sources.');
  489. }
  490. const base64Data = await new Promise((res, rej) => {
  491. // eslint-disable-next-line no-undef
  492. const reader = new FileReader();
  493. reader.onload = () => res(reader.result);
  494. reader.onerror = error => rej(error);
  495. reader.readAsDataURL(source.file);
  496. }); // we only want the data after the comma
  497. // For example: "data:application/pdf;base64,JVBERi0xLjQKJ..." we would only want "JVBERi0xLjQKJ..."
  498. const [first, second] = base64Data.split(','); // in the event there is no 'data:application/pdf;base64,' at the beginning of the base64 string
  499. // use the entire string instead
  500. const data = second ? second : first;
  501. const newSource = {
  502. format: 'base64',
  503. base64: data,
  504. type: source.type || (source.file ? source.file.type : null)
  505. };
  506. return await DefaultController.saveBase64(name, newSource, options);
  507. },
  508. saveBase64: function (name
  509. /*: string*/
  510. , source
  511. /*: FileSource*/
  512. , options
  513. /*:: ?: FullOptions*/
  514. ) {
  515. if (source.format !== 'base64') {
  516. throw new Error('saveBase64 can only be used with Base64-type sources.');
  517. }
  518. const data
  519. /*: { base64: any, _ContentType?: any, fileData: Object }*/
  520. = {
  521. base64: source.base64,
  522. fileData: {
  523. metadata: _objectSpread({}, options.metadata),
  524. tags: _objectSpread({}, options.tags)
  525. }
  526. };
  527. delete options.metadata;
  528. delete options.tags;
  529. if (source.type) {
  530. data._ContentType = source.type;
  531. }
  532. return _CoreManager.default.getRESTController().request('POST', 'files/' + name, data, options);
  533. },
  534. download: function (uri, options) {
  535. if (XHR) {
  536. return this.downloadAjax(uri, options);
  537. } else {
  538. return new Promise((resolve, reject) => {
  539. const client = uri.indexOf('https') === 0 ? require('https') : require('http');
  540. const req = client.get(uri, resp => {
  541. resp.setEncoding('base64');
  542. let base64 = '';
  543. resp.on('data', data => base64 += data);
  544. resp.on('end', () => {
  545. resolve({
  546. base64,
  547. contentType: resp.headers['content-type']
  548. });
  549. });
  550. });
  551. req.on('abort', () => {
  552. resolve({});
  553. });
  554. req.on('error', reject);
  555. options.requestTask(req);
  556. });
  557. }
  558. },
  559. downloadAjax: function (uri, options) {
  560. return new Promise((resolve, reject) => {
  561. const xhr = new XHR();
  562. xhr.open('GET', uri, true);
  563. xhr.responseType = 'arraybuffer';
  564. xhr.onerror = function (e) {
  565. reject(e);
  566. };
  567. xhr.onreadystatechange = function () {
  568. if (xhr.readyState !== xhr.DONE) {
  569. return;
  570. }
  571. if (!this.response) {
  572. return resolve({});
  573. }
  574. const bytes = new Uint8Array(this.response);
  575. resolve({
  576. base64: ParseFile.encodeBase64(bytes),
  577. contentType: xhr.getResponseHeader('content-type')
  578. });
  579. };
  580. options.requestTask(xhr);
  581. xhr.send();
  582. });
  583. },
  584. deleteFile: function (name
  585. /*: string*/
  586. , options
  587. /*:: ?: FullOptions*/
  588. ) {
  589. const headers = {
  590. 'X-Parse-Application-ID': _CoreManager.default.get('APPLICATION_ID')
  591. };
  592. if (options.useMasterKey) {
  593. headers['X-Parse-Master-Key'] = _CoreManager.default.get('MASTER_KEY');
  594. }
  595. let url = _CoreManager.default.get('SERVER_URL');
  596. if (url[url.length - 1] !== '/') {
  597. url += '/';
  598. }
  599. url += 'files/' + name;
  600. return _CoreManager.default.getRESTController().ajax('DELETE', url, '', headers).catch(response => {
  601. // TODO: return JSON object in server
  602. if (!response || response === 'SyntaxError: Unexpected end of JSON input') {
  603. return Promise.resolve();
  604. } else {
  605. return _CoreManager.default.getRESTController().handleError(response);
  606. }
  607. });
  608. },
  609. _setXHR(xhr
  610. /*: any*/
  611. ) {
  612. XHR = xhr;
  613. },
  614. _getXHR() {
  615. return XHR;
  616. }
  617. };
  618. _CoreManager.default.setFileController(DefaultController);
  619. var _default = ParseFile;
  620. exports.default = _default;
  621. exports.b64Digit = b64Digit;