import Ember from 'ember';
import config from 'ember-get-config';
/**
* @module ember-osf
* @submodule services
*/
/**
* An Ember service for doing things to files.
* Essentially a wrapper for the Waterbutler API.
* http://waterbutler.readthedocs.io/
*
* @class file-manager
* @extends Ember.Service
*/
export default Ember.Service.extend({
session: Ember.inject.service(),
store: Ember.inject.service(),
currentUser: Ember.inject.service('current-user'),
/**
* Get a URL to download the given file.
*
* @method getDownloadUrl
* @param {file} file A `file` model
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the URL.
* @param {Object} [options.query.version] `file-version` ID
* @return {String} Download URL
*/
getDownloadUrl(file, options = {}) {
let url = file.get('links.download');
if (!options.query) {
options.query = {};
}
if (file.get('isFolder')) {
options.query.zip = '';
}
let queryString = Ember.$.param(options.query);
if (queryString.length) {
return `${url}?${queryString}`;
} else {
return url;
}
},
/**
* Download the contents of the given file.
*
* @method getContents
* @param {file} file A `file` model with `isFolder == false`.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @return {Promise} Promise that resolves to the file contents or rejects
* with an error message.
*/
getContents(file, options = {}) {
let url = file.get('links.download');
return this._waterbutlerRequest('GET', url, options);
},
/**
* Upload a new version of an existing file.
*
* @method updateContents
* @param {file} file A `file` model with `isFolder == false`.
* @param {Object} contents A native `File` object or another appropriate
* payload for uploading.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @return {Promise} Promise that resolves to the updated `file` model or
* rejects with an error message.
*/
updateContents(file, contents, options = {}) {
let url = file.get('links').upload;
if (!options.query) {
options.query = {};
}
options.query.kind = 'file';
options.data = contents;
let p = this._waterbutlerRequest('PUT', url, options);
return p.then(() => this._reloadModel(file));
},
/**
* Check out a file, so only the current user can modify it.
*
* @method checkOut
* @param {file} file `file` model with `isFolder == false`.
* @return {Promise} Promise that resolves on success or rejects with an
* error message.
*/
checkOut(file) {
return Ember.run(() => {
let userID = this.get('session.data.authenticated.id');
file.set('checkout', userID);
return file.save().catch((error) => {
file.rollbackAttributes();
throw error;
});
});
},
/**
* Check in a file, so anyone with permission can modify it.
*
* @method checkOut
* @param {file} file `file` model with `isFolder == false`.
* @return {Promise} Promise that resolves on success or rejects with an
* error message.
*/
checkIn(file) {
return Ember.run(() => {
file.set('checkout', null);
return file.save().catch((error) => {
file.rollbackAttributes();
throw error;
});
});
},
/**
* Create a new folder
*
* @method addSubfolder
* @param {file} folder Location of the new folder, a `file` model with
* `isFolder == true`.
* @param {String} name Name of the folder to create.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @return {Promise} Promise that resolves to the new folder's model or
* rejects with an error message.
*/
addSubfolder(folder, name, options = {}) {
let url = folder.get('links').new_folder;
if (!options.query) {
options.query = {};
}
options.query.name = name;
options.query.kind = 'folder';
// HACK: This is the only WB link that already has a query string
let queryStart = url.search(/\?kind=folder$/);
if (queryStart > -1) {
url = url.slice(0, queryStart);
}
let p = this._waterbutlerRequest('PUT', url, options);
return p.then(() => this._getNewFileInfo(folder, name));
},
/**
* Upload a file
*
* @method uploadFile
* @param {file} folder Location of the new file, a `file` model with
* `isFolder == true`.
* @param {String} name Name of the new file.
* @param {Object} contents A native `File` object or another appropriate
* payload for uploading.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @return {Promise} Promise that resolves to the new file's model or
* rejects with an error message.
*/
uploadFile(folder, name, contents, options = {}) {
let url = folder.get('links').upload;
options.data = contents;
if (!options.query) {
options.query = {};
}
options.query.name = name;
options.query.kind = 'file';
let p = this._waterbutlerRequest('PUT', url, options);
return p.then(() => this._getNewFileInfo(folder, name));
},
/**
* Rename a file or folder
*
* @method rename
* @param {file} file `file` model to rename.
* @param {String} newName New name for the file.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @return {Promise} Promise that resolves to the updated `file` model or
* rejects with an error message.
*/
rename(file, newName, options = {}) {
let url = file.get('links').move;
options.data = JSON.stringify({ action: 'rename', rename: newName });
let p = this._waterbutlerRequest('POST', url, options);
return p.then(() => this._reloadModel(file));
},
/**
* Move (or copy) a file or folder
*
* @method move
* @param {file} file `file` model to move.
* @param {file} targetFolder Where to move the file, a `file` model with
* `isFolder == true`.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @param {String} [options.data.rename] If specified, also rename the file
* to the given name.
* @param {String} [options.data.resource] Optional node ID. If specified,
* move the file to that node.
* @param {String} [options.data.provider] Optional provider name. If
* specified, move the file to that provider.
* @param {String} [options.data.action='move'] Either 'move' or 'copy'.
* @param {String} [options.data.conflict='replace'] Specifies what to do if
* a file of the same name already exists in the target folder. If 'keep',
* rename this file to avoid conflict. If replace, the existing file is
* destroyed.
* @return {Promise} Promise that resolves to the the updated (or newly
* created) `file` model or rejects with an error message.
*/
move(file, targetFolder, options = {}) {
let url = file.get('links').move;
let defaultData = {
action: 'move',
path: targetFolder.get('path')
};
Ember.$.extend(defaultData, options.data);
options.data = JSON.stringify(defaultData);
let p = this._waterbutlerRequest('POST', url, options);
return p.then((wbResponse) => {
let name = wbResponse.data.attributes.name;
return this._getNewFileInfo(targetFolder, name);
});
},
/**
* Copy a file or folder.
* Convenience method for `move` with `options.copy == true`.
*
* @method copy
* @param {file} file `file` model to copy.
* @param {file} targetFolder Where to copy the file, a `file` model with
* `isFolder == true`.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @param {String} [options.data.rename] If specified, also rename the file
* to the given name.
* @param {String} [options.data.resource] Optional node ID. If specified,
* move the file to that node.
* @param {String} [options.data.provider] Optional provider name. If
* specified, move the file to that provider.
* @param {String} [options.data.conflict='replace'] Specifies what to do if
* a file of the same name already exists in the target folder. If 'keep',
* rename this file to avoid conflict. If replace, the existing file is
* destroyed.
* @return {Promise} Promise that resolves to the the new `file` model or
* rejects with an error message.
*/
copy(file, targetFolder, options={}) {
if (!options.data) {
options.data = {};
}
options.data.action = 'copy';
return this.move(file, targetFolder, options);
},
/**
* Delete a file or folder
*
* @method deleteFile
* @param {file} file `file` model to delete.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @return {Promise} Promise that resolves on success or rejects with an
* error message.
*/
deleteFile(file, options = {}) {
let url = file.get('links').delete;
let p = this._waterbutlerRequest('DELETE', url, options);
return p.then(() => file.get('parentFolder').then((parent) => {
if (parent) {
return this._reloadModel(parent.get('files'));
} else {
this.get('store').unloadRecord(file);
return true;
}
})
);
},
/**
* Check whether the given url corresponds to a model that is currently
* reloading after a file operation.
*
* Used by `mixin:file-cache-bypass` to avoid a race condition where the
* cache might return stale, inaccurate data.
*
* @method isReloadingUrl
* @param {String} url
* @return {Boolean} `true` if `url` corresponds to a pending reload on a
* model immediately after a Waterbutler action, otherwise `false`.
*/
isReloadingUrl(url) {
return !!this._reloadingUrls[url];
},
/**
* Hash set of URLs for `model.reload()` calls that are still pending.
*
* @property _reloadingUrls
* @private
*/
_reloadingUrls: {},
/**
* Force-reload a model from the API.
*
* @method _reloadModel
* @private
* @param {Object} model `file` model or a `files` relationship
* @return {Promise} Promise that resolves to the reloaded model or
* rejects with an error message.
*/
_reloadModel(model) {
// If it's a file model, it has its own URL in `links.info`.
let reloadUrl = model.get('links.info');
if (!reloadUrl) {
// If it's not a file model, it must be a relationship.
// HACK: Looking at Ember's privates.
reloadUrl = model.get('content.relationship.link');
}
if (reloadUrl) {
this._reloadingUrls[reloadUrl] = true;
}
return model.reload().then((freshModel) => {
if (reloadUrl) {
delete this._reloadingUrls[reloadUrl];
}
return freshModel;
}).catch((error) => {
if (reloadUrl) {
delete this._reloadingUrls[reloadUrl];
}
throw error;
});
},
/**
* Make a Waterbutler request
*
* @method _waterbutlerRequest
* @private
* @param {String} method HTTP method for the request.
* @param {String} url Waterbutler URL.
* @param {Object} [options] Options hash
* @param {Object} [options.query] Key-value hash of query parameters to
* add to the request URL.
* @param {Object} [options.data] Payload to be sent.
* @return {Promise} Promise that resolves to the data returned from the
* server on success, or rejects with an error message.
*/
_waterbutlerRequest(method, url, options = {}) {
if (options.query) {
let queryString = Ember.$.param(options.query);
url = `${url}?${queryString}`;
}
let headers = {};
let authType = config['ember-simple-auth'].authorizer;
this.get('session').authorize(authType, (headerName, content) => {
headers[headerName] = content;
});
return new Ember.RSVP.Promise((resolve, reject) => {
const opts = {
url,
method,
headers,
data: options.data,
processData: false
};
let p = this.get('currentUser').authenticatedAJAX(opts);
p.done((data) => resolve(data));
p.fail((_, __, error) => reject(error));
});
},
/**
* Get the `file` model for a newly created file.
*
* @method _getNewFileInfo
* @private
* @param {file} parentFolder Model for the new file's parent folder.
* @param {String} name Name of the new file.
* @return {Promise} Promise that resolves to the new file's model or
* rejects with an error message.
*/
_getNewFileInfo(parentFolder, name) {
let p = parentFolder.queryHasMany('files', {
'filter[name]': name
});
return p.then((files) => {
let file = files.findBy('name', name);
if (!file) {
throw 'Cannot load metadata for uploaded file.';
}
return file;
});
}
});