API Docs for: 0.25.0
Show:

File: addon/services/file-manager.js

  1. import Ember from 'ember';
  2. import config from 'ember-get-config';
  3.  
  4. /**
  5. * @module ember-osf
  6. * @submodule services
  7. */
  8.  
  9. /**
  10. * An Ember service for doing things to files.
  11. * Essentially a wrapper for the Waterbutler API.
  12. * http://waterbutler.readthedocs.io/
  13. *
  14. * @class file-manager
  15. * @extends Ember.Service
  16. */
  17. export default Ember.Service.extend({
  18. session: Ember.inject.service(),
  19. store: Ember.inject.service(),
  20. currentUser: Ember.inject.service('current-user'),
  21.  
  22. /**
  23. * Get a URL to download the given file.
  24. *
  25. * @method getDownloadUrl
  26. * @param {file} file A `file` model
  27. * @param {Object} [options] Options hash
  28. * @param {Object} [options.query] Key-value hash of query parameters to
  29. * add to the URL.
  30. * @param {Object} [options.query.version] `file-version` ID
  31. * @return {String} Download URL
  32. */
  33. getDownloadUrl(file, options = {}) {
  34. let url = file.get('links.download');
  35.  
  36. if (!options.query) {
  37. options.query = {};
  38. }
  39. if (file.get('isFolder')) {
  40. options.query.zip = '';
  41. }
  42. let queryString = Ember.$.param(options.query);
  43. if (queryString.length) {
  44. return `${url}?${queryString}`;
  45. } else {
  46. return url;
  47. }
  48. },
  49.  
  50. /**
  51. * Download the contents of the given file.
  52. *
  53. * @method getContents
  54. * @param {file} file A `file` model with `isFolder == false`.
  55. * @param {Object} [options] Options hash
  56. * @param {Object} [options.query] Key-value hash of query parameters to
  57. * add to the request URL.
  58. * @param {Object} [options.data] Payload to be sent.
  59. * @return {Promise} Promise that resolves to the file contents or rejects
  60. * with an error message.
  61. */
  62. getContents(file, options = {}) {
  63. let url = file.get('links.download');
  64. return this._waterbutlerRequest('GET', url, options);
  65. },
  66.  
  67. /**
  68. * Upload a new version of an existing file.
  69. *
  70. * @method updateContents
  71. * @param {file} file A `file` model with `isFolder == false`.
  72. * @param {Object} contents A native `File` object or another appropriate
  73. * payload for uploading.
  74. * @param {Object} [options] Options hash
  75. * @param {Object} [options.query] Key-value hash of query parameters to
  76. * add to the request URL.
  77. * @param {Object} [options.data] Payload to be sent.
  78. * @return {Promise} Promise that resolves to the updated `file` model or
  79. * rejects with an error message.
  80. */
  81. updateContents(file, contents, options = {}) {
  82. let url = file.get('links').upload;
  83. if (!options.query) {
  84. options.query = {};
  85. }
  86. options.query.kind = 'file';
  87. options.data = contents;
  88.  
  89. let p = this._waterbutlerRequest('PUT', url, options);
  90. return p.then(() => this._reloadModel(file));
  91. },
  92.  
  93. /**
  94. * Check out a file, so only the current user can modify it.
  95. *
  96. * @method checkOut
  97. * @param {file} file `file` model with `isFolder == false`.
  98. * @return {Promise} Promise that resolves on success or rejects with an
  99. * error message.
  100. */
  101. checkOut(file) {
  102. return Ember.run(() => {
  103. let userID = this.get('session.data.authenticated.id');
  104. file.set('checkout', userID);
  105. return file.save().catch((error) => {
  106. file.rollbackAttributes();
  107. throw error;
  108. });
  109. });
  110. },
  111.  
  112. /**
  113. * Check in a file, so anyone with permission can modify it.
  114. *
  115. * @method checkOut
  116. * @param {file} file `file` model with `isFolder == false`.
  117. * @return {Promise} Promise that resolves on success or rejects with an
  118. * error message.
  119. */
  120. checkIn(file) {
  121. return Ember.run(() => {
  122. file.set('checkout', null);
  123. return file.save().catch((error) => {
  124. file.rollbackAttributes();
  125. throw error;
  126. });
  127. });
  128. },
  129.  
  130. /**
  131. * Create a new folder
  132. *
  133. * @method addSubfolder
  134. * @param {file} folder Location of the new folder, a `file` model with
  135. * `isFolder == true`.
  136. * @param {String} name Name of the folder to create.
  137. * @param {Object} [options] Options hash
  138. * @param {Object} [options.query] Key-value hash of query parameters to
  139. * add to the request URL.
  140. * @param {Object} [options.data] Payload to be sent.
  141. * @return {Promise} Promise that resolves to the new folder's model or
  142. * rejects with an error message.
  143. */
  144. addSubfolder(folder, name, options = {}) {
  145. let url = folder.get('links').new_folder;
  146. if (!options.query) {
  147. options.query = {};
  148. }
  149. options.query.name = name;
  150. options.query.kind = 'folder';
  151.  
  152. // HACK: This is the only WB link that already has a query string
  153. let queryStart = url.search(/\?kind=folder$/);
  154. if (queryStart > -1) {
  155. url = url.slice(0, queryStart);
  156. }
  157. let p = this._waterbutlerRequest('PUT', url, options);
  158. return p.then(() => this._getNewFileInfo(folder, name));
  159. },
  160.  
  161. /**
  162. * Upload a file
  163. *
  164. * @method uploadFile
  165. * @param {file} folder Location of the new file, a `file` model with
  166. * `isFolder == true`.
  167. * @param {String} name Name of the new file.
  168. * @param {Object} contents A native `File` object or another appropriate
  169. * payload for uploading.
  170. * @param {Object} [options] Options hash
  171. * @param {Object} [options.query] Key-value hash of query parameters to
  172. * add to the request URL.
  173. * @param {Object} [options.data] Payload to be sent.
  174. * @return {Promise} Promise that resolves to the new file's model or
  175. * rejects with an error message.
  176. */
  177. uploadFile(folder, name, contents, options = {}) {
  178. let url = folder.get('links').upload;
  179. options.data = contents;
  180. if (!options.query) {
  181. options.query = {};
  182. }
  183. options.query.name = name;
  184. options.query.kind = 'file';
  185.  
  186. let p = this._waterbutlerRequest('PUT', url, options);
  187. return p.then(() => this._getNewFileInfo(folder, name));
  188. },
  189.  
  190. /**
  191. * Rename a file or folder
  192. *
  193. * @method rename
  194. * @param {file} file `file` model to rename.
  195. * @param {String} newName New name for the file.
  196. * @param {Object} [options] Options hash
  197. * @param {Object} [options.query] Key-value hash of query parameters to
  198. * add to the request URL.
  199. * @param {Object} [options.data] Payload to be sent.
  200. * @return {Promise} Promise that resolves to the updated `file` model or
  201. * rejects with an error message.
  202. */
  203. rename(file, newName, options = {}) {
  204. let url = file.get('links').move;
  205. options.data = JSON.stringify({ action: 'rename', rename: newName });
  206.  
  207. let p = this._waterbutlerRequest('POST', url, options);
  208. return p.then(() => this._reloadModel(file));
  209. },
  210.  
  211. /**
  212. * Move (or copy) a file or folder
  213. *
  214. * @method move
  215. * @param {file} file `file` model to move.
  216. * @param {file} targetFolder Where to move the file, a `file` model with
  217. * `isFolder == true`.
  218. * @param {Object} [options] Options hash
  219. * @param {Object} [options.query] Key-value hash of query parameters to
  220. * add to the request URL.
  221. * @param {Object} [options.data] Payload to be sent.
  222. * @param {String} [options.data.rename] If specified, also rename the file
  223. * to the given name.
  224. * @param {String} [options.data.resource] Optional node ID. If specified,
  225. * move the file to that node.
  226. * @param {String} [options.data.provider] Optional provider name. If
  227. * specified, move the file to that provider.
  228. * @param {String} [options.data.action='move'] Either 'move' or 'copy'.
  229. * @param {String} [options.data.conflict='replace'] Specifies what to do if
  230. * a file of the same name already exists in the target folder. If 'keep',
  231. * rename this file to avoid conflict. If replace, the existing file is
  232. * destroyed.
  233. * @return {Promise} Promise that resolves to the the updated (or newly
  234. * created) `file` model or rejects with an error message.
  235. */
  236. move(file, targetFolder, options = {}) {
  237. let url = file.get('links').move;
  238. let defaultData = {
  239. action: 'move',
  240. path: targetFolder.get('path')
  241. };
  242. Ember.$.extend(defaultData, options.data);
  243. options.data = JSON.stringify(defaultData);
  244.  
  245. let p = this._waterbutlerRequest('POST', url, options);
  246. return p.then((wbResponse) => {
  247. let name = wbResponse.data.attributes.name;
  248. return this._getNewFileInfo(targetFolder, name);
  249. });
  250. },
  251.  
  252. /**
  253. * Copy a file or folder.
  254. * Convenience method for `move` with `options.copy == true`.
  255. *
  256. * @method copy
  257. * @param {file} file `file` model to copy.
  258. * @param {file} targetFolder Where to copy the file, a `file` model with
  259. * `isFolder == true`.
  260. * @param {Object} [options] Options hash
  261. * @param {Object} [options.query] Key-value hash of query parameters to
  262. * add to the request URL.
  263. * @param {Object} [options.data] Payload to be sent.
  264. * @param {String} [options.data.rename] If specified, also rename the file
  265. * to the given name.
  266. * @param {String} [options.data.resource] Optional node ID. If specified,
  267. * move the file to that node.
  268. * @param {String} [options.data.provider] Optional provider name. If
  269. * specified, move the file to that provider.
  270. * @param {String} [options.data.conflict='replace'] Specifies what to do if
  271. * a file of the same name already exists in the target folder. If 'keep',
  272. * rename this file to avoid conflict. If replace, the existing file is
  273. * destroyed.
  274. * @return {Promise} Promise that resolves to the the new `file` model or
  275. * rejects with an error message.
  276. */
  277. copy(file, targetFolder, options={}) {
  278. if (!options.data) {
  279. options.data = {};
  280. }
  281. options.data.action = 'copy';
  282. return this.move(file, targetFolder, options);
  283. },
  284.  
  285. /**
  286. * Delete a file or folder
  287. *
  288. * @method deleteFile
  289. * @param {file} file `file` model to delete.
  290. * @param {Object} [options] Options hash
  291. * @param {Object} [options.query] Key-value hash of query parameters to
  292. * add to the request URL.
  293. * @param {Object} [options.data] Payload to be sent.
  294. * @return {Promise} Promise that resolves on success or rejects with an
  295. * error message.
  296. */
  297. deleteFile(file, options = {}) {
  298. let url = file.get('links').delete;
  299. let p = this._waterbutlerRequest('DELETE', url, options);
  300. return p.then(() => file.get('parentFolder').then((parent) => {
  301. if (parent) {
  302. return this._reloadModel(parent.get('files'));
  303. } else {
  304. this.get('store').unloadRecord(file);
  305. return true;
  306. }
  307. })
  308. );
  309. },
  310.  
  311. /**
  312. * Check whether the given url corresponds to a model that is currently
  313. * reloading after a file operation.
  314. *
  315. * Used by `mixin:file-cache-bypass` to avoid a race condition where the
  316. * cache might return stale, inaccurate data.
  317. *
  318. * @method isReloadingUrl
  319. * @param {String} url
  320. * @return {Boolean} `true` if `url` corresponds to a pending reload on a
  321. * model immediately after a Waterbutler action, otherwise `false`.
  322. */
  323. isReloadingUrl(url) {
  324. return !!this._reloadingUrls[url];
  325. },
  326.  
  327. /**
  328. * Hash set of URLs for `model.reload()` calls that are still pending.
  329. *
  330. * @property _reloadingUrls
  331. * @private
  332. */
  333. _reloadingUrls: {},
  334.  
  335. /**
  336. * Force-reload a model from the API.
  337. *
  338. * @method _reloadModel
  339. * @private
  340. * @param {Object} model `file` model or a `files` relationship
  341. * @return {Promise} Promise that resolves to the reloaded model or
  342. * rejects with an error message.
  343. */
  344. _reloadModel(model) {
  345. // If it's a file model, it has its own URL in `links.info`.
  346. let reloadUrl = model.get('links.info');
  347. if (!reloadUrl) {
  348. // If it's not a file model, it must be a relationship.
  349. // HACK: Looking at Ember's privates.
  350. reloadUrl = model.get('content.relationship.link');
  351. }
  352. if (reloadUrl) {
  353. this._reloadingUrls[reloadUrl] = true;
  354. }
  355.  
  356. return model.reload().then((freshModel) => {
  357. if (reloadUrl) {
  358. delete this._reloadingUrls[reloadUrl];
  359. }
  360. return freshModel;
  361. }).catch((error) => {
  362. if (reloadUrl) {
  363. delete this._reloadingUrls[reloadUrl];
  364. }
  365. throw error;
  366. });
  367. },
  368.  
  369. /**
  370. * Make a Waterbutler request
  371. *
  372. * @method _waterbutlerRequest
  373. * @private
  374. * @param {String} method HTTP method for the request.
  375. * @param {String} url Waterbutler URL.
  376. * @param {Object} [options] Options hash
  377. * @param {Object} [options.query] Key-value hash of query parameters to
  378. * add to the request URL.
  379. * @param {Object} [options.data] Payload to be sent.
  380. * @return {Promise} Promise that resolves to the data returned from the
  381. * server on success, or rejects with an error message.
  382. */
  383. _waterbutlerRequest(method, url, options = {}) {
  384. if (options.query) {
  385. let queryString = Ember.$.param(options.query);
  386. url = `${url}?${queryString}`;
  387. }
  388.  
  389. let headers = {};
  390. let authType = config['ember-simple-auth'].authorizer;
  391. this.get('session').authorize(authType, (headerName, content) => {
  392. headers[headerName] = content;
  393. });
  394.  
  395. return new Ember.RSVP.Promise((resolve, reject) => {
  396. const opts = {
  397. url,
  398. method,
  399. headers,
  400. data: options.data,
  401. processData: false
  402. };
  403.  
  404. let p = this.get('currentUser').authenticatedAJAX(opts);
  405.  
  406. p.done((data) => resolve(data));
  407. p.fail((_, __, error) => reject(error));
  408. });
  409. },
  410.  
  411. /**
  412. * Get the `file` model for a newly created file.
  413. *
  414. * @method _getNewFileInfo
  415. * @private
  416. * @param {file} parentFolder Model for the new file's parent folder.
  417. * @param {String} name Name of the new file.
  418. * @return {Promise} Promise that resolves to the new file's model or
  419. * rejects with an error message.
  420. */
  421. _getNewFileInfo(parentFolder, name) {
  422. let p = parentFolder.queryHasMany('files', {
  423. 'filter[name]': name
  424. });
  425. return p.then((files) => {
  426. let file = files.findBy('name', name);
  427. if (!file) {
  428. throw 'Cannot load metadata for uploaded file.';
  429. }
  430. return file;
  431. });
  432. }
  433. });
  434.