Skip to content

Instantly share code, notes, and snippets.

@johnschult
Created November 2, 2015 20:14
Show Gist options
  • Save johnschult/a2708776440fed83d713 to your computer and use it in GitHub Desktop.
Save johnschult/a2708776440fed83d713 to your computer and use it in GitHub Desktop.
var Throttle, _insts, bound, cp, formatFleURL, fs, rcp, request;
if (Meteor.isServer) {
/*
@description Require "fs-extra" npm package
*/
fs = Npm.require("fs-extra");
request = Npm.require("request");
Throttle = Npm.require("throttle");
/*
@var {object} bound - Meteor.bindEnvironment aka Fiber wrapper
*/
bound = Meteor.bindEnvironment(function(callback) {
return callback();
});
}
/*
@object
@name _insts
@description Object of Meteor.Files instances
*/
_insts = {};
/*
@function
@name rcp
@param {Object} obj - Initial object
@description Create object with only needed props
*/
rcp = function(obj) {
var o;
o = {
currentFile: obj.currentFile,
search: obj.search,
storagePath: obj.storagePath,
collectionName: obj.collectionName,
downloadRoute: obj.downloadRoute,
chunkSize: obj.chunkSize,
debug: obj.debug,
_prefix: obj._prefix,
cacheControl: obj.cacheControl,
versions: obj.versions
};
return o;
};
/*
@function
@name cp
@param {Object} to - Destanation
@param {Object} from - Source
@description Copy-Paste only needed props from one to another object
*/
cp = function(to, from) {
to.currentFile = from.currentFile;
to.search = from.search;
to.storagePath = from.storagePath;
to.collectionName = from.collectionName;
to.downloadRoute = from.downloadRoute;
to.chunkSize = from.chunkSize;
to.debug = from.debug;
to._prefix = from._prefix;
to.cacheControl = from.cacheControl;
to.versions = from.versions;
return to;
};
/*
@class
@namespace Meteor
@name Files
@param config {Object} - Configuration object with next properties:
@param config.debug {Boolean} - Turn on/of debugging and extra logging
@param config.schema {Object} - Collection Schema
@param config.public {Boolean} - Store files in folder accessible for proxy servers, for limits, and more - read docs
@param config.strict {Boolean} - Strict mode for partial content, if is `true` server will return `416` response code, when `range` is not specified, otherwise server return `206`
@param config.protected {Function} - If `true` - files will be served only to authorized users, if `function()` - you're able to check visitor's permissions in your own way function's context has:
- `request` - On server only
- `response` - On server only
- `user()`
- `userId`
@param config.chunkSize {Number} - Upload chunk size
@param config.permissions {Number} - Permissions which will be set to uploaded files, like: `511` or `0o777`
@param config.storagePath {String} - Storage path on file system
@param config.cacheControl {String} - Default `Cache-Control` header
@param config.throttle {Number} - bps throttle threshold
@param config.downloadRoute {String} - Server Route used to retrieve files
@param config.collectionName {String} - Collection name
@param config.namingFunction {Function}- Function which returns `String`
@param config.integrityCheck {Boolean} - Check file's integrity before serving to users
@param config.onBeforeUpload {Function}- Function which executes on server after receiving each chunk and on client right before beginning upload. Function context is `File` - so you are able to check for extension, mime-type, size and etc.
return `true` to continue
return `false` or `String` to abort upload
@param config.allowClientCode {Boolean} - Allow to run `remove` from client
@param config.downloadCallback {Function} - Callback triggered each time file is requested
@param config.onbeforeunloadMessage {String|Function} - Message shown to user when closing browser's window or tab while upload process is running
@description Create new instance of Meteor.Files
*/
Meteor.Files = (function() {
function Files(config) {
var _methods, cookie, self;
if (config) {
this.storagePath = config.storagePath, this.collectionName = config.collectionName, this.downloadRoute = config.downloadRoute, this.schema = config.schema, this.chunkSize = config.chunkSize, this.namingFunction = config.namingFunction, this.debug = config.debug, this.onbeforeunloadMessage = config.onbeforeunloadMessage, this.permissions = config.permissions, this.allowClientCode = config.allowClientCode, this.onBeforeUpload = config.onBeforeUpload, this.integrityCheck = config.integrityCheck, this["protected"] = config["protected"], this["public"] = config["public"], this.strict = config.strict, this.downloadCallback = config.downloadCallback, this.cacheControl = config.cacheControl, this.throttle = config.throttle;
}
if (this.debug == null) {
this.debug = false;
}
if (this["public"] == null) {
this["public"] = false;
}
if (this.strict == null) {
this.strict = true;
}
if (this["protected"] == null) {
this["protected"] = false;
}
if (this.chunkSize == null) {
this.chunkSize = 272144;
}
if (this.permissions == null) {
this.permissions = 0x1ff;
}
if (this.cacheControl == null) {
this.cacheControl = 'public, max-age=31536000, s-maxage=31536000';
}
if (this.collectionName == null) {
this.collectionName = 'MeteorUploadFiles';
}
if (this.namingFunction == null) {
this.namingFunction = function() {
return Random._randomString(17, 'AZQWXSECDRFVTBGYNHUJMIKOLPzaqwsxecdrfvtgbyhnujimkolp');
};
}
if (this.integrityCheck == null) {
this.integrityCheck = true;
}
if (this.onBeforeUpload == null) {
this.onBeforeUpload = false;
}
if (this.allowClientCode == null) {
this.allowClientCode = true;
}
if (this.downloadCallback == null) {
this.downloadCallback = false;
}
if (this.onbeforeunloadMessage == null) {
this.onbeforeunloadMessage = 'Upload in a progress... Do you want to abort?';
}
if (this.throttle == null) {
this.throttle = false;
}
cookie = new Cookies();
if (this["protected"] && Meteor.isClient) {
if (!cookie.has('meteor_login_token') && Meteor._localStorage.getItem('Meteor.loginToken')) {
cookie.set('meteor_login_token', Meteor._localStorage.getItem('Meteor.loginToken'), null, '/');
}
}
if (this["public"] && this.storagePath) {
this.downloadRoute = this.storagePath.indexOf('/') !== 1 ? "/uploads/" + this.storagePath : "/uploads" + this.storagePath;
this.storagePath = this.storagePath.indexOf('/') !== 1 ? "../web.browser/" + this.storagePath : "../web.browser" + this.storagePath;
}
if (!this.storagePath) {
this.storagePath = this["public"] ? "../web.browser/uploads/" + this.collectionName : "/assets/app/uploads/" + this.collectionName;
this.downloadRoute = this["public"] ? "/uploads/" + this.collectionName : !this.downloadRoute ? '/cdn/storage' : void 0;
}
if (!this.downloadRoute) {
this.downloadRoute = '/cdn/storage';
}
if (!this.schema) {
this.schema = {
size: {
type: Number
},
name: {
type: String
},
type: {
type: String
},
path: {
type: String
},
isVideo: {
type: Boolean
},
isAudio: {
type: Boolean
},
isImage: {
type: Boolean
},
_prefix: {
type: String
},
extension: {
type: String,
optional: true
},
_storagePath: {
type: String
},
_downloadRoute: {
type: String
},
_collectionName: {
type: String
},
meta: {
type: Object,
blackbox: true,
optional: true
},
userId: {
type: String,
optional: true
},
updatedAt: {
type: Date,
autoValue: function() {
return new Date();
}
},
versions: {
type: Object,
blackbox: true
}
};
}
check(this.debug, Boolean);
check(this.schema, Object);
check(this["public"], Boolean);
check(this.strict, Boolean);
check(this.throttle, Match.OneOf(false, Number));
check(this["protected"], Match.OneOf(Boolean, Function));
check(this.chunkSize, Number);
check(this.permissions, Number);
check(this.storagePath, String);
check(this.downloadRoute, String);
check(this.integrityCheck, Boolean);
check(this.collectionName, String);
check(this.namingFunction, Function);
check(this.onBeforeUpload, Match.OneOf(Boolean, Function));
check(this.allowClientCode, Boolean);
check(this.downloadCallback, Match.OneOf(Boolean, Function));
check(this.onbeforeunloadMessage, Match.OneOf(String, Function));
if (this["public"] && this["protected"]) {
throw new Meteor.Error(500, "[Meteor.File." + this.collectionName + "]: Files can not be public and protected at the same time!");
}
this.collection = new Mongo.Collection(this.collectionName);
this.storagePath = this.storagePath.replace(/\/$/, '');
this.downloadRoute = this.downloadRoute.replace(/\/$/, '');
self = this;
this.cursor = null;
this.search = {};
this.currentFile = null;
this.collection.attachSchema(this.schema);
this.collection.deny({
insert: function() {
return true;
},
update: function() {
return true;
},
remove: function() {
return true;
}
});
this._prefix = SHA256(this.collectionName + this.storagePath + this.downloadRoute);
_insts[this._prefix] = this;
this.checkAccess = function(http) {
var result, text, user, userFuncs, userId;
if (this["protected"]) {
user = false;
userFuncs = this.getUser(http);
user = userFuncs.user, userId = userFuncs.userId;
user = user();
if (_.isFunction(this["protected"])) {
result = http ? this["protected"].call(_.extend(http, userFuncs)) : this["protected"].call(userFuncs);
} else {
result = !!user;
}
if (http && !result) {
if (this.debug) {
console.warn("Access denied!");
}
if (http) {
text = "Access denied!";
http.response.writeHead(401, {
'Content-Length': text.length,
'Content-Type': "text/plain"
});
http.response.end(text);
}
return false;
} else {
return true;
}
} else {
return true;
}
};
if (!this["public"]) {
Router.route(this.downloadRoute + "/" + this.collectionName + "/:_id/:version/:name", function() {
if (self.checkAccess(this)) {
return self.findOne(this.params._id).download.call(self, this, this.params.version);
}
}, {
where: 'server'
});
} else {
Router.route(this.downloadRoute + "/:file", function() {
var version;
if (this.params.file.indexOf('-') !== -1) {
version = this.params.file.split('-')[0];
return self.download.call(self, this, version);
} else {
return this.response.writeHead(404);
}
}, {
where: 'server'
});
}
this.methodNames = {
MeteorFileAbort: "MeteorFileAbort" + this._prefix,
MeteorFileWrite: "MeteorFileWrite" + this._prefix,
MeteorFileUnlink: "MeteorFileUnlink" + this._prefix
};
if (Meteor.isServer) {
_methods = {};
_methods[self.methodNames.MeteorFileUnlink] = function(inst) {
check(inst, Object);
if (self.debug) {
console.info("Meteor.Files Debugger: [MeteorFileUnlink]");
}
if (self.allowClientCode) {
return self.remove.call(cp(_insts[inst._prefix], inst), inst.search);
} else {
throw new Meteor.Error(401, '[Meteor.Files] [remove()] Run code from client is not allowed!');
}
};
_methods[self.methodNames.MeteorFileWrite] = function(unitArray, fileData, meta, first, chunksQty, currentChunk, totalSentChunks, randFileName, part, partsQty, fileSize) {
var _path, binary, e, error, extension, extensionWithDot, fileName, i, isUploadAllowed, last, path, pathName, pathPart, ref, result;
if (meta == null) {
meta = {};
}
this.unblock();
check(part, Number);
check(meta, Match.Optional(Object));
check(first, Boolean);
check(fileSize, Number);
check(partsQty, Number);
check(fileData, Object);
check(unitArray, Match.OneOf(Uint8Array, Object));
check(chunksQty, Number);
check(randFileName, String);
check(currentChunk, Number);
check(totalSentChunks, Number);
if (self.debug) {
console.info("Meteor.Files Debugger: [MeteorFileWrite] {name: " + randFileName + ", meta:" + meta + "}");
}
if (self.debug) {
console.info("Meteor.Files Debugger: Received chunk #" + currentChunk + " of " + chunksQty + " chunks, in part: " + part + ", file: " + (fileData.name || fileData.fileName));
}
if (self.onBeforeUpload && _.isFunction(self.onBeforeUpload)) {
isUploadAllowed = self.onBeforeUpload.call(fileData);
if (isUploadAllowed !== true) {
throw new Meteor.Error(403, _.isString(isUploadAllowed) ? isUploadAllowed : "@onBeforeUpload() returned false");
}
}
i = 0;
binary = '';
while (i < unitArray.byteLength) {
binary += String.fromCharCode(unitArray[i]);
i++;
}
last = chunksQty * partsQty <= totalSentChunks;
fileName = self.getFileName(fileData);
ref = self.getExt(fileName), extension = ref.extension, extensionWithDot = ref.extensionWithDot;
pathName = self["public"] ? self.storagePath + "/original-" + randFileName : self.storagePath + "/" + randFileName;
path = self["public"] ? self.storagePath + "/original-" + randFileName + extensionWithDot : self.storagePath + "/" + randFileName + extensionWithDot;
pathPart = partsQty > 1 ? pathName + "_" + part + extensionWithDot : path;
result = self.dataToSchema({
name: fileName,
path: path,
meta: meta,
type: fileData != null ? fileData.type : void 0,
size: fileData.size,
extension: extension
});
result.chunk = currentChunk;
result.last = last;
try {
if (first) {
fs.outputFileSync(pathPart, binary, 'binary');
} else {
fs.appendFileSync(pathPart, binary, 'binary');
}
} catch (_error) {
e = _error;
error = new Meteor.Error(500, "Unfinished upload (probably caused by server reboot or aborted operation)", e);
console.error(error);
return error;
}
if ((chunksQty === currentChunk) && self.debug) {
console.info("Meteor.Files Debugger: The part #" + part + " of file " + fileName + " (binary) was saved to " + pathPart);
}
if (last) {
if (partsQty > 1) {
i = 2;
while (i <= partsQty) {
_path = pathName + "_" + i + extensionWithDot;
fs.appendFileSync(pathName + '_1' + extensionWithDot, fs.readFileSync(_path), 'binary');
fs.unlink(_path);
i++;
}
fs.renameSync(pathName + '_1' + extensionWithDot, path);
}
fs.chmod(path, self.permissions);
if (self["public"]) {
result._id = randFileName;
}
result.type = self.getMimeType(fileData);
result._id = self.collection.insert(_.clone(result));
if (self.debug) {
console.info("Meteor.Files Debugger: The file " + fileName + " (binary) was saved to " + path);
}
}
return result;
};
_methods[self.methodNames.MeteorFileAbort] = function(randFileName, partsQty, fileData) {
var extensionWithDot, i, path, pathName, results;
check(partsQty, Number);
check(fileData, Object);
check(randFileName, String);
pathName = self["public"] ? self.storagePath + "/original-" + randFileName : self.storagePath + "/" + randFileName;
extensionWithDot = "." + fileData.ext;
if (partsQty > 1) {
i = 0;
results = [];
while (i <= partsQty) {
path = pathName + "_" + i + extensionWithDot;
if (fs.existsSync(path)) {
fs.unlink(path);
}
results.push(i++);
}
return results;
}
};
Meteor.methods(_methods);
}
}
/*
Extend Meteor.Files with mime library
@url https://github.com/broofa/node-mime
@description Temporary removed from package due to unstability
*/
/*
@isomorphic
@function
@class Meteor.Files
@name getMimeType
@param {Object} fileData - File Object
@description Returns file's mime-type
@returns {String}
*/
Files.prototype.getMimeType = function(fileData) {
var mime;
check(fileData, Object);
if (fileData != null ? fileData.type : void 0) {
mime = fileData.type;
}
if (!mime || !_.isString(mime)) {
mime = 'application/octet-stream';
}
return mime;
};
/*
@isomorphic
@function
@class Meteor.Files
@name getFileName
@param {Object} fileData - File Object
@description Returns file's name
@returns {String}
*/
Files.prototype.getFileName = function(fileData) {
var cleanName, fileName;
fileName = fileData.name || fileData.fileName;
if (_.isString(fileName) && fileName.length > 0) {
cleanName = function(str) {
return str.replace(/\.\./g, '').replace(/\//g, '');
};
return cleanName(fileData.name || fileData.fileName);
} else {
return '';
}
};
/*
@isomorphic
@function
@class Meteor.Files
@name getUser
@description Returns object with `userId` and `user()` method which return user's object
@returns {Object}
*/
Files.prototype.getUser = function(http) {
var cookie, result, user;
result = {
user: function() {
return void 0;
},
userId: void 0
};
if (Meteor.isServer) {
if (http) {
cookie = http.request.Cookies;
if (_.has(Package, 'accounts-base') && cookie.has('meteor_login_token')) {
user = Meteor.users.findOne({
"services.resume.loginTokens.hashedToken": Accounts._hashLoginToken(cookie.get('meteor_login_token'))
});
if (user) {
result.user = function() {
return user;
};
result.userId = user._id;
}
}
}
} else {
if (_.has(Package, 'accounts-base') && Meteor.userId()) {
result.user = function() {
return Meteor.user();
};
result.userId = Meteor.userId();
}
}
return result;
};
/*
@isomorphic
@function
@class Meteor.Files
@name getExt
@param {String} FileName - File name
@description Get extension from FileName
@returns {Object}
*/
Files.prototype.getExt = function(fileName) {
var extension;
if (!!~fileName.indexOf('.')) {
extension = fileName.split('.').pop();
return {
extension: extension,
extensionWithDot: '.' + extension
};
} else {
return {
extension: '',
extensionWithDot: ''
};
}
};
/*
@isomorphic
@function
@class Meteor.Files
@name dataToSchema
@param {Object} data - File data
@description Build object in accordance with schema from File data
@returns {Object}
*/
Files.prototype.dataToSchema = function(data) {
return {
name: data.name,
extension: data.extension,
path: data.path,
meta: data.meta,
type: data.type,
size: data.size,
versions: {
original: {
path: data.path,
size: data.size,
type: data.type,
extension: data.extension
}
},
isVideo: !!~data.type.toLowerCase().indexOf("video"),
isAudio: !!~data.type.toLowerCase().indexOf("audio"),
isImage: !!~data.type.toLowerCase().indexOf("image"),
_prefix: data._prefix || this._prefix,
_storagePath: data._storagePath || this.storagePath,
_downloadRoute: data._downloadRoute || this.downloadRoute,
_collectionName: data._collectionName || this.collectionName
};
};
/*
@isomorphic
@function
@class Meteor.Files
@name srch
@param {String|Object} search - Search data
@description Build search object
@returns {Object}
*/
Files.prototype.srch = function(search) {
if (search && _.isString(search)) {
this.search = {
_id: search
};
} else {
this.search = search;
}
return this.search;
};
/*
@server
@function
@class Meteor.Files
@name write
@param {Buffer} buffer - Binary File's Buffer
@param {Object} opts - {fileName: '', type: '', size: 0, meta: {...}}
@param {Function} callback - function(error, fileObj){...}
@description Write buffer to FS and add to Meteor.Files Collection
@returns {Files} - Return this
*/
Files.prototype.write = Meteor.isServer ? function(buffer, opts, callback) {
var extension, extensionWithDot, fileName, path, randFileName, ref, result;
if (opts == null) {
opts = {};
}
if (this.debug) {
console.info("Meteor.Files Debugger: [write(buffer, " + opts + ", callback)]");
}
check(opts, Match.Optional(Object));
check(callback, Match.Optional(Function));
if (this.checkAccess()) {
randFileName = this.namingFunction();
fileName = opts.name || opts.fileName ? opts.name || opts.fileName : randFileName;
ref = this.getExt(fileName), extension = ref.extension, extensionWithDot = ref.extensionWithDot;
path = this["public"] ? this.storagePath + "/original-" + randFileName + extensionWithDot : this.storagePath + "/" + randFileName + extensionWithDot;
opts.type = this.getMimeType(opts);
if (!opts.meta) {
opts.meta = {};
}
if (!opts.size) {
opts.size = buffer.length;
}
result = this.dataToSchema({
name: fileName,
path: path,
meta: opts.meta,
type: opts.type,
size: opts.size,
extension: extension
});
if (this.debug) {
console.info("Meteor.Files Debugger: The file " + fileName + " (binary) was added to " + this.collectionName);
}
fs.outputFileSync(path, buffer, 'binary');
result._id = this.collection.insert(_.clone(result));
callback && callback(null, result);
return result;
}
} : void 0;
/*
@server
@function
@class Meteor.Files
@name load
@param {String} url - URL to file
@param {Object} opts - {fileName: '', meta: {...}}
@param {Function} callback - function(error, fileObj){...}
@description Download file, write stream to FS and add to Meteor.Files Collection
@returns {Files} - Return this
*/
Files.prototype.load = Meteor.isServer ? function(url, opts, callback) {
var extension, extensionWithDot, fileName, path, randFileName, ref, self;
if (opts == null) {
opts = {};
}
if (this.debug) {
console.info("Meteor.Files Debugger: [load(" + url + ", " + opts + ", callback)]");
}
check(url, String);
check(opts, Match.Optional(Object));
check(callback, Match.Optional(Function));
self = this;
if (this.checkAccess()) {
randFileName = this.namingFunction();
fileName = opts.name || opts.fileName ? opts.name || opts.fileName : randFileName;
ref = this.getExt(fileName), extension = ref.extension, extensionWithDot = ref.extensionWithDot;
path = this["public"] ? this.storagePath + "/original-" + randFileName + extensionWithDot : this.storagePath + "/" + randFileName + extensionWithDot;
if (!opts.meta) {
opts.meta = {};
}
return request.get(url).on('error', function(error) {
throw new Meteor.Error(500, ("Error on [load(" + url + ", " + opts + ")]; Error:") + JSON.stringify(error));
}).on('response', function(response) {
return bound(function() {
var result;
result = self.dataToSchema({
name: fileName,
path: path,
meta: opts.meta,
type: response.headers['content-type'],
size: response.headers['content-length'],
extension: extension
});
if (this.debug) {
console.info("Meteor.Files Debugger: The file " + fileName + " (binary) was loaded to " + this.collectionName);
}
result._id = self.collection.insert(_.clone(result));
return callback && callback(null, result);
});
}).pipe(fs.createOutputStream(path));
}
} : void 0;
/*
@server
@function
@class Meteor.Files
@name addFile
@param {String} path - Path to file
@param {String} path - Path to file
@description Add file from FS to Meteor.Files
@returns {Files} - Return this
*/
Files.prototype.addFile = Meteor.isServer ? function(path, opts, callback) {
var error, extension, extensionWithDot, fileName, fileSize, pathParts, ref, result, stats;
if (opts == null) {
opts = {};
}
if (this.debug) {
console.info("[addFile(" + path + ")]");
}
if (this["public"]) {
throw new Meteor.Error(403, "Can not run [addFile()] on public collection");
}
check(path, String);
check(opts, Match.Optional(Object));
check(callback, Match.Optional(Function));
try {
stats = fs.statSync(path);
if (stat.isFile()) {
fileSize = stats.size;
pathParts = path.split('/');
fileName = pathParts[pathParts.length - 1];
ref = this.getExt(fileName), extension = ref.extension, extensionWithDot = ref.extensionWithDot;
if (!opts.type) {
opts.type = 'application/*';
}
if (!opts.meta) {
opts.meta = {};
}
if (!opts.size) {
opts.size = fileSize;
}
result = this.dataToSchema({
name: fileName,
path: path,
meta: opts.meta,
type: opts.type,
size: opts.size,
extension: extension,
_storagePath: path.replace("/" + fileName, '')
});
result._id = this.collection.insert(_.clone(result));
if (this.debug) {
console.info("The file " + fileName + " (binary) was added to " + this.collectionName);
}
callback && callback(null, result);
return result;
} else {
error = new Meteor.Error(400, "[Files.addFile(" + path + ")]: File does not exist");
callback && callback(error);
return error;
}
} catch (_error) {
error = _error;
callback && callback(error);
return error;
}
} : void 0;
/*
@isomorphic
@function
@class Meteor.Files
@name findOne
@param {String|Object} search - `_id` of the file or `Object` like, {prop:'val'}
@description Load file
@returns {Files} - Return this
*/
Files.prototype.findOne = function(search) {
if (this.debug) {
console.info("Meteor.Files Debugger: [findOne(" + search + ")]");
}
check(search, Match.OneOf(Object, String));
this.srch(search);
if (this.checkAccess()) {
this.currentFile = this.collection.findOne(this.search);
this.cursor = null;
return this;
}
};
/*
@isomorphic
@function
@class Meteor.Files
@name find
@param {String|Object} search - `_id` of the file or `Object` like, {prop:'val'}
@description Load file or bunch of files
@returns {Files} - Return this
*/
Files.prototype.find = function(search) {
if (this.debug) {
console.info("Meteor.Files Debugger: [find(" + search + ")]");
}
check(search, Match.OneOf(Object, String));
this.srch(search);
if (this.checkAccess) {
this.currentFile = null;
this.cursor = this.collection.find(this.search);
return this;
}
};
/*
@isomorphic
@function
@class Meteor.Files
@name get
@description Return value of current cursor or file
@returns {Object|[Object]}
*/
Files.prototype.get = function() {
if (this.debug) {
console.info("Meteor.Files Debugger: [get()]");
}
if (this.cursor) {
return this.cursor.fetch();
}
return this.currentFile;
};
/*
@isomorphic
@function
@class Meteor.Files
@name fetch
@description Alias for `get()` method
@returns {[Object]}
*/
Files.prototype.fetch = function() {
var data;
if (this.debug) {
console.info("Meteor.Files Debugger: [fetch()]");
}
data = this.get();
if (!_.isArray(data)) {
return [data];
} else {
return data;
}
};
/*
@client
@function
@class Meteor.Files
@name insert
@param {Object} config - Configuration object with next properties:
{File|Object} file - HTML5 `files` item, like in change event: `e.currentTarget.files[0]`
{Object} meta - Additional data as object, use later for search
{Number} streams - Quantity of parallel upload streams
{Function} onUploaded - Callback triggered when upload is finished, with two arguments `error` and `fileRef`
{Function} onProgress - Callback triggered when chunk is sent, with only argument `progress`
{Function} onBeforeUpload - Callback triggered right before upload is started, with only `FileReader` argument:
context is `File` - so you are able to check for extension, mime-type, size and etc.
return true to continue
return false to abort upload
@description Upload file to server over DDP
@url https://developer.mozilla.org/en-US/docs/Web/API/FileReader
@returns {Object} with next properties:
{ReactiveVar} onPause - Is upload process on the pause?
{Function} pause - Pause upload process
{Function} continue - Continue paused upload process
{Function} toggle - Toggle continue/pause if upload process
{Function} abort - Abort upload
*/
Files.prototype.insert = Meteor.isClient ? function(config) {
var beforeunload, end, ext, extension, extensionWithDot, file, fileData, fileReader, i, isUploadAllowed, j, last, len, meta, mime, onAbort, onBeforeUpload, onProgress, onUploaded, part, partSize, parts, randFileName, ref, ref1, result, self, streams, totalSentChunks, upload, uploaded;
if (this.checkAccess()) {
if (this.debug) {
console.info("Meteor.Files Debugger: [insert()]");
}
file = config.file, meta = config.meta, onUploaded = config.onUploaded, onProgress = config.onProgress, onBeforeUpload = config.onBeforeUpload, onAbort = config.onAbort, streams = config.streams;
if (meta == null) {
meta = {};
}
check(meta, Match.Optional(Object));
check(onAbort, Match.Optional(Function));
check(streams, Match.Optional(Number));
check(onUploaded, Match.Optional(Function));
check(onProgress, Match.Optional(Function));
check(onBeforeUpload, Match.Optional(Function));
if (file) {
if (this.debug) {
console.time('insert');
}
self = this;
beforeunload = function(e) {
var message;
message = _.isFunction(self.onbeforeunloadMessage) ? self.onbeforeunloadMessage.call(null) : self.onbeforeunloadMessage;
if (e) {
e.returnValue = message;
}
return message;
};
window.addEventListener("beforeunload", beforeunload, false);
result = {
onPause: new ReactiveVar(false),
continueFrom: [],
pause: function() {
return this.onPause.set(true);
},
"continue": function() {
var func, j, len, ref;
this.onPause.set(false);
ref = this.continueFrom;
for (j = 0, len = ref.length; j < len; j++) {
func = ref[j];
func.call(null);
}
return this.continueFrom = [];
},
toggle: function() {
if (this.onPause.get()) {
return this["continue"]();
} else {
return this.pause();
}
},
progress: new ReactiveVar(0),
abort: function() {
window.removeEventListener("beforeunload", beforeunload, false);
onAbort && onAbort.call(file, fileData);
this.pause();
Meteor.call(self.methodNames.MeteorFileAbort, randFileName, streams, file);
return delete upload;
}
};
result.progress.set = _.throttle(result.progress.set, 250);
Tracker.autorun(function() {
if (Meteor.status().connected) {
result["continue"]();
if (self.debug) {
return console.info("Meteor.Files Debugger: Connection established continue() upload");
}
} else {
result.pause();
if (self.debug) {
return console.info("Meteor.Files Debugger: Connection error set upload on pause()");
}
}
});
if (!streams) {
streams = 1;
}
totalSentChunks = 0;
ref = this.getExt(file.name), extension = ref.extension, extensionWithDot = ref.extensionWithDot;
if (!file.type) {
ref1 = this.getMimeType({}), ext = ref1.ext, mime = ref1.mime;
file.type = mime;
}
fileData = {
size: file.size,
type: file.type,
name: file.name,
ext: extension,
extension: extension,
'mime-type': file.type
};
file = _.extend(file, fileData);
last = false;
parts = [];
uploaded = 0;
partSize = Math.ceil(file.size / streams);
result.file = file;
randFileName = this.namingFunction();
i = 1;
while (i <= streams) {
parts.push({
to: partSize * i,
from: partSize * (i - 1),
size: partSize,
part: i,
chunksQty: this.chunkSize < partSize ? Math.ceil(partSize / this.chunkSize) : 1
});
i++;
}
end = function(error, data) {
if (self.debug) {
console.timeEnd('insert');
}
window.removeEventListener("beforeunload", beforeunload, false);
result.progress.set(0);
return onUploaded && onUploaded.call(self, error, data);
};
if (onBeforeUpload && _.isFunction(onBeforeUpload)) {
isUploadAllowed = onBeforeUpload.call(file);
if (isUploadAllowed !== true) {
end(new Meteor.Error(403, _.isString(isUploadAllowed) ? isUploadAllowed : "onBeforeUpload() returned false"), null);
return false;
}
}
if (this.onBeforeUpload && _.isFunction(this.onBeforeUpload)) {
isUploadAllowed = this.onBeforeUpload.call(file);
if (isUploadAllowed !== true) {
end(new Meteor.Error(403, _.isString(isUploadAllowed) ? isUploadAllowed : "@onBeforeUpload() returned false"), null);
return false;
}
}
upload = function(filePart, part, chunksQtyInPart, fileReader) {
var currentChunk, first;
currentChunk = 1;
first = true;
if (this.debug) {
console.time("insertPart" + part);
}
fileReader.onload = function(chunk) {
var arrayBuffer, progress, unitArray;
++totalSentChunks;
progress = (uploaded / file.size) * 100;
result.progress.set(progress);
onProgress && onProgress(progress);
last = part === streams && currentChunk >= chunksQtyInPart;
uploaded += self.chunkSize;
arrayBuffer = chunk.srcElement || chunk.target;
unitArray = new Uint8Array(arrayBuffer.result);
if (chunksQtyInPart === 1) {
Meteor.call(self.methodNames.MeteorFileWrite, unitArray, fileData, meta, first, chunksQtyInPart, currentChunk, totalSentChunks, randFileName, part, streams, file.size, function(error, data) {
if (error) {
return end(error);
}
if (data.last) {
return end(error, data);
}
});
} else {
Meteor.call(self.methodNames.MeteorFileWrite, unitArray, fileData, meta, first, chunksQtyInPart, currentChunk, totalSentChunks, randFileName, part, streams, file.size, function(error, data) {
var next;
if (error) {
return end(error);
}
next = function() {
var from, to;
if (data.chunk + 1 <= chunksQtyInPart) {
from = currentChunk * self.chunkSize;
to = from + self.chunkSize;
fileReader.readAsArrayBuffer(filePart.slice(from, to));
return currentChunk = ++data.chunk;
} else if (data.last) {
return end(error, data);
}
};
if (!result.onPause.get()) {
return next.call(null);
} else {
return result.continueFrom.push(next);
}
});
}
return first = false;
};
if (!result.onPause.get()) {
return fileReader.readAsArrayBuffer(filePart.slice(0, self.chunkSize));
}
};
for (i = j = 0, len = parts.length; j < len; i = ++j) {
part = parts[i];
fileReader = new FileReader;
upload.call(null, file.slice(part.from, part.to), i + 1, part.chunksQty, fileReader);
}
return result;
} else {
return console.warn("Meteor.Files: [insert({file: 'file', ..})]: file property is required");
}
}
} : void 0;
/*
@isomorphic
@function
@class Meteor.Files
@name remove
@param {String|Object} search - `_id` of the file or `Object` like, {prop:'val'}
@description Remove file(s) on cursor or find and remove file(s) if search is set
@returns {undefined}
*/
Files.prototype.remove = function(search) {
var files;
if (this.debug) {
console.info("Meteor.Files Debugger: [remove(" + search + ")]");
}
check(search, Match.Optional(Match.OneOf(Object, String)));
if (this.checkAccess()) {
this.srch(search);
if (Meteor.isClient) {
Meteor.call(this.methodNames.MeteorFileUnlink, rcp(this));
void 0;
}
if (Meteor.isServer) {
files = this.collection.find(this.search);
if (files.count() > 0) {
files.forEach((function(_this) {
return function(file) {
return _this.unlink(file);
};
})(this));
}
this.collection.remove(this.search);
return void 0;
}
}
};
/*
@sever
@function
@class Meteor.Files
@name unlink
@param {Object} file - fileObj
@description Unlink files and it's versions from FS
@returns {undefined}
*/
Files.prototype.unlink = Meteor.isServer ? function(file) {
if (file.versions && !_.isEmpty(file.versions)) {
_.each(file.versions, function(version) {
return fs.remove(version.path);
});
} else {
fs.remove(file.path);
}
return void 0;
} : void 0;
/*
@server
@function
@class Meteor.Files
@name download
@param {Object|Files} self - Instance of MEteor.Files
@description Initiates the HTTP response
@returns {undefined}
*/
Files.prototype.download = Meteor.isServer ? function(http, version) {
var array, dispositionEncoding, dispositionName, dispositionType, end, fileRef, fileStats, partiral, reqRange, responseType, start, stream, streamErrorHandler, take, text;
if (version == null) {
version = 'original';
}
if (this.debug) {
console.info("Meteor.Files Debugger: [download(" + http + ", " + version + ")]");
}
responseType = '200';
if (!this["public"]) {
if (this.currentFile) {
if (_.has(this.currentFile, 'versions') && _.has(this.currentFile.versions, version)) {
fileRef = this.currentFile.versions[version];
} else {
fileRef = this.currentFile;
}
} else {
fileRef = false;
}
}
if (this["public"]) {
fileRef = {
path: this.storagePath + "/" + http.params.file
};
responseType = fs.existsSync(fileRef.path) ? '200' : '404';
} else if (!fileRef || !_.isObject(fileRef) || !fs.existsSync(fileRef.path)) {
responseType = '404';
} else if (this.currentFile) {
if (this.downloadCallback) {
if (!this.downloadCallback.call(_.extend(http, this.getUser(http)), this.currentFile)) {
responseType = '404';
}
}
partiral = false;
reqRange = false;
fileStats = fs.statSync(fileRef.path);
if (fileStats.size !== fileRef.size && !this.integrityCheck) {
fileRef.size = fileStats.size;
}
if (fileStats.size !== fileRef.size && this.integrityCheck) {
responseType = '400';
}
if (http.params.query.download && http.params.query.download === 'true') {
dispositionType = 'attachment; ';
} else {
dispositionType = 'inline; ';
}
dispositionName = "filename=\"" + (encodeURIComponent(this.currentFile.name)) + "\"; filename=*UTF-8\"" + (encodeURIComponent(this.currentFile.name)) + "\"; ";
dispositionEncoding = 'charset=utf-8';
http.response.setHeader('Content-Type', fileRef.type);
http.response.setHeader('Content-Disposition', dispositionType + dispositionName + dispositionEncoding);
http.response.setHeader('Accept-Ranges', 'bytes');
http.response.setHeader('Last-Modified', this.currentFile.updatedAt.toUTCString());
http.response.setHeader('Connection', 'keep-alive');
if (http.request.headers.range) {
partiral = true;
array = http.request.headers.range.split(/bytes=([0-9]*)-([0-9]*)/);
start = parseInt(array[1]);
end = parseInt(array[2]);
if (isNaN(end)) {
end = (start + this.chunkSize) < fileRef.size ? start + this.chunkSize : fileRef.size;
}
take = end - start;
} else {
start = 0;
end = void 0;
take = this.chunkSize;
}
if (take > 4096000) {
take = 4096000;
end = start + take;
}
if (partiral || (http.params.query.play && http.params.query.play === 'true')) {
reqRange = {
start: start,
end: end
};
if (isNaN(start) && !isNaN(end)) {
reqRange.start = end - take;
reqRange.end = end;
}
if (!isNaN(start) && isNaN(end)) {
reqRange.start = start;
reqRange.end = start + take;
}
if ((start + take) >= fileRef.size) {
reqRange.end = fileRef.size - 1;
}
http.response.setHeader('Pragma', 'private');
http.response.setHeader('Expires', new Date(+(new Date) + 1000 * 32400).toUTCString());
http.response.setHeader('Cache-Control', 'private, maxage=10800, s-maxage=32400');
if ((this.strict && !http.request.headers.range) || reqRange.start >= fileRef.size || reqRange.end > fileRef.size) {
responseType = '416';
} else {
responseType = '206';
}
} else {
http.response.setHeader('Cache-Control', this.cacheControl);
responseType = '200';
}
}
streamErrorHandler = function(error) {
http.response.writeHead(500);
return http.response.end(error.toString());
};
switch (responseType) {
case '400':
if (this.debug) {
console.warn("Meteor.Files Debugger: [download(" + http + ", " + version + ")] [400] Content-Length mismatch!: " + fileRef.path);
}
text = "Content-Length mismatch!";
http.response.writeHead(400, {
'Content-Type': 'text/plain',
'Cache-Control': 'no-cache',
'Content-Length': text.length
});
http.response.end(text);
break;
case '404':
if (this.debug) {
console.warn("Meteor.Files Debugger: [download(" + http + ", " + version + ")] [404] File not found: " + (fileRef && fileRef.path ? fileRef.path : void 0));
}
text = "File Not Found :(";
http.response.writeHead(404, {
'Content-Length': text.length,
'Content-Type': "text/plain"
});
http.response.end(text);
break;
case '416':
if (this.debug) {
console.info("Meteor.Files Debugger: [download(" + http + ", " + version + ")] [416] Content-Range is not specified!: " + fileRef.path);
}
http.response.writeHead(416, {
'Content-Range': "bytes */" + fileRef.size
});
http.response.end();
break;
case '200':
if (this.debug) {
console.info("Meteor.Files Debugger: [download(" + http + ", " + version + ")] [200]: " + fileRef.path);
}
stream = fs.createReadStream(fileRef.path);
stream.on('open', (function(_this) {
return function() {
http.response.writeHead(200);
if (_this.throttle) {
return stream.pipe(new Throttle({
bps: _this.throttle,
chunksize: _this.chunkSize
})).pipe(http.response);
} else {
return stream.pipe(http.response);
}
};
})(this)).on('error', streamErrorHandler);
break;
case '206':
if (this.debug) {
console.info("Meteor.Files Debugger: [download(" + http + ", " + version + ")] [206]: " + fileRef.path);
}
http.response.setHeader('Content-Range', "bytes " + reqRange.start + "-" + reqRange.end + "/" + fileRef.size);
http.response.setHeader('Content-Length', take);
http.response.setHeader('Transfer-Encoding', 'chunked');
if (this.throttle) {
stream = fs.createReadStream(fileRef.path, {
start: reqRange.start,
end: reqRange.end
});
stream.on('open', function() {
return http.response.writeHead(206);
}).on('error', streamErrorHandler).on('end', function() {
return http.response.end();
}).pipe(new Throttle({
bps: this.throttle,
chunksize: this.chunkSize
})).pipe(http.response);
} else {
stream = fs.createReadStream(fileRef.path, {
start: reqRange.start,
end: reqRange.end
});
stream.on('open', function() {
return http.response.writeHead(206);
}).on('error', streamErrorHandler).on('data', function(chunk) {
return http.response.write(chunk);
}).on('end', function() {
return http.response.end();
});
}
break;
}
return void 0;
} : void 0;
/*
@isomorphic
@function
@class Meteor.Files
@name link
@param {Object} fileRef - File reference object
@param {String} version - [Optional] Version of file you would like to request
@param {Boolean} pub - [Optional] is file located in publicity available folder?
@description Returns URL to file
@returns {String}
*/
Files.prototype.link = function(fileRef, version, pub) {
if (version == null) {
version = 'original';
}
if (pub == null) {
pub = false;
}
if (this.debug) {
console.info("Meteor.Files Debugger: [link()]");
}
if (_.isString(fileRef)) {
version = fileRef;
fileRef = void 0;
}
if (!fileRef && !this.currentFile) {
return void 0;
}
if (this["public"]) {
return formatFleURL(fileRef || this.currentFile, version, true);
} else {
return formatFleURL(fileRef || this.currentFile, version, false);
}
};
return Files;
})();
/*
@isomorphic
@function
@name formatFleURL
@param {Object} fileRef - File reference object
@param {String} version - [Optional] Version of file you would like build URL for
@param {Boolean} pub - [Optional] is file located in publicity available folder?
@description Returns formatted URL for file
@returns {String}
*/
formatFleURL = function(fileRef, version, pub) {
var ext, ref, root;
if (version == null) {
version = 'original';
}
if (pub == null) {
pub = false;
}
root = __meteor_runtime_config__.ROOT_URL.replace(/\/+$/, "");
if ((fileRef != null ? (ref = fileRef.extension) != null ? ref.length : void 0 : void 0) > 0) {
ext = '.' + fileRef.extension;
} else {
ext = '';
}
if (pub) {
return root + (fileRef._downloadRoute + "/" + version + "-" + fileRef._id + ext);
} else {
return root + (fileRef._downloadRoute + "/" + fileRef._collectionName + "/" + fileRef._id + "/" + version + "/" + fileRef._id + ext);
}
};
if (Meteor.isClient) {
/*
@client
@TemplateHelper
@name fileURL
@param {Object} fileRef - File reference object
@param {String} version - [Optional] Version of file you would like to request
@description Get download URL for file by fileRef, even without subscription
@example {{fileURL fileRef}}
@returns {String}
*/
Template.registerHelper('fileURL', function(fileRef, version) {
if (!fileRef || !_.isObject(fileRef)) {
return void 0;
}
version = !_.isString(version) ? 'original' : version;
if (fileRef._id) {
if (fileRef._storagePath.indexOf('../web.browser') !== -1) {
return formatFleURL(fileRef, version, true);
} else {
return formatFleURL(fileRef, version, false);
}
} else {
return null;
}
});
}
// ---
// generated by coffee-script 1.9.2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment