/* * Copyright (c) 2022. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ import {AuthUserImageType} from "@chamesu/common/domains/auth/user/type"; import {Request} from "express"; import fs from "fs"; import path from "path"; import {v4} from "uuid"; import { createQueueMessageTemplate, publishMessageQueue } from "../../../../modules/message-queue"; import {ImageCropCoordinates, ImageCropResizeConfig} from "@chamesu/common/domains/image/crop"; export async function uploadUserImageWithRequest(request: Request, destPath: string, userId: number) : Promise<{ type: string, path: string, original: string, coordinates: ImageCropCoordinates }>{ const { coordinatesWidth, coordinatesHeight, coordinatesLeft, coordinatesTop } = > request.body; const width : number = parseInt(coordinatesWidth); const height : number = parseInt(coordinatesHeight); const left : number = parseInt(coordinatesLeft); const top : number = parseInt(coordinatesTop); if(Number.isNaN(width) || Number.isNaN(height) || Number.isNaN(left) || Number.isNaN(top)) { throw new Error('Coordinaten des Bildes sind nicht gültig.'); } const coordinates : ImageCropCoordinates = {width, height, left, top} // @ts-ignore const type : string | undefined = !!request.files.cover ? 'cover' : (!!request.files.avatar ? 'avatar' : undefined); if(typeof type === 'undefined') { throw new Error('Es wurde weder ein Avatar noch ein Cover zum Upload angegeben...'); } // @ts-ignore const buffer : Buffer = request.files[type][0].buffer; const mimeTypesAllowed : string[] = [ 'image/jpeg', 'image/png', 'image/jpg', 'image/svg+xml', 'image/tiff', 'image/gif' ]; // @ts-ignore const mimeType : string = request.files[type][0].mimetype; if(mimeTypesAllowed.indexOf(mimeType) === -1) { throw new Error('Der mimetype '+mimeType+' ist nicht als Bildformat erlaubt.'); } const upload = await uploadUserImage(destPath, userId, type, buffer, coordinates); let queueMessage = createQueueMessageTemplate(); queueMessage.type = 'resize'; queueMessage.data = { srcFilePath: upload.filePath, cropCoordinates: coordinates, cropResizeConfig: upload.cropResizeConfig, type: 'user-'+type, id: userId }; publishMessageQueue('image.command', queueMessage).then(r => r); return { type, path: upload.urlPath, original: upload.fileName, coordinates } } export async function uploadUserImage( destPath: string, userId: number, type: AuthUserImageType, image: Buffer, coordinates : ImageCropCoordinates ) : Promise<{ fileName: string, filePath: string, directoryPath: string, urlPath: string, cropResizeConfig: ImageCropResizeConfig }> { const directoryPath : string = path.resolve(destPath, 'user', type, userId.toString()); // clear existing avatar and sizes if(fs.existsSync(directoryPath)) { fs.rmdirSync(directoryPath, {recursive: true}); } try { await fs.promises.stat(directoryPath); await fs.promises.chmod(directoryPath, 0o775); } catch (e) { await fs.promises.mkdir(directoryPath, { mode: 0o775 }); } let cropResizeConfig : ImageCropResizeConfig | undefined; switch (type) { case "avatar": cropResizeConfig = UserTypeConfigs.avatar; break; case "cover": cropResizeConfig = UserTypeConfigs.cover; break; } const ratio : string = (coordinates.width / coordinates.height).toFixed(1); if(ratio !== cropResizeConfig.ratio) { throw new Error('Das Verhältnis des auszuschneidenen Teils ist nicht gültig.'); } const fileName : string = v4()+'.png'; const filePath : string = path.resolve(directoryPath, fileName); fs.writeFileSync(filePath, image); return { fileName, filePath, directoryPath, urlPath: 'static/user/'+type+'/'+userId+'/', cropResizeConfig }; } export const UserTypeConfigs : Record = { avatar: { ratio: '1.0', sizes: [ { name: 'large', width: 400 }, { name: 'medium', width: 200 }, { name: 'small', width: 128 }, { name: 'tiny', width: 64 } ] }, cover: { ratio: '3.6', sizes: [ { name: 'large', width: 3600 }, { name: 'small', width: 1800 }, { name: 'tiny', width: 900 } ] } }