API Docs for: 0.8.0
Show:

File: include/service/media/fs_media_provider.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 os    = require('os');
var fs    = require('fs');
var path  = require('path');
var async = require('async');
var util  = require('../../util.js');

module.exports = function FsMediaProviderModule(pb) {

    /**
     * A media provider that uses the underlying file system as the method of
     * storage.
     * @class FsMediaProvider
     * @constructor
     * @param {Object} context
     * @param {String} [context.parentDir]
     * @param {String} context.site
     */
    function FsMediaProvider(context) {
        this.parentDir = context.parentDir || pb.config.media.parent_dir;
    }

    /**
     * Retrieves the item on the file system as a stream.
     * @method getStream
     * @param {String} mediaPath The path/key to the media.  Typically this is a
     * path such as: /media/2014/9/540a3ff0e30ddfb9e60000be-1409957872680.jpg
     * @param {Object} [options] Options for interacting with S3
     * @param {String} [options.bucket] The S3 bucket to interact with
     * @param {Function} cb A callback that provides two parameters: An Error, if
     * occurred and a ReadableStream that contains the media content.
     */
    FsMediaProvider.prototype.getStream = function(mediaPath, cb) {
        var ap = FsMediaProvider.getMediaPath(this.parentDir, mediaPath);
        cb(null, fs.createReadStream(ap));
    };

    /**
     * Retrieves the content from the file system as a String or Buffer.
     * @method get
     * @param {String} mediaPath The path/key to the media.  Typically this is a
     * path such as: /media/2014/9/540a3ff0e30ddfb9e60000be-1409957872680.jpg
     * @param {Function} cb A callback that provides two parameters: An Error, if
     * occurred and an entity that contains the media content.
     */
    FsMediaProvider.prototype.get = function(mediaPath, cb) {
        var ap = FsMediaProvider.getMediaPath(this.parentDir, mediaPath);
        fs.readFile(ap, cb);
    };

    /**
     * Sets media content into the file system based on the specified media path and
     * options.  The stream provided must be a ReadableStream.
     * @method setStream
     * @param {ReadableStream} stream The content stream
     * @param {String} mediaPath The path/key to the media.  Typically this is a
     * path such as: /media/2014/9/540a3ff0e30ddfb9e60000be-1409957872680.jpg
     * @param {Function} cb A callback that provides two parameters: An Error, if
     * occurred and the success of the operation.
     */
    FsMediaProvider.prototype.setStream = function(stream, mediaPath, cb) {

        this.createWriteStream(mediaPath, function(err, fileStream) {
            if (util.isError(err)) {
                return cb(err);
            }

            pb.log.silly('FsMediaProvider: Piping stream to [%s]', mediaPath);
            stream.pipe(fileStream);
            stream.on('end', cb);
            stream.on('error', cb);
        });
    };

    /**
     * Sets media content into an file system based on the specified media path and
     * options.  The data must be in the form of a String or Buffer.
     * @method setStream
     * @param {String|Buffer} fileDataStrOrBuffOrStream The content to persist
     * @param {String} mediaPath The path/key to the media.  Typically this is a
     * path such as: /media/2014/9/540a3ff0e30ddfb9e60000be-1409957872680.jpg
     * @param {Function} cb A callback that provides two parameters: An Error, if
     * occurred and the success of the operation.
     */
    FsMediaProvider.prototype.set = function(fileDataStrOrBuff, mediaPath, cb) {
        var ap = FsMediaProvider.getMediaPath(this.parentDir, mediaPath);
        this.mkdirs(ap, function(err) {
            if (util.isError(err)) {
                return cb(err);
            }
            fs.writeFile(ap, fileDataStrOrBuff, cb);
        });
    };

    /**
     * Creates a writable stream to a file with the specified path.  The resource
     * is overwritten if already exists.
     * @method createWriteStream
     * @param {String} mediaPath The path/key to the media.  Typically this is a
     * path such as: /media/2014/9/540a3ff0e30ddfb9e60000be-1409957872680.jpg
     * @param {Function} cb A callback that provides two parameters: An Error, if
     * occurred and a WriteableStream.
     */
    FsMediaProvider.prototype.createWriteStream = function(mediaPath, cb) {
        var ap = FsMediaProvider.getMediaPath(this.parentDir, mediaPath);
        this.mkdirs(ap, function(err) {
            if(util.isError(err)) {
                return cb(err);
            }

            try {
                cb(null, fs.createWriteStream(ap));
            }
            catch(e) {
                cb(e);
            }
        });
    };

    /**
     * Checks to see if the file actually exists on disk
     * @method exists
     * @param {String} mediaPath The path/key to the media.  Typically this is a
     * path such as: /media/2014/9/540a3ff0e30ddfb9e60000be-1409957872680.jpg
     * @param {Function} cb A callback that provides two parameters: An Error, if
     * occurred and a Boolean.
     */
    FsMediaProvider.prototype.exists = function(mediaPath, cb) {
        var ap = FsMediaProvider.getMediaPath(this.parentDir, mediaPath);
        fs.exists(ap, function(exists) {
            cb(null, exists);
        });
    };

    /**
     * Deletes a file from the file system
     * @method delete
     * @param {String} mediaPath The path/key to the media.  Typically this is a
     * path such as: /media/2014/9/540a3ff0e30ddfb9e60000be-1409957872680.jpg
     * @param {Function} cb A callback that provides two parameters: An Error, if
     * occurred and the success of the operation.
     */
    FsMediaProvider.prototype.delete = function(mediaPath, cb) {
        var ap = FsMediaProvider.getMediaPath(this.parentDir, mediaPath);
        fs.exists(ap, function(exists) {
            fs.unlink(ap, cb);
        });
    };

    /**
     * Retrieve the stats on the file
     * @method stat
     * @param {String} mediaPath The path/key to the media.  Typically this is a
     * path such as: /media/2014/9/540a3ff0e30ddfb9e60000be-1409957872680.jpg
     * @param {Function} cb A callback that provides two parameters: An Error, if
     * occurred and an object that contains the file stats
     */
    FsMediaProvider.prototype.stat = function(mediaPath, cb) {
        var ap = FsMediaProvider.getMediaPath(this.parentDir, mediaPath);
        fs.stat(ap, cb);
    };

    /**
     * Recursively creates the directory structure based on the absolute file path
     * provided.
     * @method mkdirs
     * @param {String} absoluteFilePath
     * @param {Function} A callback that provides two parameters: Error, if
     * occurred and result of the attempt at the creation of each directory in the
     * path.
     */
    FsMediaProvider.prototype.mkdirs = function(absoluteFilePath, cb) {

        var pieces = absoluteFilePath.split(path.sep);
        pb.log.silly('FsMediaProvider: Ensuring directories exist for path: %s', absoluteFilePath);

        var curr      = '';
        var isWindows = os.type().toLowerCase().indexOf('windows') !== -1;
        var tasks     = util.getTasks(pieces, function(pieces, i) {
            return function(callback) {

                //we need to skip the first one bc it will probably be empty and we
                //want to skip the last one because it will probably be the file
                //name not a directory.
                var p = pieces[i];
                if (p.length === 0 || i >= pieces.length - 1) {
                    return callback();
                }

                curr += (isWindows && i === 0 ? '' : path.sep) + p;
                fs.exists(curr, function(exists) {
                    if (exists) {
                        pb.log.silly('FsMediaProvider: Skipping creation of [%s] because it already exists', curr);
                        return callback();
                    }

                    pb.log.silly('FsMediaProvider: Creating directory [%s]', curr);
                    fs.mkdir(curr, callback);
                });
            };
        });
        async.series(tasks, cb);
    };

    /**
     * Generates an absolute path based on the parent directory and media path.
     * The parent directory is expected to a single directory or set of directories
     * nested under the pb.config.docRoot.
     * @static
     * @method getMediaPath
     * @param {String} parentDir
     * @param {String} mediaPath
     * @return {String} Absolute path to the resource
     */
    FsMediaProvider.getMediaPath = function(parentDir, mediaPath) {

        var absolutePath = '';
        if (parentDir.indexOf('/') !== 0) {

            //we have a relative path meant to be from the project directory
            absolutePath = path.join(pb.config.docRoot, parentDir, mediaPath);
        }
        else {
            absolutePath = path.join(parentDir, mediaPath);
        }
        return absolutePath;
    };

    //exports
    return FsMediaProvider;
};