Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#32: File constraints now available on both client and server #35

Merged
merged 11 commits into from
Jan 4, 2015
3 changes: 2 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@

//Internals
"S3Policy": false,
"Slingshot": true
"Slingshot": true,
"matchAllowedFileTypes": false
}
}
134 changes: 29 additions & 105 deletions lib/directive.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
/**
* @module meteor-slingshot
*/

Slingshot = {};


/**
* @callback Directive~authorize
*
Expand Down Expand Up @@ -80,6 +73,9 @@ Slingshot._directives = {};
Slingshot.createDirective = function (name, service, options) {
if (_.has(Slingshot._directives, name))
throw new Error("Directive '" + name + "' already exists");

var restrictions = Slingshot.getRestrictions(name);
_.defaults(options, restrictions);

return (Slingshot._directives[name] =
new Slingshot.Directive(service, options));
Expand All @@ -95,9 +91,6 @@ Slingshot.getDirective = function (name) {
return this._directives[name];
};


var matchAllowedFileTypes = Match.OneOf(String, [String], RegExp, null);

/**
* @param {Object} service
* @param {Directive} directive
Expand All @@ -106,7 +99,7 @@ var matchAllowedFileTypes = Match.OneOf(String, [String], RegExp, null);

Slingshot.Directive = function (service, directive) {
check(this, Slingshot.Directive);

//service does not have to be a plain-object, so checking fields individually
check(service.directiveMatch, Object);
check(service.upload, Function);
Expand All @@ -122,7 +115,7 @@ Slingshot.Directive = function (service, directive) {
cacheControl: Match.Optional(String),
contentDisposition: Match.Optional(Match.OneOf(String, null))
}, service.directiveMatch));

/**
* @method storageService
* @returns {Object}
Expand All @@ -142,79 +135,6 @@ Slingshot.Directive = function (service, directive) {

_.extend(Slingshot.Directive.prototype, {

/**
*
* @method requestAuthorization
*
* @throws Meteor.Error
*
* @param {FileInfo} file
* @param {Object} [meta]
*
* @returns {Boolean}
*/

requestAuthorization: function (method, file, meta) {
return this.checkFileSize(file.size) && this.checkFileType(file.type) &&
this._directive.authorize.call(method, file, meta);
},

/**
* @throws Meteor.Error
*
* @param {Number} size - Size of file in bytes.
* @returns {boolean}
*/

checkFileSize: function (size) {
var maxSize = Math.min(this._directive.maxSize,
this.storageService().maxSize || Infinity);

if (maxSize && size > maxSize)
throw new Meteor.Error("Upload denied", "File exceeds allowed size of " +
formatBytes(maxSize));

return true;
},

/**
*
* @throws Meteor.Error
*
* @param {String} type - Mime type
* @returns {boolean}
*/

checkFileType: function (type) {
var allowed = this._directive.allowedFileTypes;

if (allowed instanceof RegExp) {

if (!allowed.test(type))
throw new Meteor.Error("Upload denied",
type + " is not an allowed file type");

return true;
}

if (_.isArray(allowed)) {
if (allowed.indexOf(type) < 0) {
throw new Meteor.Error("Upload denied",
type + " is not one of the followed allowed file types: " +
allowed.join(", "));
}

return true;
}

if (allowed !== type) {
throw new Meteor.Error("Upload denied", "Only file of type " + allowed +
" can be uploaded");
}

return true;
},

/**
* @param {{userId: String}} method
* @param {FileInfo} file
Expand All @@ -238,7 +158,31 @@ _.extend(Slingshot.Directive.prototype, {
});

return instructions;
},

/**
*
* @method requestAuthorization
*
* @throws Meteor.Error
*
* @param {Object} context
* @param {FileInfo} file
* @param {Object} [meta]
* @param {Function} [authorize]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this param doc.

*
* @returns {Boolean}
*/

requestAuthorization: function (context, file, meta) {
var validators = Slingshot.Validators,
restrictions = _.pick(this._directive,
['authorize', 'maxSize', 'allowedFileTypes']
);

return validators.authorize(context, file, meta, restrictions);
}

});

