API Docs for: 0.8.0
Show:

File: include/service/entities/content/media_service_v2.js

/*
 Copyright (C) 2016  PencilBlue, LLC

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

//dependencies
var util  = require('../../../util.js');
var async = require('async');
var path = require('path');
var fs = require('fs');

module.exports = function(pb) {

    //pb dependencies
    var DAO = pb.DAO;
    var BaseObjectService = pb.BaseObjectService;
    var ValidationService = pb.ValidationService;

    /**
     * Provides functions to interact with media objects.  This also includes interacting with the media contents
     *
     * @class MediaServiceV2
     * @constructor
     * @extends BaseObjectService
     * @param {Object} context
     * @param {string} context.site
     * @param {boolean} context.onlyThisSite
     * @param {MediaProvider} [context.provider]
     */
    function MediaServiceV2(context) {

        /**
         * @property topicService
         * @type {TopicService}
         */
        this.topicService = new pb.TopicService(util.merge(context, {}));

        /**
         * @property provider
         * @type {MediaProvider}
         */
        this.provider = context.provider || MediaServiceV2.loadMediaProvider(context);

        context.type = TYPE;
        MediaServiceV2.super_.call(this, context);
    }
    util.inherits(MediaServiceV2, BaseObjectService);

    /**
     *
     * @private
     * @static
     * @readonly
     * @property TYPE
     * @type {String}
     */
    var TYPE = 'media';

    /**
     * @private
     * @static
     * @property MEDIA_PROVIDERS
     * @type {Object}
     */
    var MEDIA_PROVIDERS = Object.freeze({
        fs: pb.media.providers.FsMediaProvider,
        mongo: pb.media.providers.MongoMediaProvider
    });

    /**
     * Contains the list of media renderers
     * @private
     * @static
     * @property REGISTERED_MEDIA_RENDERERS
     * @type {Array}
     */
    var REGISTERED_MEDIA_RENDERERS = [
        pb.media.renderers.ImageMediaRenderer,
        pb.media.renderers.VideoMediaRenderer,
        pb.media.renderers.YouTubeMediaRenderer,
        pb.media.renderers.DailyMotionMediaRenderer,
        pb.media.renderers.VimeoMediaRenderer,
        pb.media.renderers.VineMediaRenderer,
        pb.media.renderers.InstagramMediaRenderer,
        pb.media.renderers.SlideShareMediaRenderer,
        pb.media.renderers.TrinketMediaRenderer,
        pb.media.renderers.StorifyMediaRenderer,
        pb.media.renderers.KickStarterMediaRenderer,
        pb.media.renderers.PdfMediaRenderer
    ];

    /**
     *
     * @method getContentStreamByPath
     * @param {String} mediaPath
     * @param {Function} cb
     */
    MediaServiceV2.prototype.getContentStreamByPath = function(mediaPath, cb) {
        this.provider.getStream(mediaPath, cb);
    };

    /**
     * @method getContentStreamById
     * @param {string} id
     * @param {Function} cb
     */
    MediaServiceV2.prototype.getContentStreamById = function(id, cb) {
        var self = this;
        var tasks = [
            function(callback) { self.dao.loadById(id, TYPE, callback); },
            function(media, callback) {
                if (!media) {
                    return callback(null, null);
                }
                self.provider.getStream(media.location, function(err, stream) {
                    callback(err, stream ? {
                        stream: stream,
                        mime: media.mime || pb.RequestHandler.getMimeFromPath(media.location)
                    } : null);
                });
            }
        ];
        async.waterfall(tasks, cb);
    };

    /**
     * Renders a resource by type and location (mediaId).
     * @method renderByLocation
     * @param {Object} options
     * @param {String} options.location The unique media identifier for the type
     * @param {String} [options.type] The type of provider that knows how to render
     * the resource
     * @param {Object} [options.attrs] The desired HTML attributes that will be
     * added to the element that provides the rendering
     * @param {Object} [options.style] The desired style overrides for the media
     * @param {String} [options.view] The view type that the media will be rendered
     * for (view, editor, post).  Any style options provided will override those
     * provided by the default style associated with the view.
     * @param {boolean} [options.isFile=false]
     * @param {Function} cb A callback that provides two parameters.  An Error if
     * exists and the rendered HTML content for the media resource.
     */
    MediaServiceV2.prototype.renderByLocation = function(options, cb) {
        var result = options.type ? MediaServiceV2.getRendererByType(options.type) : MediaServiceV2.getRenderer(options.location, options.isFile);
        if (!result) {
            var failures = [ BaseObjectService.validationFailure('type', 'An invalid type was provided') ];
            var err = BaseObjectService.validationError(failures);
            return cb(err, null);
        }

        //set style properties if we can
        if (options.view) {
            options.style = MediaServiceV2.getStyleForView(result.renderer, options.view, options.style);
        }

        result.renderer.render(options, options, cb);
    };

    /**
     * Renders a media resource by ID where ID refers the to the media descriptor
     * id.
     * @method renderById
     * @param {String} id The media resource ID
     * @param {Object} options
     * @param {Object} [options.attrs] The desired HTML attributes that will be
     * added to the element that provides the rendering
     * @param {Object} [options.style] The desired style overrides for the media
     * @param {String} [options.view] The view type that the media will be rendered
     * for (view, editor, post).  Any style options provided will override those
     * provided by the default style associated with the view.
     * @param {Function} cb A callback that provides two parameters. An Error if
     * exists and the rendered HTML content for the media resource.
     */
    MediaServiceV2.prototype.renderById = function(id, options, cb) {
        var self = this;

        this.dao.loadById(id, TYPE, function (err, media) {
            if (util.isError(err) || !media) {
                return cb(err, null);
            }

            //render
            self.render(media, options, cb);
        });
    };

    /**
     * Renders the media represented by the provided media descriptor.
     * @method render
     * @param {object} media The media resource ID
     * @param {string} media.media_type
     * @param {Object} options
     * @param {Object} [options.attrs] The desired HTML attributes that will be
     * added to the element that provides the rendering
     * @param {Object} [options.style] The desired style overrides for the media
     * @param {String} [options.view] The view type that the media will be rendered
     * for (view, editor, post).  Any style options provided will override those
     * provided by the default style associated with the view.
     * @param {Function} cb A callback that provides two parameters. An Error if
     * exists and the rendered HTML content for the media resource.
     */
    MediaServiceV2.prototype.render = function(media, options, cb) {
        //retrieve renderer
        var result = MediaServiceV2.getRendererByType(media.media_type);
        if (!result) {
            return cb(null, null);
        }

        //set style properties if we can
        if (options.view) {
            options.style = MediaServiceV2.getStyleForView(result.renderer, options.view, options.style);
        }

        //render media
        result.renderer.render(media, options, cb);
    };

    /**
     *
     * @method validate
     * @param {Object} context
     * @param {Object} context.data The DTO that was provided for persistence
     * @param {MediaServiceV2} context.service An instance of the service that triggered
     * the event that called this handler
     * @param {Function} cb A callback that takes a single parameter: an error if occurred
     */
    MediaServiceV2.prototype.validate = function(context, cb) {
        var obj = context.data;
        var errors = context.validationErrors;

        //ensure we have a request body
        if (!util.isObject(obj)) {
            errors.push(BaseObjectService.validationFailure('', 'The descriptor must be an object'));
            return cb(null, errors);
        }

        //validate that we have a type
        if (!obj.media_type) {
            //lack of a media type indicates that the content was bad
            errors.push(BaseObjectService.validationFailure('content', 'Invalid content was provided'));
        }

        //validate the caption
        if (!ValidationService.isStr(obj.caption, false)) {
            errors.push(BaseObjectService.validationFailure('caption', 'Caption must be a valid string'));
        }

        //validate other stuff
        var tasks = [
            util.wrapTask(this, this.validateName, [context]),
            util.wrapTask(this, this.validateTopicReferences, [context])
        ];
        async.series(tasks, cb);
    };

    /**
     * Validates any references to topic objects in the data object passed through the context.
     * @method validateTopicReferences
     * @param {object} context
     * @param {object} context.data
     * @param {Array} context.data.media_topics
     * @param {Array} context.validationErrors
     * @param {function} cb (Error)
     */
    MediaServiceV2.prototype.validateTopicReferences = function(context, cb) {
        var obj = context.data;
        var errors = context.validationErrors;

        if (!util.isArray(obj.media_topics)) {
            errors.push(BaseObjectService.validationFailure('media_topics', 'A valid array of topic IDs must be provided'));
            return cb();
        }

        //don't bother check the DB is the array is empty.
        if (obj.media_topics.length === 0) {
            return cb();
        }

        //verify that each topic exists
        var opts = {
            select: {name: 1},
            where: DAO.getIdInWhere(obj.media_topics)
        };
        this.topicService.getAll(opts, function(err, topics) {
            if (util.isError(err)) {
                return cb(err);
            }

            //convert to map
            var map = util.arrayToObj(topics, function(topics, i) {
                return topics[i].id.toString();
            });

            //find the ones that don't exist
            obj.media_topics.forEach(function(item, index) {
                if (!map[item]) {
                    errors.push(BaseObjectService.validationFailure('media_topics[' + index + ']', item + ' is not a valid reference'));
                }
            });

            cb();
        });
    };

    /**
     * @method validateName
     * @param {object} context
     * @param {object} context.data
     * @param {string} context.data.name
     * @param {Array} context.validationErrors
     * @param {function} cb (Error)
     */
    MediaServiceV2.prototype.validateName = function(context, cb) {
        var obj = context.data;
        var errors = context.validationErrors;

        if (!ValidationService.isNonEmptyStr(obj.name, true)) {
            errors.push(BaseObjectService.validationFailure('name', 'The name cannot be empty'));
            return cb();
        }

        //ensure the media name is unique
        var where = { name: obj.name };
        this.dao.unique(TYPE, where, obj[DAO.getIdField()], function(err, isUnique) {
            if(util.isError(err)) {
                return cb(err);
            }
            else if(!isUnique) {
                errors.push(BaseObjectService.validationFailure('name', 'The name ' + obj.name + ' is already in use'));
            }
            cb();
        });
    };

    /**
     * Persists the uploaded media content to the provider
     * @method persistContent
     * @param {object} context
     * @param {object} context.data
     * @param {File} context.data.content Formidable file descriptor
     * @param {function} cb (Error)
     */
    MediaServiceV2.prototype.persistContent = function(context, cb) {
        var obj = context.data;
        var fileDescriptor = obj.content;

        //delete file reference before persistence
        delete obj.content;

        //generate a random media ID
        var stream = fs.createReadStream(fileDescriptor.path);
        this.provider.setStream(stream, obj.location, cb);
    };

    /**
     *
     * @static
     * @method onFormat
     * @param {Object} context
     * @param {object} context.data The incoming request body
     * @param {MediaServiceV2} context.service An instance of the service that triggered
     * the event that called this handler
     * @param {Function} cb A callback that takes a single parameter: an error if occurred
     */
    MediaServiceV2.onFormat = function(context, cb) {
        var dto = context.data;
        dto.name = BaseObjectService.sanitize(dto.name);
        dto.caption = BaseObjectService.sanitize(dto.caption);

        //when media topics are presented as a string then delimit them and convert to an array
        if (ValidationService.isNonEmptyStr(dto.media_topics, true)) {
            dto.media_topics = dto.media_topics.split(',');
        }
        else if (util.isNullOrUndefined(dto.media_topics) || dto.media_topics === '') {
            dto.media_topics = [];
        }
        cb(null);
    };

    /**
     *
     * @static
     * @method onMerge
     * @param {Object} context
     * @param {object} context.data The incoming request body
     * @param {object} context.object The object to be persisted
     * @param {MediaServiceV2} context.service An instance of the service that triggered
     * the event that called this handler
     * @param {Function} cb A callback that takes a single parameter: an error if occurred
     */
    MediaServiceV2.onMerge = function(context, cb) {
        var dto = context.data;
        var obj = context.object;

        obj.name = dto.name;
        obj.caption = dto.caption;
        obj.media_topics = dto.media_topics;

        if (!dto.content) {
            //no content was sent so we should rely on what is already available
            return cb();
        }

        //check to see if we have a file handle
        var mediaUrl;
        if ( (obj.isFile = util.isObject(dto.content)) ) {

            //ensure the content is available for validation
            obj.content = dto.content;
            obj.meta = obj.meta || {};
            obj.meta.mime = obj.content.type;
            obj.meta.fileName = obj.content.name;
            obj.meta.fileSize = obj.content.size;
            mediaUrl = MediaServiceV2.generateMediaPath(obj.content.name);
        }
        else if (util.isString(dto.content)) {
            //we were given a link, clear out any old refs to a file based media obj
            mediaUrl = dto.content;
            obj.meta = {};
        }

        //determine type
        var result = MediaServiceV2.getRenderer(mediaUrl, obj.isFile);
        if (!result) {
            obj.location = null;
            obj.media_type = null;
            return cb();
        }

        var renderer = result.renderer;
        var tasks = {

            meta: function(callback) {
                renderer.getMeta(mediaUrl, obj.isFile, callback);
            },

            thumbnail: function(callback) {
                renderer.getThumbnail(mediaUrl, callback);
            },

            mediaId: function(callback) {
                renderer.getMediaId(mediaUrl, callback);
            }
        };
        async.series(tasks, function(err, taskResult) {
            obj.media_type = result.type;
            if (taskResult) {
                obj.icon = renderer.getIcon(result.type);
                obj.thumbnail = taskResult.thumbnail;
                obj.location = taskResult.mediaId;
                util.merge(taskResult.meta, obj.meta);
            }
            cb(err);
        });
    };

    /**
     *
     * @static
     * @method onValidate
     * @param {Object} context
     * @param {Object} context.data The DTO that was provided for persistence
     * @param {MediaServiceV2} context.service An instance of the service that triggered
     * the event that called this handler
     * @param {Function} cb A callback that takes a single parameter: an error if occurred
     */
    MediaServiceV2.onValidate = function(context, cb) {
        context.service.validate(context, cb);
    };

    /**
     * @static
     * @method onBeforeSave
     * @param {object} context
     * @param {object} context.data The media descriptor object
     * @param {MediaServiceV2} context.service
     * @param {function} cb (Error)
     */
    MediaServiceV2.onBeforeSave = function(context, cb) {
        var obj = context.data;
        if (obj.content) {
            return context.service.persistContent(context, cb);
        }
        cb();
    };

    /**
     * Retrieves a media renderer for the specified URL
     * @static
     * @method getRendererByType
     * @param {String} mediaUrl The media URL
     * @param {Boolean} [isFile=false] TRUE if the URL represents an uploaded file, FALSE if not
     * @return {object|null} A media renderer interface implementation or NULL if
     * none support the given URL.
     */
    MediaServiceV2.getRenderer = function(mediaUrl, isFile) {
        if (typeof isFile === 'undefined') {
            isFile = MediaServiceV2.isFile(mediaUrl);
        }

        for (var i = 0; i < REGISTERED_MEDIA_RENDERERS.length; i++) {

            var t = REGISTERED_MEDIA_RENDERERS[i].getType(mediaUrl, isFile);
            if (t !== null) {

                pb.log.silly('MediaService: Selected media renderer [%s] for URI [%s]', REGISTERED_MEDIA_RENDERERS[i].getName(), mediaUrl);
                return {
                    type: t,
                    renderer: REGISTERED_MEDIA_RENDERERS[i]
                };
            }
        }

        pb.log.warn('MediaServiceV2: Failed to select media renderer URI [%s]', mediaUrl);
        return null;
    };

    /**
     * Retrieves a media renderer for the specified type
     * @static
     * @method getRendererByType
     * @param {String} type The media type
     * @return {object|null} A media renderer interface implementation or NULL if
     * none support the given type.
     */
    MediaServiceV2.getRendererByType = function(type) {
        for (var i = 0; i < REGISTERED_MEDIA_RENDERERS.length; i++) {

            var types = REGISTERED_MEDIA_RENDERERS[i].getSupportedTypes();
            if (types && types[type]) {

                pb.log.silly('MediaService: Selected media renderer [%s] for type [%s]', REGISTERED_MEDIA_RENDERERS[i].getName(), type);
                return {
                    type: type,
                    renderer: REGISTERED_MEDIA_RENDERERS[i]
                };
            }
        }

        pb.log.warn('MediaServiceV2: Failed to select media renderer type [%s]', type);
        return null;
    };

    /**
     * Generates a media placeholder for templating
     * @static
     * @method getMediaFlag
     * @param {String} mid The media descriptor ID
     * @param {Object} [options] The list of attributes to be provided to the
     * rendering element.
     * @return {String}
     */
    MediaServiceV2.getMediaFlag = function(mid, options) {
        if (!mid) {
            throw new Error('The media id is required but ['+mid+'] was provided');
        }
        else if (!util.isObject(options)) {
            options = {};
        }

        var flag = '^media_display_'+mid+'/';

        var cnt = 0;
        Object.keys(options).forEach(function(opt) {
            if (cnt++ > 0) {
                flag += ',';
            }
            flag += opt + ':' + options[opt];
        });
        flag += '^';
        return flag;
    };

    /**
     * Given a content string the function will search for and extract the first
     * occurrence of a media flag. The parsed value that is returned will include:
     * <ul>
     * <li>startIndex - The index where the flag was found to start</li>
     * <li>endIndex - The position in the content string of the last character of the media flag</li>
     * <li>flag - The entire media flag including the start and end markers</li>
     * <li>id - The media descriptor id that is referenced by the media flag</li>
     * <li>style - A hash of the style properties declared for the flag</li>
     * <li>cleanFlag - The media flag stripped of the start and end markers</li>
     * </ul>
     * @static
     * @method extractNextMediaFlag
     * @param {String} content The content string that potentially contains 1 or more media flags
     * @return {Object} An object that contains the information about the parsed media flag.
     */
    MediaServiceV2.extractNextMediaFlag = function(content) {
        if (!util.isString(content)) {
            return null;
        }

        //ensure a media tags exists
        var prefix = '^media_display_';
        var startIndex = content.indexOf(prefix);
        if (startIndex < 0) {
            return null;
        }

        //ensure media tag is properly terminated
        var endIndex = content.indexOf('^', startIndex + 1);
        if (endIndex < 0) {
            return null;
        }

        var flag   = content.substring(startIndex, endIndex + 1);
        var result = MediaServiceV2.parseMediaFlag(flag);
        if (result) {
            result.startIndex = startIndex;
            result.endIndex = endIndex;
            result.flag = flag;
        }
        return result;
    };

    /**
     * Parses a media flag and returns each part in an object. The parsed value that
     * is returned will include:
     * <ul>
     * <li>id - The media descriptor id that is referenced by the media flag</li>
     * <li>style - A hash of the style properties declared for the flag</li>
     * <li>cleanFlag - The media flag stripped of the start and end markers</li>
     * </ul>
     * @static
     * @method parseMediaFlag
     * @param {String} flag The content string that potentially contains 1 or more media flags
     * @return {Object} An object that contains the information about the parsed media flag.
     */
    MediaServiceV2.parseMediaFlag = function(flag) {
        if (!util.isString(flag)) {
            return null;
        }

        //strip flag start and end markers if exist
        var hasStartMarker = flag.charAt(0) === '^';
        var hasEndMarker   = flag.charAt(flag.length - 1) === '^';
        flag = flag.substring(hasStartMarker ? 1 : 0, hasEndMarker ? flag.length - 1 : undefined);

        //split on forward slash as it is the division between id and style
        var prefix = 'media_display_';
        var parts = flag.split('/');
        var id    = parts[0].substring(prefix.length);

        var style = {};
        if (parts[1] && parts[1].length) {
            parts[1].split(',').forEach(function(item) {
                var division = item.split(':');
                style[division[0]] = division[1];
            });
        }

        return {
            id: id,
            style: style,
            cleanFlag: flag
        };
    };

    /**
     * Provides a mechanism to retrieve all of the supported extension types
     * that can be uploaded into the system.
     * @static
     * @method getSupportedExtensions
     * @return {Array} provides an array of strings
     */
    MediaServiceV2.getSupportedExtensions = function() {

        var extensions = {};
        REGISTERED_MEDIA_RENDERERS.forEach(function(provider) {

            //for backward compatibility check for existence of extension retrieval
            if (!util.isFunction(provider.getSupportedExtensions)) {
                pb.log.warn('MediaServiceV2: Renderer %s does provide an implementation for getSupportedExtensions', provider.getName());
                return;
            }

            //retrieve the extensions
            var exts = provider.getSupportedExtensions();
            if (!util.isArray(exts)) {
                return;
            }

            //add them to the hash
            exts.forEach(function(extension) {
                extensions[extension] = true;
            });
        });

        return Object.keys(extensions);
    };

    /**
     * Registers a media renderer
     * @static
     * @method registerRenderer
     * @param {Function|Object} interfaceImplementation A prototype or object that implements the media renderer interface.
     * @return {Boolean} TRUE if the implementation was registered, FALSE if not
     */
    MediaServiceV2.registerRenderer = function(interfaceImplementation) {
        if (!interfaceImplementation) {
            return false;
        }

        REGISTERED_MEDIA_RENDERERS.push(interfaceImplementation);
        return true;
    };

    /**
     * Indicates if a media renderer is already registered
     * @static
     * @method isRegistered
     * @param {Function|Object} interfaceImplementation A prototype or object that implements the media renderer interface
     * @return {Boolean} TRUE if registered, FALSE if not
     */
    MediaServiceV2.isRegistered = function(interfaceImplementation) {
        return REGISTERED_MEDIA_RENDERERS.indexOf(interfaceImplementation) >= 0;
    };

    /**
     * Unregisters a media renderer
     * @static
     * @method unregisterRenderer
     * @param {Function|Object} interfaceToUnregister A prototype or object that implements the media renderer interface
     * @return {Boolean} TRUE if unregistered, FALSE if not
     */
    MediaServiceV2.unregisterRenderer = function(interfaceToUnregister) {
        var index = REGISTERED_MEDIA_RENDERERS.indexOf(interfaceToUnregister);
        if (index >= 0) {
            REGISTERED_MEDIA_RENDERERS.splice(index, 1);
            return true;
        }
        return false;
    };

    /**
     *
     * @static
     * @method loadMediaProvider
     * @param {Object} context
     * @param {String} context.site
     * @return {MediaProvider} An instance of a media provider or NULL when no
     * provider can be loaded.
     */
    MediaServiceV2.loadMediaProvider = function(context) {
        var ProviderType = MEDIA_PROVIDERS[pb.config.media.provider];
        if (util.isNullOrUndefined(ProviderType)) {
            ProviderType = MediaServiceV2.findProviderType();
        }
        return !!ProviderType ? new ProviderType(context) : null;
    };

    /**
     * Looks up the prototype for the media provider based on the configuration
     * @static
     * @method findProviderType
     * @return {MediaProvider}
     */
    MediaServiceV2.findProviderType = function() {
        var paths = [path.join(pb.config.docRoot, pb.config.media.provider), pb.config.media.provider];
        for(var i = 0; i < paths.length; i++) {
            try{
                return require(paths[i])(pb);
            }
            catch(e){
                pb.log.silly(e.stack);
            }
        }
        return null;
    };

    /**
     * The default editor implementations all for three position values to declared
     * for embedded media (none, left, right, center).  These values map to HTML
     * alignments.  This function retrieves the HTML style attribute for the
     * provided position.
     * @static
     * @method getStyleForPosition
     * @param {String} position Can be one of 4 values: none, left, right, center
     * @return {String} The HTML formatted style attribute(s)
     */
    MediaServiceV2.getStyleForPosition = function(position) {
        var positionToStyle = {
            left: 'float: left;margin-right: 1em',
            right: 'float: right;margin-left: 1em',
            center: 'text-align: center'
        };
        return positionToStyle[position] || '';
    };

    /**
     * Generates the path to uploaded media
     * @static
     * @method generateMediaPath
     * @param {String} originalFilename
     * @return {String}
     */
    MediaServiceV2.generateMediaPath = function(originalFilename) {
        var now = new Date();
        var filename  = MediaServiceV2.generateFilename(originalFilename);
        return pb.UrlService.urlJoin('/media', now.getFullYear() + '', (now.getMonth() + 1) + '', filename);
    };

    /**
     * Generates a filename for a new media object
     * @static
     * @method generateFilename
     * @param {String} originalFilename
     * @return {String}
     */
    MediaServiceV2.generateFilename = function(originalFilename){
        var now = new Date();

        //calculate extension
        var ext = '';
        var extIndex = originalFilename.lastIndexOf('.');
        if (extIndex >= 0){
            ext = originalFilename.substr(extIndex);
        }

        //build file name
        return util.uniqueId() + '-' + now.getTime() + ext;
    };

    /**
     * Retrieves the font awesome icon for the media type.
     * @static
     * @method getMediaIcon
     * @param {String} mediaType
     * @return {String}
     */
    MediaServiceV2.getMediaIcon = function(mediaType) {

        var result = MediaServiceV2.getRendererByType(mediaType);
        if (!result) {
            return '';
        }
        return result.renderer.getIcon(mediaType);
    };

    /**
     * Sets the proper icon and link for an array of media items
     * @static
     * @method formatMedia
     * @param {Array} media The array of media objects to format
     * @return {Array} The same array of media that was passed in
     */
    MediaServiceV2.formatMedia = function(media) {
        var quickLookup = {};
        media.forEach(function(item) {

            //get the renderer
            var renderer = quickLookup[item.media_type];
            if (!renderer) {
                var result = MediaServiceV2.getRendererByType(item.media_type);
                if (!result) {
                    pb.log.warn('MediaService: Media item [%s] contains an unsupported media type.', media[pb.DAO.getIdField()]);
                }
                else {
                    quickLookup[item.media_type] = renderer = result.renderer;
                }
            }

            item.icon = renderer ? renderer.getIcon(item.media_type) : 'question';
            item.link = renderer ? renderer.getNativeUrl(item) : '#';
        });
        return media;
    };

    /**
     * Retrieves the base style for the given renderer and view.  Overrides will be
     * applied on top of the base style.
     * @static
     * @method getStyleForView
     * @param {MediaRenderer} renderer An implementation of MediaRenderer
     * @param {String} view The view to retrieve the default styling for (view,
     * editor, post)
     * @param {Object} [overrides] A hash of style properties that will be applied
     * to the base style for the given view
     */
    MediaServiceV2.getStyleForView = function(renderer, view, overrides) {
        if (!overrides) {
            overrides = {};
        }

        var base = renderer.getStyle(view);
        var clone = util.clone(base);
        util.merge(overrides, clone);
        return clone;
    };

    /**
     * Determines if the media URI is a file.  It is determined to be a file if and
     * only if the URI does not begin with "http" or "//".
     * @static
     * @method isFile
     * @param {String} mediaUrl A URI string that points to a media resource
     */
    MediaServiceV2.isFile = function(mediaUrl) {
        return !(mediaUrl.indexOf('http') === 0 || mediaUrl.indexOf('//') === 0);
    };

    //Event Registries
    BaseObjectService.on(TYPE + '.' + BaseObjectService.FORMAT, MediaServiceV2.onFormat);
    BaseObjectService.on(TYPE + '.' + BaseObjectService.MERGE, MediaServiceV2.onMerge);
    BaseObjectService.on(TYPE + '.' + BaseObjectService.VALIDATE, MediaServiceV2.onValidate);
    BaseObjectService.on(TYPE + '.' + BaseObjectService.BEFORE_SAVE, MediaServiceV2.onBeforeSave);

    //exports
    return MediaServiceV2;
};