API Docs for: 0.25.0
Show:

File: addon/services/file-manager.js

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;
        });
    }
});