Meteor.methods({
Expand Down Expand Up @@ -284,26 +228,6 @@ Meteor.methods({
}
});


/** Human readable data-size in bytes.
*
* @param size {Number}
* @returns {string}
*/

function formatBytes(size) {
var units = ['Bytes', 'KB', 'MB', 'GB', 'TB'],
unit = units.shift();

while (size >= 0x400 && units.length) {
size /= 0x400;
unit = units.shift();
}

return (Math.round(size * 100) / 100) + " " + unit;
}


function quoteString(string, quotes) {
return quotes + string.replace(quotes, '\\' + quotes) + quotes;
}
150 changes: 150 additions & 0 deletions lib/restrictions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* @module meteor-slingshot
*/

Slingshot = {};

/* global matchAllowedFileTypes: true */
matchAllowedFileTypes = Match.OneOf(String, [String], RegExp, null);

/**
* List of configured restrictions by name.
*
* @type {Object.<String, Function>}
* @private
*/

Slingshot._restrictions = {};

/**
* Creates file upload restrictions for a specific directive.
*
* @param {string} name - A unique identifier of the directive.
* @param {Object} restrictions - The file upload restrictions.
* @returns {Object}
*/

Slingshot.fileRestrictions = function (name, restrictions) {
check(restrictions, {
authorize: Match.Optional(Function),
maxSize: Match.Optional(Match.OneOf(Number, null)),
allowedFileTypes: Match.Optional(matchAllowedFileTypes),
});

if (Meteor.isServer) {
var directive = Slingshot.getDirective(name);
if (directive) {
_.extend(directive._directive, restrictions);
}
}

return (Slingshot._restrictions[name] =
_.extend(Slingshot._restrictions[name] || {}, restrictions));
};

/**
* @param {string} name - The unique identifier of the directive to
* retrieve the restrictions for.
* @returns {Object}
*/

Slingshot.getRestrictions = function (name) {
return this._restrictions[name] || {};
};

Slingshot.Validators = {

/**
*
* @method authorize
*
* @throws Meteor.Error
*
* @param {Object} context
* @param {FileInfo} file
* @param {Object} [meta]
* @param {Object} [restrictions]
*
* @returns {Boolean}
*/

authorize: function (context, file, meta, restrictions) {
return this.checkFileSize(file.size, restrictions.maxSize) &&
this.checkFileType(file.type, restrictions.allowedFileTypes) &&
(typeof restrictions.authorize !== 'function' ||
restrictions.authorize.call(context, file, meta));
},

/**
* @throws Meteor.Error
*
* @param {Number} size - Size of file in bytes.
* @param {Number} maxSize - Max size of file in bytes.
* @returns {boolean}
*/

checkFileSize: function (size, maxSize) {
maxSize = Math.min(maxSize, Infinity);

if (maxSize && size > maxSize)
throw new Meteor.Error("Upload denied", "File exceeds allowed size of " +
formatBytes(maxSize));

return true;
},

/**
*
* @throws Meteor.Error
*
* @param {String} type - Mime type
* @param {RegExp,Array,String} allowed - Allowed file type(s)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use {(number|boolean)} syntax.

* @returns {boolean}
*/

checkFileType: function (type, allowed) {
if (allowed instanceof RegExp) {

if (!allowed.test(type))
throw new Meteor.Error("Upload denied",
type + " is not an allowed file type");

return true;
}

if (_.isArray(allowed)) {
if (allowed.indexOf(type) < 0) {
throw new Meteor.Error("Upload denied",
type + " is not one of the followed allowed file types: " +
allowed.join(", "));
}

return true;
}

if (allowed !== type) {
throw new Meteor.Error("Upload denied", "Only file of type " + allowed +
" can be uploaded");
}

return true;
}
};

/** Human readable data-size in bytes.
*
* @param size {Number}
* @returns {string}
*/

function formatBytes(size) {
var units = ['Bytes', 'KB', 'MB', 'GB', 'TB'],
unit = units.shift();

while (size >= 0x400 && units.length) {
size /= 0x400;
unit = units.shift();
}

return (Math.round(size * 100) / 100) + " " + unit;
}
Loading