Add Group Picture upload

- Add field picture to Group type
- Add config option for image format
- Add groupPicture file handling at /upload
pull/5/head
trivernis 5 years ago
parent a4339ea540
commit 81b0aa9657

@ -68,6 +68,9 @@ graphiql = true
# The maximum complexity of queries
maxQueryComplexity = 5000
# sets the image format for the site. Values: png or webp
imageFormat = "png"
# Configuration for the api rate limit
[api.rateLimit]

@ -59,6 +59,8 @@
"typescript": "^3.7.2"
},
"dependencies": {
"@types/body-parser": "^1.17.1",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
"config": "^3.2.4",
"connect-session-sequelize": "^6.0.0",

@ -380,6 +380,9 @@ type Group {
"name of the group"
name: String!
"the groups icon"
picture: String
"the creator of the group"
creator: User!

@ -1,3 +1,4 @@
import * as sqz from "sequelize";
import {
BelongsTo,
BelongsToMany,
@ -29,6 +30,13 @@ export class Group extends Model<Group> {
@Column({allowNull: false, unique: true})
public name: string;
/**
* The url of the groups avatar picture
*/
@Column({type: sqz.STRING(512)})
public picture: string;
/**
* The id of the user who created the group
*/

@ -1,13 +1,16 @@
import * as bodyParser from "body-parser";
import * as config from "config";
import * as crypto from "crypto";
import {Router} from "express";
import * as fileUpload from "express-fileupload";
import {UploadedFile} from "express-fileupload";
import * as fsx from "fs-extra";
import {IncomingMessage} from "http";
import * as status from "http-status";
import * as path from "path";
import * as sharp from "sharp";
import globals from "../lib/globals";
import {User} from "../lib/models";
import {Group, User} from "../lib/models";
import Route from "../lib/Route";
const dataDirName = "data";
@ -29,6 +32,8 @@ interface IUploadConfirmation {
success: boolean;
}
type ImageFit = "cover" | "contain" | "fill" | "inside" | "outside";
/**
* Represents an upload handler.
*/
@ -60,12 +65,15 @@ export class UploadRoute extends Route {
public async init() {
await fsx.ensureDir(this.dataDir);
this.router.use(fileUpload());
this.router.use(bodyParser());
// Uploads a file to the data directory and returns the filename
this.router.use(async (req, res) => {
let uploadConfirmation: IUploadConfirmation;
if (req.session.userId) {
if (req.files.profilePicture) {
uploadConfirmation = await this.uploadProfilePicture(req);
} else if (req.files.groupPicture) {
uploadConfirmation = await this.uploadGroupPicture(req);
} else {
res.status(status.BAD_REQUEST);
uploadConfirmation = {
@ -103,26 +111,18 @@ export class UploadRoute extends Route {
let fileName: string;
const profilePic = request.files.profilePicture as UploadedFile;
try {
const fileBasename = UploadRoute.getFileName() + ".webp";
const filePath = path.join(this.dataDir, fileBasename);
await sharp(profilePic.data)
.resize(512, 512)
.normalise()
.webp({smartSubsample: true, reductionEffort: 6})
.toFile(filePath);
fileName = `/${dataDirName}/${fileBasename}`;
const user = await User.findByPk(request.session.userId);
if (user.profilePicture) {
const oldProfilePicture = path.join(this.dataDir, path.basename(user.profilePicture));
if (await fsx.pathExists(oldProfilePicture)) {
await fsx.unlink(oldProfilePicture);
} else {
globals.logger.warn(`Could not delete ${oldProfilePicture}: Not found!`);
if (user) {
fileName = await this.processAndStoreImage(profilePic.data);
if (user.profilePicture) {
await this.deleteWebImage(user.profilePicture);
}
user.profilePicture = fileName;
await user.save();
success = true;
} else {
error = "User not found";
}
user.profilePicture = fileName;
await user.save();
success = true;
} catch (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
@ -134,4 +134,96 @@ export class UploadRoute extends Route {
success,
};
}
/**
* Uploads an avatar image for a group.
* @param request
*/
private async uploadGroupPicture(request: any): Promise<IUploadConfirmation> {
let success = false;
let error: string;
let fileName: string;
const groupPicture = request.files.groupPicture as UploadedFile;
if (request.body.groupId) {
try {
const user = await User.findByPk(request.session.userId);
const group = await Group.findByPk(request.body.groupId);
if (!group) {
error = `No group with the id '${request.body.groupId}' found.`;
return {
error,
success,
};
}
const isAdmin = await group.$has("rAdmins", user);
if (isAdmin) {
fileName = await this.processAndStoreImage(groupPicture.data);
if (group.picture) {
await this.deleteWebImage(group.picture);
}
group.picture = fileName;
await group.save();
success = true;
} else {
error = "You are not a group admin.";
}
} catch (err) {
globals.logger.error(err.message);
globals.logger.debug(err.stack);
error = err.message;
}
} else {
error = "No groupId provided! (the request body must contain a groupId)";
}
return {
error,
fileName,
success,
};
}
/**
* Converts a file to the webp format and stores it with a uuid filename.
* The web path for the image is returned.
* @param data
* @param width
* @param height
* @param fit
*/
private async processAndStoreImage(data: Buffer, width = 512, height = 512,
fit: ImageFit = "cover"): Promise<string> {
const fileBasename = UploadRoute.getFileName() + "." + config.get("api.imageFormat");
const filePath = path.join(this.dataDir, fileBasename);
let image = await sharp(data)
.resize(width, height, {
fit,
})
.normalise();
if (config.get("api.imageFormat") === "webp") {
image = await image.webp({
reductionEffort: 6,
smartSubsample: true,
});
} else {
image = await image.png({
adaptiveFiltering: true,
colors: 128,
});
}
await image.toFile(filePath);
return `/${dataDirName}/${fileBasename}`;
}
/**
* Deletes an image for a provided web path.
* @param webPath
*/
private async deleteWebImage(webPath: string) {
const realPath = path.join(this.dataDir, path.basename(webPath));
if (await fsx.pathExists(realPath)) {
await fsx.unlink(realPath);
} else {
globals.logger.warn(`Could not delete web image ${realPath}: Not found!`);
}
}
}

@ -35,7 +35,7 @@
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6"
integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==
"@types/body-parser@*":
"@types/body-parser@*", "@types/body-parser@^1.17.1":
version "1.17.1"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897"
integrity sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==
@ -749,7 +749,7 @@ bluebird@^3.5.0:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.1.tgz#df70e302b471d7473489acf26a93d63b53f874de"
integrity sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==
body-parser@1.19.0:
body-parser@1.19.0, body-parser@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==

Loading…
Cancel
Save