Skip to content

Commit

Permalink
#32: File constraints now available on both client and server, added …
Browse files Browse the repository at this point in the history
…client side validation
  • Loading branch information
Pagebakers committed Dec 29, 2014
1 parent ffa2209 commit f784f3f
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 116 deletions.
4 changes: 3 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@

//Internals
"S3Policy": false,
"Slingshot": true
"Slingshot": true,
"matchAllowedFileTypes": false,
"mixins": false
}
}
134 changes: 26 additions & 108 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,9 +73,12 @@ 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));
new Slingshot.Directive(name, service, options));
};

/**
Expand All @@ -95,18 +91,17 @@ Slingshot.getDirective = function (name) {
return this._directives[name];
};


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

/**
* @param {Object} service
* @param {Directive} directive
* @constructor
*/

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


check(name, String);

//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 +117,13 @@ Slingshot.Directive = function (service, directive) {
cacheControl: Match.Optional(String),
contentDisposition: Match.Optional(Match.OneOf(String, null))
}, service.directiveMatch));


/**
* @public
* @property {String} The directive name
*/
this.name = name;

/**
* @method storageService
* @returns {Object}
Expand All @@ -140,80 +141,7 @@ Slingshot.Directive = function (service, directive) {
this._directive = 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;
},
_.extend(Slingshot.Directive.prototype, mixins, {

/**
* @param {{userId: String}} method
Expand All @@ -238,6 +166,16 @@ _.extend(Slingshot.Directive.prototype, {
});

return instructions;
},

/**
* @param {String} restriction - Name of the restriction to retrieve
* @returns {mixed} The restriction configuration
*/

getRestriction: function (restriction) {
return Slingshot.getRestrictions(this.name)[restriction] ||
this._directive[restriction];
}
});

Expand Down Expand Up @@ -284,26 +222,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;
}
140 changes: 140 additions & 0 deletions lib/restrictions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* @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),
});

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] || {};
};

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

requestAuthorization: function (method, file, meta) {
var authorize = this.getRestriction("authorize");
return this.checkFileSize(file.size) && this.checkFileType(file.type) &&
(typeof authorize !== 'function' || 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.getRestriction("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.getRestriction("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;
}
};

/** 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

0 comments on commit f784f3f

Please sign in to comment.