|
|
@ -1,13 +1,16 @@
|
|
|
|
|
|
|
|
import * as bodyParser from "body-parser";
|
|
|
|
|
|
|
|
import * as config from "config";
|
|
|
|
import * as crypto from "crypto";
|
|
|
|
import * as crypto from "crypto";
|
|
|
|
import {Router} from "express";
|
|
|
|
import {Router} from "express";
|
|
|
|
import * as fileUpload from "express-fileupload";
|
|
|
|
import * as fileUpload from "express-fileupload";
|
|
|
|
import {UploadedFile} from "express-fileupload";
|
|
|
|
import {UploadedFile} from "express-fileupload";
|
|
|
|
import * as fsx from "fs-extra";
|
|
|
|
import * as fsx from "fs-extra";
|
|
|
|
|
|
|
|
import {IncomingMessage} from "http";
|
|
|
|
import * as status from "http-status";
|
|
|
|
import * as status from "http-status";
|
|
|
|
import * as path from "path";
|
|
|
|
import * as path from "path";
|
|
|
|
import * as sharp from "sharp";
|
|
|
|
import * as sharp from "sharp";
|
|
|
|
import globals from "../lib/globals";
|
|
|
|
import globals from "../lib/globals";
|
|
|
|
import {User} from "../lib/models";
|
|
|
|
import {Group, User} from "../lib/models";
|
|
|
|
import Route from "../lib/Route";
|
|
|
|
import Route from "../lib/Route";
|
|
|
|
|
|
|
|
|
|
|
|
const dataDirName = "data";
|
|
|
|
const dataDirName = "data";
|
|
|
@ -29,6 +32,8 @@ interface IUploadConfirmation {
|
|
|
|
success: boolean;
|
|
|
|
success: boolean;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type ImageFit = "cover" | "contain" | "fill" | "inside" | "outside";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Represents an upload handler.
|
|
|
|
* Represents an upload handler.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
@ -60,12 +65,15 @@ export class UploadRoute extends Route {
|
|
|
|
public async init() {
|
|
|
|
public async init() {
|
|
|
|
await fsx.ensureDir(this.dataDir);
|
|
|
|
await fsx.ensureDir(this.dataDir);
|
|
|
|
this.router.use(fileUpload());
|
|
|
|
this.router.use(fileUpload());
|
|
|
|
|
|
|
|
this.router.use(bodyParser());
|
|
|
|
// Uploads a file to the data directory and returns the filename
|
|
|
|
// Uploads a file to the data directory and returns the filename
|
|
|
|
this.router.use(async (req, res) => {
|
|
|
|
this.router.use(async (req, res) => {
|
|
|
|
let uploadConfirmation: IUploadConfirmation;
|
|
|
|
let uploadConfirmation: IUploadConfirmation;
|
|
|
|
if (req.session.userId) {
|
|
|
|
if (req.session.userId) {
|
|
|
|
if (req.files.profilePicture) {
|
|
|
|
if (req.files.profilePicture) {
|
|
|
|
uploadConfirmation = await this.uploadProfilePicture(req);
|
|
|
|
uploadConfirmation = await this.uploadProfilePicture(req);
|
|
|
|
|
|
|
|
} else if (req.files.groupPicture) {
|
|
|
|
|
|
|
|
uploadConfirmation = await this.uploadGroupPicture(req);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
res.status(status.BAD_REQUEST);
|
|
|
|
res.status(status.BAD_REQUEST);
|
|
|
|
uploadConfirmation = {
|
|
|
|
uploadConfirmation = {
|
|
|
@ -103,35 +111,119 @@ export class UploadRoute extends Route {
|
|
|
|
let fileName: string;
|
|
|
|
let fileName: string;
|
|
|
|
const profilePic = request.files.profilePicture as UploadedFile;
|
|
|
|
const profilePic = request.files.profilePicture as UploadedFile;
|
|
|
|
try {
|
|
|
|
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);
|
|
|
|
const user = await User.findByPk(request.session.userId);
|
|
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
|
|
fileName = await this.processAndStoreImage(profilePic.data);
|
|
|
|
if (user.profilePicture) {
|
|
|
|
if (user.profilePicture) {
|
|
|
|
const oldProfilePicture = path.join(this.dataDir, path.basename(user.profilePicture));
|
|
|
|
await this.deleteWebImage(user.profilePicture);
|
|
|
|
if (await fsx.pathExists(oldProfilePicture)) {
|
|
|
|
|
|
|
|
await fsx.unlink(oldProfilePicture);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
globals.logger.warn(`Could not delete ${oldProfilePicture}: Not found!`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
user.profilePicture = fileName;
|
|
|
|
user.profilePicture = fileName;
|
|
|
|
await user.save();
|
|
|
|
await user.save();
|
|
|
|
success = true;
|
|
|
|
success = true;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
error = "User not found";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
|
|
globals.logger.error(err.message);
|
|
|
|
|
|
|
|
globals.logger.debug(err.stack);
|
|
|
|
|
|
|
|
error = err.message;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
error,
|
|
|
|
|
|
|
|
fileName,
|
|
|
|
|
|
|
|
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) {
|
|
|
|
} catch (err) {
|
|
|
|
globals.logger.error(err.message);
|
|
|
|
globals.logger.error(err.message);
|
|
|
|
globals.logger.debug(err.stack);
|
|
|
|
globals.logger.debug(err.stack);
|
|
|
|
error = err.message;
|
|
|
|
error = err.message;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
error = "No groupId provided! (the request body must contain a groupId)";
|
|
|
|
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
error,
|
|
|
|
error,
|
|
|
|
fileName,
|
|
|
|
fileName,
|
|
|
|
success,
|
|
|
|
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!`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|