Skip to content

Commit

Permalink
Adds option UploadFS.config.defaultStorePermissions
Browse files Browse the repository at this point in the history
Fixes #95 (store permission checks)
Fixes #94 (HTTP `Range` result from stream)
  • Loading branch information
jalik committed Oct 13, 2016
1 parent 66a30c7 commit 4333d80
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 97 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ Also I'll be glad to receive donations, whatever you give it will be much apprec

[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SS78MUMW8AH4N)

## Version 0.7.1
- Adds default store permissions (`UploadFS.config.defaultStorePermissions`)
- Fixes store permissions (#95)
- Fixes HTTP `Range` result from stream (#94) : works with ufs-local and ufs-gridfs

## Version 0.7.0_2
- Adds support for `Range` request headers (to seek audio/video files)
- Upgrades dependencies
Expand Down Expand Up @@ -187,6 +192,22 @@ In this documentation, I am using the `UploadFS.store.Local` store which saves f
You can access and modify settings via `UploadFS.config`.
```js
// Set default permissions for all stores (you can later overwrite the default permissions on each store)
UploadFS.config.defaultStorePermissions = new UploadFS.StorePermissions({
insert: function (userId, doc) {
return userId;
},
update: function (userId, doc) {
return userId === doc.userId;
},
remove: function (userId, doc) {
return userId === doc.userId;
}
});

// Use HTTPS in URLs
UploadFS.config.https = true;

// Activate simulation for slowing file reading
UploadFS.config.simulateReadDelay = 1000; // 1 sec

Expand Down Expand Up @@ -456,6 +477,22 @@ PhotosStore = new UploadFS.store.Local({
});
```
or you can set default permissions for all stores (since v0.7.1) :
```js
UploadFS.config.defaultStorePermissions = new UploadFS.StorePermissions({
insert: function (userId, doc) {
return userId;
},
update: function (userId, doc) {
return userId === doc.userId;
},
remove: function (userId, doc) {
return userId === doc.userId;
}
});
```
## Securing file access (server)
When returning the file for a HTTP request, you can do some checks to decide whether or not the file should be sent to the client.
Expand Down
2 changes: 1 addition & 1 deletion package.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package.describe({
name: 'jalik:ufs',
version: '0.7.0_2',
version: '0.7.1',
author: '[email protected]',
summary: 'Base package for UploadFS',
homepage: 'https://github.com/jalik/jalik-ufs',
Expand Down
76 changes: 38 additions & 38 deletions ufs-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {Meteor} from 'meteor/meteor';
* @constructor
*/
UploadFS.Config = function (options) {
// Set default options
// Default options
options = _.extend({
defaultStorePermissions: null,
https: false,
simulateReadDelay: 0,
simulateUploadSpeed: 0,
Expand All @@ -19,6 +20,9 @@ UploadFS.Config = function (options) {
}, options);

// Check options
if (options.defaultStorePermissions && !(options.defaultStorePermissions instanceof UploadFS.StorePermissions)) {
throw new TypeError('defaultStorePermissions is not an instance of UploadFS.StorePermissions');
}
if (typeof options.https !== 'boolean') {
throw new TypeError('https is not a function');
}
Expand All @@ -41,52 +45,48 @@ UploadFS.Config = function (options) {
throw new Meteor.Error('tmpDirPermissions is not a string');
}

// Public attributes
/**
* Default store permissions
* @type {UploadFS.StorePermissions}
*/
this.defaultStorePermissions = options.defaultStorePermissions;
/**
* Use or not secured protocol in URLS
* @type {boolean}
*/
this.https = options.https;
/**
* The simulation read delay
* @type {Number}
*/
this.simulateReadDelay = parseInt(options.simulateReadDelay);
/**
* The simulation upload speed
* @type {Number}
*/
this.simulateUploadSpeed = parseInt(options.simulateUploadSpeed);
/**
* The simulation write delay
* @type {Number}
*/
this.simulateWriteDelay = parseInt(options.simulateWriteDelay);
/**
* The URL root path of stores
* @type {string}
*/
this.storesPath = options.storesPath;
/**
* The temporary directory of uploading files
* @type {string}
*/
this.tmpDir = options.tmpDir;
/**
* The permissions of the temporary directory
* @type {string}
*/
this.tmpDirPermissions = options.tmpDirPermissions;
};

/**
* Simulation read delay in milliseconds
* @type {number}
*/
UploadFS.Config.prototype.simulateReadDelay = 0;

/**
* Simulation upload speed in milliseconds
* @type {number}
*/
UploadFS.Config.prototype.simulateUploadSpeed = 0;

/**
* Simulation write delay in milliseconds
* @type {number}
*/
UploadFS.Config.prototype.simulateWriteDelay = 0;

/**
* URL path to stores
* @type {string}
*/
UploadFS.Config.prototype.storesPath = null;

/**
* Local temporary directory for uploading files
* @type {string}
*/
UploadFS.Config.prototype.tmpDir = null;

/**
* Permissions of the local temporary directory
* @type {string}
*/
UploadFS.Config.prototype.tmpDirPermissions = '0700';

/**
* Global configuration
* @type {UploadFS.Config}
Expand Down
2 changes: 1 addition & 1 deletion ufs-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {_} from 'meteor/underscore';
UploadFS.Filter = function (options) {
let self = this;

// Set default options
// Default options
options = _.extend({
contentTypes: null,
extensions: null,
Expand Down
2 changes: 1 addition & 1 deletion ufs-methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Meteor.methods({
/**
* Creates the file and returns the file upload token
* @param file
* @returns {{fileId: file, token: *, url}}
* @returns {{fileId: string, token: *, url}}
*/
ufsCreate: function (file) {
check(file, Object);
Expand Down
6 changes: 2 additions & 4 deletions ufs-mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,8 @@ UploadFS.addMimeType = function (extension, mime) {
* @returns {*}
*/
UploadFS.getMimeType = function (extension) {
if (extension) {
extension = extension.toLowerCase();
return MIME[extension];
}
extension = extension.toLowerCase();
return MIME[extension];
};

/**
Expand Down
66 changes: 37 additions & 29 deletions ufs-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,38 +199,22 @@ WebApp.connectHandlers.use((req, res, next) => {
d.run(() => {
// Check if the file can be accessed
if (store.onRead.call(store, fileId, file, req, res) !== false) {
// Open the file stream
let rs = store.getReadStream(fileId, file);
let ws = new stream.PassThrough();

rs.on('error', Meteor.bindEnvironment((err) => {
store.onReadError.call(store, err, fileId, file);
res.end();
}));
ws.on('error', Meteor.bindEnvironment((err) => {
store.onReadError.call(store, err, fileId, file);
res.end();
}));
ws.on('close', () => {
// Close output stream at the end
ws.emit('end');
});
let options = {};
let status = 200;

// Prepare response headers
let headers = {
'Content-Type': file.type,
'Content-Length': file.size
};

// Transform stream
store.transformRead(rs, ws, fileId, file, req, headers);

// Parse request headers
if (typeof req.headers === 'object') {

// Send partial data
// Send data in range
if (typeof req.headers.range === 'string') {
let range = req.headers.range;

// Range is not valid
if (!range) {
res.writeHead(416);
res.end();
Expand All @@ -241,16 +225,40 @@ WebApp.connectHandlers.use((req, res, next) => {
let start = parseInt(positions[0], 10);
let total = file.size;
let end = positions[1] ? parseInt(positions[1], 10) : total - 1;
let chunkSize = (end - start) + 1;

// Update headers
headers['Content-Range'] = `bytes ${start}-${end}/${total}`;
headers['Accept-Ranges'] = `bytes`;
headers['Content-Length'] = chunkSize;
res.writeHead(206, headers);
ws.pipe(res);
return;
headers['Content-Length'] = (end - start) + 1;

status = 206; // partial content
options.start = start;
options.end = end;
}
}

// Open the file stream
let rs = store.getReadStream(fileId, file, options);
let ws = new stream.PassThrough();

rs.on('error', Meteor.bindEnvironment((err) => {
store.onReadError.call(store, err, fileId, file);
res.end();
}));
ws.on('error', Meteor.bindEnvironment((err) => {
store.onReadError.call(store, err, fileId, file);
res.end();
}));
ws.on('close', () => {
// Close output stream at the end
ws.emit('end');
});

// Transform stream
store.transformRead(rs, ws, fileId, file, req, headers);

// Parse request headers
if (typeof req.headers === 'object') {
// Compress data using if needed (ignore audio/video as they are already compressed)
if (typeof req.headers['accept-encoding'] === 'string' && !/^(audio|video)/.test(file.type)) {
let accept = req.headers['accept-encoding'];
Expand All @@ -259,15 +267,15 @@ WebApp.connectHandlers.use((req, res, next) => {
if (accept.match(/\bgzip\b/)) {
headers['Content-Encoding'] = 'gzip';
delete headers['Content-Length'];
res.writeHead(200, headers);
res.writeHead(status, headers);
ws.pipe(zlib.createGzip()).pipe(res);
return;
}
// Compress with deflate
else if (accept.match(/\bdeflate\b/)) {
headers['Content-Encoding'] = 'deflate';
delete headers['Content-Length'];
res.writeHead(200, headers);
res.writeHead(status, headers);
ws.pipe(zlib.createDeflate()).pipe(res);
return;
}
Expand All @@ -276,7 +284,7 @@ WebApp.connectHandlers.use((req, res, next) => {

// Send raw data
if (!headers['Content-Encoding']) {
res.writeHead(200, headers);
res.writeHead(status, headers);
ws.pipe(res);
}

Expand Down
51 changes: 35 additions & 16 deletions ufs-store-permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,56 @@ import {_} from 'meteor/underscore';
* @constructor
*/
UploadFS.StorePermissions = function (options) {
let self = this;

// Default options
options = _.extend({
insert: null,
remove: null,
update: null
}, options);

// Check options
if (typeof options.insert === 'function') {
self.insert = options.insert;
this.insert = options.insert;
}
if (typeof options.remove === 'function') {
self.remove = options.remove;
this.remove = options.remove;
}
if (typeof options.update === 'function') {
self.update = options.update;
this.update = options.update;
}

self.checkInsert = (userId, file) => {
if (typeof self.insert === 'function') {
self.insert.call(self, userId, file);
let checkPermission = (permission, userId, file)=> {
if (typeof this[permission] === 'function') {
return this[permission](userId, file);
}
return true; // by default allow all
};
self.checkRemove = (userId, file) => {
if (typeof self.remove === 'function') {
self.remove.call(self, userId, file);
}

/**
* Checks the insert permission
* @param userId
* @param file
* @returns {*}
*/
this.checkInsert = (userId, file) => {
return checkPermission('insert', userId, file);
};
self.checkUpdate = (userId, file) => {
if (typeof self.update === 'function') {
self.update.call(self, userId, file);
}
/**
* Checks the remove permission
* @param userId
* @param file
* @returns {*}
*/
this.checkRemove = (userId, file) => {
return checkPermission('remove', userId, file);
};
/**
* Checks the update permission
* @param userId
* @param file
* @returns {*}
*/
this.checkUpdate = (userId, file) => {
return checkPermission('update', userId, file);
};
};
Loading

0 comments on commit 4333d80

Please sign in to comment.