Yes, there is. It’s a two-pronged approach - firstly, you don’t want to have your app reload every time you upload something (which means /public
is right out), secondly, you need to know how to upload files through server code.
The first aspect is easily solved - you simply use nginx as a proxy server in front of meteor. This has several advantages:
- you can easily enable SSL
- gzip and other features are enabled as well
- you can serve static files directly without involving node / meteor.
This is my nginx
configuration:
upstream my_meteor_app {
server 127.0.0.1:3000;
}
server {
listen [::]:80;
listen 80;
server_name your_dns_address.here;
access_log /var/log/nginx/your_dns_address.here.ssl.access_log;
error_log /var/log/nginx/your_dns_address.here.ssl.error_log;
location /static {
alias /var/www/static;
}
location / {
proxy_pass http://my_meteor_app/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
This means that everything except /static
is served by meteor while your uploads will live in /var/www/static
The meteor code to accomplish this looks like this:
On the client, somewhere in a template, React module or whatever (this example uses React but you can easily change that)
<input type="file" onChange={(event) => { beginFileUpload(event); }} />
and the corresponding code for the function beginFileUpload
beginFileUpload: (e) => {
let input = e.target;
_.each(input.files, function(file) {
let fileReader = new FileReader();
let encoding = "binary";
let name = file.name;
fileReader.onload = function() {
if(fileReader.readAsBinaryString) {
Meteor.call('saveFile', fileReader.result, name, '', encoding);
} else {
var binary = "";
var bytes = new Uint8Array(fileReader.result);
var length = bytes.byteLength;
for(var i=0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
Meteor.call('saveFile', binary, name, '', encoding);
}
};
fileReader.onloadend = function (e) {
console.log(e);
};
fileReader.onloadstart = function (e) {
console.log(e);
};
fileReader.onprogress = function (e) {
console.log(e);
};
fileReader.onabort = function (e) {
console.log(e);
};
fileReader.onerror = function (e) {
console.log(e);
};
if(fileReader.readAsBinaryString) {
fileReader.readAsBinaryString(file);
} else {
fileReader.readAsArrayBuffer(file);
}
});
}
The if(fileReader.readAsBinaryString)
is necessary only if you want to support InternetExplorer because IE does not have this method. You can see that you can easily plugin other functions to update the user about the progress of the upload.
Should also work for multiple files.
With this code you’re calling a meteor method. It looks like this:
const fs = require('fs');
const pathNpm = require('path');
Meteor.methods({
saveFile: function(blob, name, path, encoding) {
if(Roles.userIsInRole(Meteor.user()._id, 'admin', 'group_name')) {
if(process.env.NODE_ENV === "production") {
var path = '/var/www/static';
} else {
var path = process.env['METEOR_SHELL_DIR'] + '/../../../public/static';
}
var filename = name.toLowerCase().replace(/ /g,'_').replace(/ä/g,'ae').replace(/ö/g,'oe').replace(/ü/g,'ue').replace(/ß/g,'ss').replace(/[^a-z0-9_.]/g,'');
fs.writeFile(path+"/"+filename, blob, encoding, Meteor.bindEnvironment(function(err){
if(err) {
console.log("Error:"+err);
} else {
console.log("Success");
WhateverCollectionYouNeed.insert({name: name, filename: filename});
}
}));
}
}
})
This method does a bit more than strictly needed but you get the gist - three things to mention, though:
a) this one here uses alanning:roles
to determine if a user is allowed to actually save the file (in this case, only the role “admin” for the group “group_name” would be allowed to do so).
b) The if(process.env.NODE_ENV === "production")
is a workaround for Windows because it’s a bit complicated to determine where the development directory lives exactly. In this case, if we’re in development files will be saved to /public/static
in your meteor directory (you’ll have to create this directory yourself, though).
c) I’m also doing a regex to replace special characters (the German Umlauts get translated to their ASCII equivalents, everything else special gets simply deleted, leaving only ASCII).
Oh, and don’t forget to give your web server r/w rights to /var/www/static