diff --git a/src/helpers/app-constants.ts b/src/helpers/app-constants.ts index 198cdbe2c..659946f63 100644 --- a/src/helpers/app-constants.ts +++ b/src/helpers/app-constants.ts @@ -3,4 +3,4 @@ import * as path from 'path'; export const MAX_PROFILE_PICTURE_SIZE = 2 * 1024 * 1024; export const VALID_UPLOADS_MIME_TYPES = ['image/jpeg', 'image/png']; export const BASE_URL = "https://staging.api-nestjs.boilerplate.hng.tech"; -export const PROFILE_PHOTO_UPLOADS = path.join(__dirname, '..', 'uploads') \ No newline at end of file +export const PROFILE_PHOTO_UPLOADS = path.join(__dirname, '..', 'uploads') \ No newline at end of file diff --git a/src/modules/profile/profile.service.ts b/src/modules/profile/profile.service.ts index 56c57afaf..525d243f5 100644 --- a/src/modules/profile/profile.service.ts +++ b/src/modules/profile/profile.service.ts @@ -3,6 +3,7 @@ import { HttpStatus, Injectable, InternalServerErrorException, + Logger, NotFoundException, } from '@nestjs/common'; import { Profile } from './entities/profile.entity'; @@ -11,12 +12,13 @@ import { InjectRepository } from '@nestjs/typeorm'; import { User } from '../user/entities/user.entity'; import { UpdateProfileDto } from './dto/update-profile.dto'; import * as sharp from 'sharp'; -import * as fs from 'fs/promises'; +import * as fs from 'fs'; import * as path from 'path'; import { CustomHttpException } from '../../helpers/custom-http-filter'; import * as SYS_MSG from '../../helpers/SystemMessages'; import { UploadProfilePicDto } from './dto/upload-profile-pic.dto'; import { PROFILE_PHOTO_UPLOADS } from '../../helpers/app-constants'; +import { pipeline, Readable } from 'stream'; @Injectable() export class ProfileService { @@ -41,7 +43,7 @@ export class ProfileService { const userProfile = await this.userRepository.findOne({ where: { id: userId }, - relations: ['profile'] + relations: ['profile'], }); const profile = userProfile.profile; @@ -130,7 +132,7 @@ export class ProfileService { throw new CustomHttpException(SYS_MSG.USER_NOT_FOUND, HttpStatus.NOT_FOUND); } - const profile = user.profile + const profile = user.profile; if (!profile) { throw new CustomHttpException(SYS_MSG.PROFILE_NOT_FOUND, HttpStatus.NOT_FOUND); } @@ -139,8 +141,8 @@ export class ProfileService { const previousFilePath = path.join(this.uploadsDir, path.basename(profile.profile_pic_url)); try { - await fs.access(previousFilePath); - await fs.unlink(previousFilePath); + await fs.promises.access(previousFilePath); + await fs.promises.unlink(previousFilePath); } catch (error) { if (error.code === 'ENOENT') { console.error(SYS_MSG.PROFILE_PIC_NOT_FOUND, previousFilePath); @@ -150,38 +152,39 @@ export class ProfileService { } } - const fileExtension = path.extname(uploadProfilePicDto.file.originalname); const fileName = `${userId}${fileExtension}`; const filePath = path.join(this.uploadsDir, fileName); - const writeStream = await fs.open(filePath, 'w'); - try { - await writeStream.writeFile(uploadProfilePicDto.file.buffer); - await writeStream.close(); - } catch (error) { - await writeStream.close(); - throw new CustomHttpException(SYS_MSG.FILE_SAVE_ERROR, HttpStatus.INTERNAL_SERVER_ERROR); - } - + const fileStream = Readable.from(uploadProfilePicDto.file.buffer); + const writeStream = fs.createWriteStream(filePath); - await sharp(uploadProfilePicDto.file.buffer).resize({ width: 200, height: 200 }).toFile(filePath); + return new Promise((resolve, reject) => { + pipeline(fileStream, writeStream, async err => { + if (err) { + Logger.error(SYS_MSG.FILE_SAVE_ERROR, err.stack); + reject(new CustomHttpException(SYS_MSG.FILE_SAVE_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)); + } else { + await sharp(uploadProfilePicDto.file.buffer).resize({ width: 200, height: 200 }).toFile(filePath); - profile.profile_pic_url = `${baseUrl}/uploads/${fileName}`; + profile.profile_pic_url = `${baseUrl}/uploads/${fileName}`; - await this.profileRepository.update(profile.id, profile); - const updatedProfile = await this.profileRepository.findOne({ where: { id: profile.id } }); + await this.profileRepository.update(profile.id, profile); + const updatedProfile = await this.profileRepository.findOne({ where: { id: profile.id } }); + resolve({ + status: HttpStatus.OK, + message: SYS_MSG.PICTURE_UPDATED, + data: { profile_picture_url: updatedProfile.profile_pic_url }, + }); + } + }); + }); - return { - status: HttpStatus.OK, - message: SYS_MSG.PICTURE_UPDATED, - data: { profile_picture_url: updatedProfile.profile_pic_url }, - }; } private async createUploadsDirectory() { try { - await fs.mkdir(this.uploadsDir, { recursive: true }); + await fs.promises.mkdir(this.uploadsDir, { recursive: true }); } catch (error) { console.error(SYS_MSG.ERROR_DIRECTORY, error); } diff --git a/src/modules/profile/tests/profile.service.spec.ts b/src/modules/profile/tests/profile.service.spec.ts index 9443e500e..d1c74ee22 100644 --- a/src/modules/profile/tests/profile.service.spec.ts +++ b/src/modules/profile/tests/profile.service.spec.ts @@ -6,15 +6,12 @@ import { Profile } from '../entities/profile.entity'; import { NotFoundException, InternalServerErrorException, HttpStatus } from '@nestjs/common'; import { User } from '../../user/entities/user.entity'; import { UpdateProfileDto } from '../dto/update-profile.dto'; -import * as path from 'path'; -import * as fs from 'fs/promises'; -import { FileHandle } from 'fs/promises'; +import * as fs from 'fs'; import * as sharp from 'sharp'; import { CustomHttpException } from '../../../helpers/custom-http-filter'; import { PICTURE_UPDATED } from '../../../helpers/SystemMessages'; import { mockUser } from '../../../modules/invite/mocks/mockUser'; import { mockUserWithProfile } from '../mocks/mockUser'; -jest.mock('fs/promises'); jest.mock('sharp'); describe('ProfileService', () => { let service: ProfileService; @@ -193,10 +190,7 @@ describe('ProfileService', () => { originalname: 'test.jpg', }; const mockUploadProfilePicDto = { file: mockFile as any }; - const mockFileHandle = { - writeFile: jest.fn().mockResolvedValue(undefined), - close: jest.fn().mockResolvedValue(undefined), - } as unknown as FileHandle; + it('should throw an exception if no file is provided', async () => { await expect(service.uploadProfilePicture(userId, { file: null }, baseUrl)).rejects.toThrow(CustomHttpException); @@ -222,7 +216,6 @@ describe('ProfileService', () => { }); it('should delete previous profile picture if it exists', async () => { - jest.spyOn(fs, 'open').mockResolvedValue(mockFileHandle); jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); @@ -235,8 +228,8 @@ describe('ProfileService', () => { toFile: jest.fn().mockResolvedValue(undefined), } as any); - const mockUnlink = jest.spyOn(fs, 'unlink').mockResolvedValue(undefined); - const mockAccess = jest.spyOn(fs, 'access').mockResolvedValue(undefined); + const mockUnlink = jest.spyOn(fs.promises, 'unlink').mockResolvedValue(undefined); + const mockAccess = jest.spyOn(fs.promises, 'access').mockResolvedValue(undefined); await service.uploadProfilePicture(userId, mockUploadProfilePicDto, baseUrl); @@ -245,7 +238,6 @@ describe('ProfileService', () => { }); it('should handle non-existent previous profile picture', async () => { - jest.spyOn(fs, 'open').mockResolvedValue(mockFileHandle); const mockResult: UpdateResult = { generatedMaps: [], @@ -256,7 +248,7 @@ describe('ProfileService', () => { jest.spyOn(profileRepository, 'update').mockResolvedValue(mockResult); jest.spyOn(profileRepository, 'findOne').mockResolvedValue(mockUser.profile); - (fs.access as jest.Mock).mockRejectedValue({ code: 'ENOENT' }); + (fs.promises.access as jest.Mock).mockRejectedValue({ code: 'ENOENT' }); (sharp as jest.MockedFunction).mockReturnValue({ resize: jest.fn().mockReturnThis(), @@ -268,8 +260,6 @@ describe('ProfileService', () => { it('should save new profile picture and update profile', async () => { - jest.spyOn(fs, 'open').mockResolvedValue(mockFileHandle); - jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser); (sharp as jest.MockedFunction).mockReturnValue({ diff --git a/src/uploads/testUserId.jpg b/src/uploads/testUserId.jpg new file mode 100644 index 000000000..30d74d258 --- /dev/null +++ b/src/uploads/testUserId.jpg @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/uploads/testUserId.jpg b/uploads/testUserId.jpg new file mode 100644 index 000000000..30d74d258 --- /dev/null +++ b/uploads/testUserId.jpg @@ -0,0 +1 @@ +test \ No newline at end of file