Skip to content

Commit

Permalink
Merge pull request #35 from CulturalMe/32_preload_file_constrains
Browse files Browse the repository at this point in the history
#32: File constraints now available on both client and server
  • Loading branch information
gsuess committed Jan 4, 2015
2 parents ffa2209 + 050dd59 commit 8f3563b
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 122 deletions.
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

0 comments on commit 8f3563b

Please sign in to comment.