This is what we did. @paulishca mentioned Meteor Files, which may be a better option today. I don’t recall exactly why we rolled our own, but we weren’t able to use that package at the time (not sure if streaming was even an option back then. It was a while ago.)
At any rate, if it’s useful, the code is below. We used WebApp.connectHandlers.use() to create the endpoint, s3-streams to retrieve the file, and then write the file out to the client. One niggle worth noting was the handling of UTF8 filenames. Without that, we ran into the occasional filename that would result in a white screen on some browsers (probably older versions of IE, but I honestly don’t recall.)
One reason we went with streaming through the app was to obscure the source of the files, as it allows us to use our own URL. Just makes a slightly better user experience if they’re not suddenly seeing an odd looking domain in the address bar.
Edit: ignore the token
variable in the below. That’s just used internally to track file downloads, and is set in the omitted authentication section.
import AWS from 'aws-sdk';
import S3S from 's3-streams';
import iconv from 'iconv-lite';
WebApp.connectHandlers.use('/files/applications', function filesApplications(req, res) {
// authentication bits snipped
// get the s3Key for the file
const fileRecord = _.findWhere(application.candidateFiles, {
name: file.data.fileName,
fileCategory: file.data.fileCategory,
});
const s3Client = new AWS.S3({
region: Meteor.settings.public.awsS3BucketRegion,
accessKeyId: Meteor.settings.AWSAccessKeyId,
secretAccessKey: Meteor.settings.AWSSecretAccessKey,
});
const getObjectOptions = {
Bucket: Meteor.settings.public.awsS3Bucket,
Key: fileRecord.s3Key,
};
const src = new S3S.ReadStream(s3Client, getObjectOptions);
src
.on('error', Meteor.bindEnvironment((err) => {
res.statusCode = 404;
res.end(`File not found: ${err.statusCode.toString()}`);
Logger.error(`S3 error. Token: ${token}, ${err.statusCode.toString()}`, { data: file });
}))
.on('open', Meteor.bindEnvironment((object) => {
const buff = new Buffer(file.data.fileName, 'utf8');
const filenameISO88591 = iconv.decode(buff, 'ISO-8859-1');
const filenameUTF8 = encodeURIComponent(file.data.fileName);
res.writeHead(200, {
'Content-Type': object.ContentType,
'Content-Disposition': `inline; filename="${filenameISO88591}"; filename*=UTF-8''${filenameUTF8};`,
'Content-Length': object.ContentLength,
});
Logger.info(`Delivering file. Token: ${token}`);
}))
.pipe(res)
.on('finish', Meteor.bindEnvironment(() => {
Logger.info(`File pipleine closed. Token: ${token}`);
}))
.on('error', Meteor.bindEnvironment(() => ((response) => {
response.statusCode = 404;
response.end('File not found');
Logger.error(`Error delivering file. Token: ${token}`, { data: file });
})(res)));
});