ParseFile.js 17 KB

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