From e33566a04aedeeab97f669aab44c2557fc5fecc6 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 27 May 2022 22:15:35 -0500 Subject: [PATCH] Upload profile picture and convert into webp --- server/src/api-v1/asset/asset.controller.ts | 4 +- server/src/api-v1/auth/auth.service.ts | 2 +- server/src/api-v1/user/user.controller.ts | 17 ++++++++- server/src/api-v1/user/user.service.ts | 33 +++++++++++++++- ...ption.config.ts => asset-upload.config.ts} | 10 +++-- .../src/config/profile-image-upload.config.ts | 38 +++++++++++++++++++ 6 files changed, 94 insertions(+), 10 deletions(-) rename server/src/config/{multer-option.config.ts => asset-upload.config.ts} (86%) create mode 100644 server/src/config/profile-image-upload.config.ts diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts index 068729e43f..2e9820b76d 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/src/api-v1/asset/asset.controller.ts @@ -19,7 +19,7 @@ import { import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; import { AssetService } from './asset.service'; import { FileFieldsInterceptor } from '@nestjs/platform-express'; -import { multerOption } from '../../config/multer-option.config'; +import { assetUploadOption } from '../../config/asset-upload.config'; import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; import { CreateAssetDto } from './dto/create-asset.dto'; import { ServeFileDto } from './dto/serve-file.dto'; @@ -48,7 +48,7 @@ export class AssetController { { name: 'assetData', maxCount: 1 }, { name: 'thumbnailData', maxCount: 1 }, ], - multerOption, + assetUploadOption, ), ) async uploadFile( diff --git a/server/src/api-v1/auth/auth.service.ts b/server/src/api-v1/auth/auth.service.ts index fc45f7c4f0..07eb932530 100644 --- a/server/src/api-v1/auth/auth.service.ts +++ b/server/src/api-v1/auth/auth.service.ts @@ -19,7 +19,7 @@ export class AuthService { private async validateUser(loginCredential: LoginCredentialDto): Promise { const user = await this.userRepository.findOne( { email: loginCredential.email }, - { select: ['id', 'email', 'password', 'salt', 'firstName', 'lastName', 'isAdmin'] }, + { select: ['id', 'email', 'password', 'salt', 'firstName', 'lastName', 'isAdmin', 'profileImagePath', 'isFirstLoggedIn'] }, ); const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt); diff --git a/server/src/api-v1/user/user.controller.ts b/server/src/api-v1/user/user.controller.ts index a59e1a8996..ab32e548f1 100644 --- a/server/src/api-v1/user/user.controller.ts +++ b/server/src/api-v1/user/user.controller.ts @@ -1,11 +1,12 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe, Put, Query } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe, Put, Query, UseInterceptors, UploadedFile } from '@nestjs/common'; import { UserService } from './user.service'; import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; import { CreateUserDto } from './dto/create-user.dto'; import { AdminRolesGuard } from '../../middlewares/admin-role-guard.middleware'; import { UpdateUserDto } from './dto/update-user.dto'; -import { boolean } from 'joi'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { profileImageUploadOption } from '../../config/profile-image-upload.config'; @Controller('user') export class UserController { @@ -35,4 +36,16 @@ export class UserController { async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) { return await this.userService.updateUser(updateUserDto) } + + @UseGuards(JwtAuthGuard) + @UseInterceptors(FileInterceptor('file', profileImageUploadOption)) + @Post('/profile-image') + async createProfileImage(@GetAuthUser() authUser: AuthUserDto, @UploadedFile() fileInfo: Express.Multer.File) { + return await this.userService.createProfileImage(authUser, fileInfo); + } + + @Get('/profile-image/:userId') + async getProfileImage(@Param('assetId') assetId: string) { + + } } diff --git a/server/src/api-v1/user/user.service.ts b/server/src/api-v1/user/user.service.ts index e38a406dcb..0d54ef6cd7 100644 --- a/server/src/api-v1/user/user.service.ts +++ b/server/src/api-v1/user/user.service.ts @@ -6,7 +6,7 @@ import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { UserEntity } from './entities/user.entity'; import * as bcrypt from 'bcrypt'; - +import sharp from 'sharp'; @Injectable() export class UserService { @@ -124,4 +124,35 @@ export class UserService { throw new InternalServerErrorException('Failed to register new user'); } } + + async createProfileImage(authUser: AuthUserDto, fileInfo: Express.Multer.File) { + try { + // Convert file to jpeg + let filePath = '' + const fileSave = await sharp(fileInfo.path).webp().resize(512, 512).toFile(fileInfo.path + '.webp') + + if (fileSave) { + filePath = fileInfo.path + '.webp'; + await this.userRepository.update(authUser.id, { + profileImagePath: filePath + }) + } else { + filePath = fileInfo.path; + await this.userRepository.update(authUser.id, { + profileImagePath: filePath + + }) + } + + + return { + userId: authUser.id, + profileImagePath: filePath + }; + } catch (e) { + Logger.error(e, 'Create User Profile Image'); + throw new InternalServerErrorException('Failed to create new user profile image'); + } + + } } diff --git a/server/src/config/multer-option.config.ts b/server/src/config/asset-upload.config.ts similarity index 86% rename from server/src/config/multer-option.config.ts rename to server/src/config/asset-upload.config.ts index 263e162b45..4a1c269181 100644 --- a/server/src/config/multer-option.config.ts +++ b/server/src/config/asset-upload.config.ts @@ -8,7 +8,7 @@ import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant'; import { randomUUID } from 'crypto'; import { CreateAssetDto } from '../api-v1/asset/dto/create-asset.dto'; -export const multerOption: MulterOptions = { +export const assetUploadOption: MulterOptions = { fileFilter: (req: Request, file: any, cb: any) => { if (file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng|webp)$/)) { cb(null, true); @@ -19,14 +19,14 @@ export const multerOption: MulterOptions = { storage: diskStorage({ destination: (req: Request, file: Express.Multer.File, cb: any) => { - const uploadPath = APP_UPLOAD_LOCATION; + const basePath = APP_UPLOAD_LOCATION; const fileInfo = req.body as CreateAssetDto; const yearInfo = new Date(fileInfo.createdAt).getFullYear(); const monthInfo = new Date(fileInfo.createdAt).getMonth(); if (file.fieldname == 'assetData') { - const originalUploadFolder = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`; + const originalUploadFolder = `${basePath}/${req.user['id']}/original/${req.body['deviceId']}`; if (!existsSync(originalUploadFolder)) { mkdirSync(originalUploadFolder, { recursive: true }); @@ -35,7 +35,7 @@ export const multerOption: MulterOptions = { // Save original to disk cb(null, originalUploadFolder); } else if (file.fieldname == 'thumbnailData') { - const thumbnailUploadFolder = `${uploadPath}/${req.user['id']}/thumb/${req.body['deviceId']}`; + const thumbnailUploadFolder = `${basePath}/${req.user['id']}/thumb/${req.body['deviceId']}`; if (!existsSync(thumbnailUploadFolder)) { mkdirSync(thumbnailUploadFolder, { recursive: true }); @@ -56,3 +56,5 @@ export const multerOption: MulterOptions = { }, }), }; + + diff --git a/server/src/config/profile-image-upload.config.ts b/server/src/config/profile-image-upload.config.ts new file mode 100644 index 0000000000..7ce3e648b7 --- /dev/null +++ b/server/src/config/profile-image-upload.config.ts @@ -0,0 +1,38 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; +import { existsSync, mkdirSync } from 'fs'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import { Request } from 'express'; +import { APP_UPLOAD_LOCATION } from '../constants/upload_location.constant'; + +export const profileImageUploadOption: MulterOptions = { + fileFilter: (req: Request, file: any, cb: any) => { + if (file.mimetype.match(/\/(jpg|jpeg|png|heic|heif|dng|webp)$/)) { + cb(null, true); + } else { + cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false); + } + }, + + storage: diskStorage({ + destination: (req: Request, file: Express.Multer.File, cb: any) => { + const basePath = APP_UPLOAD_LOCATION; + const profileImageLocation = `${basePath}/${req.user['id']}/profile`; + if (!existsSync(profileImageLocation)) { + mkdirSync(profileImageLocation, { recursive: true }); + } + + + cb(null, profileImageLocation); + }, + + filename: (req: Request, file: Express.Multer.File, cb: any) => { + const userId = req.user['id']; + + cb(null, `${userId}`); + }, + }), +}; + +