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
}
}
63 changes: 51 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,36 @@ Slingshot.createDirective("myFileUploads", Slingshot.S3Storage, {
This directive will not allow any files other than images to be uploaded. The
policy is directed by the meteor app server and enforced by AWS S3.

## Client side validation

On both client and server side we declare file restrictions for our directive:

```Javascript
Slingshot.fileRestrictions("myFileUploads", {
allowedFileTypes: ["image/png", "image/jpeg", "image/gif"],
maxSize: 1*0x400*0x400, //1MB,
authorize: function() {
return this.userId
}
});
```

Now Slingshot will validate the file before sending the authorization request to the server.


### Manual validation
```JavaScript
var uploader = new Slingshot.Upload("myFileUploads");

var error = uploader.validate(document.getElementById('input').files[0]);
if (error) {
console.error(error);
}
```

The validate method will return `null` if valid and returns an `Error instance` if validation fails.


## Storage services

The client side is agnostic to which storage service is used. All it
Expand Down Expand Up @@ -213,27 +243,26 @@ Meteor core packages:

### Directives

`authorize`: Function (required) - Function to determines if upload is allowed.
`authorize`: Function (**required** unless set in File Restrictions)

`maxSize`: Number (required) - Maximum file-size (in bytes). Use `null` or `0`
for unlimited.
`maxSize`: Number (**required** unless set in File Restrictions)

`allowedFileTypes` RegExp, String or Array (required) - Allowed MIME types. Use
null for any file type.
`allowedFileTypes` RegExp, String or Array (**required** unless set in File
Restrictions)

`cacheControl` String (optional) - RFC 2616 Cache-Control directive

`contentDisposition` String (required) - RFC 2616 Content-Disposition directive.
`contentDisposition` String (**required**) - RFC 2616 Content-Disposition directive.
Default is the uploaded file's name (inline). Use null to disable.

`bucket` String (required) - Name of bucket to use. Google Cloud it default is
`bucket` String (**required**) - Name of bucket to use. Google Cloud it default is
`Meteor.settings.GoogleCloudBucket`. For AWS S3 the default bucket is
`Meteor.settings.S3Bucket`.

`domain` String (optional) - Override domain to use to access bucket. Useful for
CDN.

`key` String or Function (required) - Name of the file on the cloud storage
`key` String or Function (**required**) - Name of the file on the cloud storage
service. If a function is provided, it will be called with `userId` in the
context and its return value is used as the key.

Expand All @@ -242,14 +271,24 @@ authorization will expire after the request was made. Default is 5 minutes.

`acl` String (optional)

`AWSAccessKeyId` String (required for AWS S3) - Can also be set in
`AWSAccessKeyId` String (**required** for AWS S3) - Can also be set in
`Meteor.settings`

`AWSSecretAccessKey` String (required for AWS S3) - Can also be set in
`AWSSecretAccessKey` String (**required** for AWS S3) - Can also be set in
`Meteor.settings`

`GoogleAccessId` String (required for Google Cloud Storage) - Can also be set in
`GoogleAccessId` String (**required** for Google Cloud Storage) - Can also be set in
`Meteor.settings`

`GoogleSecretKey` String (required for Google Cloud Storage) - Can also be set
`GoogleSecretKey` String (**required** for Google Cloud Storage) - Can also be set
in `Meteor.settings`

### File restrictions

`authorize`: Function (optional) - Function to determines if upload is allowed.

`maxSize`: Number (optional) - Maximum file-size (in bytes). Use `null` or `0`
for unlimited.

`allowedFileTypes` RegExp, String or Array (optional) - Allowed MIME types. Use
null for any file type.
129 changes: 26 additions & 103 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 @@ -81,6 +74,9 @@ 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 Down Expand Up @@ -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,30 @@ _.extend(Slingshot.Directive.prototype, {
});

return instructions;
},

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

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

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

});

Meteor.methods({
Expand Down Expand Up @@ -284,26 +227,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;
}
53 changes: 53 additions & 0 deletions lib/restrictions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @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] || {};
};
Loading