Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Allow request avatar placeholders as PNG or JPG instead of SVG #8193

Merged
merged 10 commits into from
Feb 14, 2018
14 changes: 0 additions & 14 deletions packages/rocketchat-file-upload/server/config/FileSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,13 @@ const FileSystemAvatars = new FileUploadClass({
// store setted bellow

get(file, req, res) {
const reqModifiedHeader = req.headers['if-modified-since'];
if (reqModifiedHeader) {
if (reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) {
res.setHeader('Last-Modified', reqModifiedHeader);
res.writeHead(304);
res.end();
return;
}
}

const filePath = this.store.getFilePath(file._id, file);

try {
const stat = Meteor.wrapAsync(fs.stat)(filePath);

if (stat && stat.isFile()) {
file = FileUpload.addExtensionTo(file);
res.setHeader('Content-Disposition', 'inline');
res.setHeader('Last-Modified', file.uploadedAt.toUTCString());
res.setHeader('Content-Type', file.type);
res.setHeader('Content-Length', file.size);

this.store.getReadStream(file._id, file).pipe(res);
}
Expand Down
70 changes: 28 additions & 42 deletions packages/rocketchat-file-upload/server/config/GridFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const getByteRange = function(header) {


// code from: https://github.com/jalik/jalik-ufs/blob/master/ufs-server.js#L310
const readFromGridFS = function(storeName, fileId, file, headers, req, res) {
const readFromGridFS = function(storeName, fileId, file, req, res) {
const store = UploadFS.getStore(storeName);
const rs = store.getReadStream(fileId, file);
const ws = new stream.PassThrough();
Expand All @@ -82,7 +82,7 @@ const readFromGridFS = function(storeName, fileId, file, headers, req, res) {
const accept = req.headers['accept-encoding'] || '';

// Transform stream
store.transformRead(rs, ws, fileId, file, req, headers);
store.transformRead(rs, ws, fileId, file, req);
const range = getByteRange(req.headers.range);
let out_of_range = false;
if (range) {
Expand All @@ -91,34 +91,34 @@ const readFromGridFS = function(storeName, fileId, file, headers, req, res) {

// Compress data using gzip
if (accept.match(/\bgzip\b/) && range === null) {
headers['Content-Encoding'] = 'gzip';
delete headers['Content-Length'];
res.writeHead(200, headers);
res.setHeader('Content-Encoding', 'gzip');
res.removeHeader('Content-Length');
res.writeHead(200);
ws.pipe(zlib.createGzip()).pipe(res);
} else if (accept.match(/\bdeflate\b/) && range === null) {
// Compress data using deflate
headers['Content-Encoding'] = 'deflate';
delete headers['Content-Length'];
res.writeHead(200, headers);
res.setHeader('Content-Encoding', 'deflate');
res.removeHeader('Content-Length');
res.writeHead(200);
ws.pipe(zlib.createDeflate()).pipe(res);
} else if (range && out_of_range) {
// out of range request, return 416
delete headers['Content-Length'];
delete headers['Content-Type'];
delete headers['Content-Disposition'];
delete headers['Last-Modified'];
headers['Content-Range'] = `bytes */${ file.size }`;
res.writeHead(416, headers);
res.removeHeader('Content-Length');
res.removeHeader('Content-Type');
res.removeHeader('Content-Disposition');
res.removeHeader('Last-Modified');
res.setHeader('Content-Range', `bytes */${ file.size }`);
res.writeHead(416);
res.end();
} else if (range) {
headers['Content-Range'] = `bytes ${ range.start }-${ range.stop }/${ file.size }`;
delete headers['Content-Length'];
headers['Content-Length'] = range.stop - range.start + 1;
res.writeHead(206, headers);
res.setHeader('Content-Range', `bytes ${ range.start }-${ range.stop }/${ file.size }`);
res.removeHeader('Content-Length');
res.setHeader('Content-Length', range.stop - range.start + 1);
res.writeHead(206);
logger.debug('File upload extracting range');
ws.pipe(new ExtractRange({ start: range.start, stop: range.stop })).pipe(res);
} else {
res.writeHead(200, headers);
res.writeHead(200);
ws.pipe(res);
}
};
Expand All @@ -140,36 +140,22 @@ new FileUploadClass({

get(file, req, res) {
file = FileUpload.addExtensionTo(file);
const headers = {
'Content-Disposition': `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`,
'Last-Modified': file.uploadedAt.toUTCString(),
'Content-Type': file.type,
'Content-Length': file.size
};
return readFromGridFS(file.store, file._id, file, headers, req, res);

res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(file.name) }`);
res.setHeader('Last-Modified', file.uploadedAt.toUTCString());
res.setHeader('Content-Type', file.type);
res.setHeader('Content-Length', file.size);

return readFromGridFS(file.store, file._id, file, req, res);
}
});

new FileUploadClass({
name: 'GridFS:Avatars',

get(file, req, res) {
const reqModifiedHeader = req.headers['if-modified-since'];
if (reqModifiedHeader && reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) {
res.setHeader('Last-Modified', reqModifiedHeader);
res.writeHead(304);
res.end();
return;
}
file = FileUpload.addExtensionTo(file);
const headers = {
'Cache-Control': 'public, max-age=0',
'Expires': '-1',
'Content-Disposition': 'inline',
'Last-Modified': file.uploadedAt.toUTCString(),
'Content-Type': file.type,
'Content-Length': file.size
};
return readFromGridFS(file.store, file._id, file, headers, req, res);

return readFromGridFS(file.store, file._id, file, req, res);
}
});
46 changes: 27 additions & 19 deletions packages/rocketchat-file-upload/server/lib/FileUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ Object.assign(FileUpload, {
// filter: new UploadFS.Filter({
// onCheck: FileUpload.validateFileUpload
// }),
// transformWrite: FileUpload.avatarTransformWrite,
getPath(file) {
return `${ RocketChat.settings.get('uniqueID') }/avatars/${ file.userId }`;
},
Expand All @@ -59,34 +58,43 @@ Object.assign(FileUpload, {
};
},

avatarTransformWrite(readStream, writeStream/*, fileId, file*/) {
if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) {
return readStream.pipe(writeStream);
}
const height = RocketChat.settings.get('Accounts_AvatarSize');
const width = height;
return (file => RocketChat.Info.GraphicsMagick.enabled ? file: file.alpha('remove'))(RocketChatFile.gm(readStream).background('#FFFFFF')).resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).stream('jpeg').pipe(writeStream);
},

avatarsOnValidate(file) {
if (RocketChatFile.enabled === false || RocketChat.settings.get('Accounts_AvatarResize') !== true) {
if (RocketChat.settings.get('Accounts_AvatarResize') !== true) {
return;
}

const tempFilePath = UploadFS.getTempFilePath(file._id);

const height = RocketChat.settings.get('Accounts_AvatarSize');
const width = height;
const future = new Future();

(file => RocketChat.Info.GraphicsMagick.enabled ? file: file.alpha('remove'))(RocketChatFile.gm(tempFilePath).background('#FFFFFF')).resize(width, `${ height }^`).gravity('Center').crop(width, height).extent(width, height).setFormat('jpeg').write(tempFilePath, Meteor.bindEnvironment(err => {
if (err != null) {
console.error(err);
}
const size = fs.lstatSync(tempFilePath).size;
this.getCollection().direct.update({_id: file._id}, {$set: {size}});
future.return();
const s = sharp(tempFilePath);
s.rotate();
// Get metadata to resize the image the first time to keep "inside" the dimensions
// then resize again to create the canvas around
s.metadata(Meteor.bindEnvironment((err, metadata) => {
s.toFormat(sharp.format.jpeg)
.resize(Math.min(height, metadata.width), Math.min(height, metadata.height))
.pipe(sharp()
.resize(height, height)
.background('#FFFFFF')
.embed()
)
// Use buffer to get the result in memory then replace the existing file
// There is no option to override a file using this library
.toBuffer()
.then(Meteor.bindEnvironment(outputBuffer => {
fs.writeFile(tempFilePath, outputBuffer, Meteor.bindEnvironment(err => {
if (err != null) {
console.error(err);
}
const size = fs.lstatSync(tempFilePath).size;
this.getCollection().direct.update({_id: file._id}, {$set: {size}});
future.return();
}));
}));
}));

return future.wait();
},

Expand Down
30 changes: 28 additions & 2 deletions server/startup/avatar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* globals FileUpload */
import _ from 'underscore';
import sharp from 'sharp';

Meteor.startup(function() {
WebApp.connectHandlers.use('/avatar/', Meteor.bindEnvironment(function(req, res/*, next*/) {
Expand Down Expand Up @@ -38,6 +39,21 @@ Meteor.startup(function() {
if (file) {
res.setHeader('Content-Security-Policy', 'default-src \'none\'');

const reqModifiedHeader = req.headers['if-modified-since'];
if (reqModifiedHeader && reqModifiedHeader === (file.uploadedAt && file.uploadedAt.toUTCString())) {
res.setHeader('Last-Modified', reqModifiedHeader);
res.writeHead(304);
res.end();
return;
}

res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('Expires', '-1');
res.setHeader('Content-Disposition', 'inline');
res.setHeader('Last-Modified', file.uploadedAt.toUTCString());
res.setHeader('Content-Type', file.type);
res.setHeader('Content-Length', file.size);

return FileUpload.get(file, req, res);
} else {
res.setHeader('Content-Type', 'image/svg+xml');
Expand Down Expand Up @@ -79,11 +95,21 @@ Meteor.startup(function() {
initials = username.replace(/[^A-Za-z0-9]/g, '').substr(0, 1).toUpperCase();
}

const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 80 80\">\n<rect width=\"100%\" height=\"100%\" rx=\"6\" ry=\"6\" fill=\"${ color }\"/>\n<text x=\"50%\" y=\"50%\" dy=\"0.36em\" text-anchor=\"middle\" pointer-events=\"none\" fill=\"#ffffff\" font-family=\"Helvetica, Arial, Lucida Grande, sans-serif\" font-size="50">\n${ initials }\n</text>\n</svg>`;
const viewSize = parseInt(req.query.size) || 200;
const fontSize = viewSize / 1.6;

const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${ viewSize } ${ viewSize }\">\n<rect width=\"100%\" height=\"100%\" fill=\"${ color }\"/>\n<text x=\"50%\" y=\"50%\" dy=\"0.36em\" text-anchor=\"middle\" pointer-events=\"none\" fill=\"#ffffff\" font-family=\"Helvetica, Arial, Lucida Grande, sans-serif\" font-size="${ fontSize }">\n${ initials }\n</text>\n</svg>`;

if (['png', 'jpg', 'jpeg'].includes(req.query.format)) {
res.setHeader('Content-Type', `image/${ req.query.format }`);
sharp(new Buffer(svg))
.toFormat(req.query.format)
.pipe(res);
return;
}

res.write(svg);
res.end();

return;
}
}
Expand Down