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
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));
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this API change can be avoided, if the we mix in the file restrictions into options before constructing the directive.

};

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