001 Загрузка файлов

Очень хорошей практикой будет реализовать сервер, который примет изображения и будет сам на фронт отправлять их в формат webp

Тут мы так же сохраняем архитектуру классического сервера:

  • контроллер хранит логику принятия и отправки результатов запросов посредством использования методов из сервиса
  • сервис же выполняет конкретную логику для выполнения задачи
nest g module files
nest g controller files --no-spec
nest g service files --no-spec

Данный модуль позволит нам типизировать файлы

npm i -D @types/multer

Данный модуль позволит нам упростить работу с встроенным в ноду функционалом fs (например, не писать проверки на существование папки)

npm i fs-extra
npm i -D @types/fs-extra

Данный модуль позволит нам вне зависимости от ОС определять корневую папку корректно

npm i app-root-path
npm i -D @types/app-root-path

Далее нам нужно будет реализовать функционал для генерации папки с именем дня, чтобы складывать в неё изображения

npm i date-fns

Первым делом нужно описать тот ответ, который мы должны отправить на клиент. Он будет содержать ссылку до изображения и его имя

src > files > dto > file-element.response.ts

export class FileElementResponse {
	url: string;
	name: string;
}

Interceptor - это декторатор в несте, который позволяет перехватить запрос и (например) воспользоваться напрямую функцией для работы с файлами

Конкретно можно обернуть контроллер в @UseInterceptors(), в который вложим FileInterceptor('files') ('files' - это имя мультиплатформы, в которой лежит файл), который уже и предоставит нам возможность работать с методами файлов

Метод uploadFiles будет принимать в себя файлы-изображения и сохранять их на сервере

src > files > files.controller.ts

import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common';
import { HttpCode } from '@nestjs/common/decorators';
import { JwtAuthGuard } from '../auth/guards/jwt.guard';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileElementResponse } from './dto/file-element.response';
import { FilesService } from './files.service';
 
@Controller('files')
export class FilesController {
	constructor(private readonly filesService: FilesService) {}
 
	// загружаем файлы на сервер и сохраняем их
	@Post('upload')
	@HttpCode(200)
	@UseGuards(JwtAuthGuard)
	@UseInterceptors(FileInterceptor('files'))
	async uploadFiles(@UploadedFile() file: Express.Multer.File): Promise<FileElementResponse[]> {
		return this.filesService.saveFiles([file]);
	}
}

Далее нам нужно реализовать сервис:

  • генерируем текущую дату с помощью format
  • создаём строку папки, которая будет использоваться для генерации папки с изображениями
  • ensureDir - данная функция обеспечивает создания дирректории (если папка есть, то хорошо, а если нет, то она будет создана)
  • далее нам нужно будет перебрать полученный массив файлов и записать их все на диск, а так же запушить все пути в массив

src > files > files.service.ts

import { Injectable } from '@nestjs/common';
import { FileElementResponse } from './dto/file-element.response';
import { format } from 'date-fns';
import { path } from 'app-root-path';
import { ensureDir, writeFile } from 'fs-extra';
 
@Injectable()
export class FilesService {
	async saveFiles(files: Express.Multer.File[]): Promise<FileElementResponse[]> {
		// генерируем текущую дату
		const dateFolder = format(new Date(), 'yyyy-MM-dd');
 
		// папка, куда будем загружать изображения
		const uploadFolder = `${path}/uploads/${dateFolder}`;
 
		// обеспечиваем создание папки
		await ensureDir(uploadFolder);
 
		// это тот ответ с ссылками, который нужно вернуть из метода
		const res: FileElementResponse[] = [];
 
		// проходимся по массиву полученных файлов
		for (const file of files) {
			// записываем файлы в нужную нам папку
			await writeFile(`${uploadFolder}/${file.originalname}`, file.buffer);
			// пушим ссылки до файлов в массив ответа из метода
			res.push({
				url: `/uploads/${dateFolder}/${file.originalname}`,
				name: file.originalname,
			});
		}
 
		return res;
	}
}

Далее совершаем запрос с представленными параетрами:

И получаем в ответ массив ссылок на изображения:

Пометка

Данные строки в запросах должны быть равны при отправке файла

002 Конвертация изображений

Данная библиотечка очень оптимальная для использования в оверлоаде, так как она крайне быстро может оптимизировать и выдать нужное нам сконвертированное изображение

npm i sharp
npm i -D @types/sharp

Далее мы должны реализовать наш мини-тип, который поможет нам удобнее пользоваться типизацией файлов (внутренним экспрессовским и нашим классом).

Работает это потому, что наши используемые поля из обоих типов пересекаются

src > files > mfile.class.ts

export class MFile {
	originalname: string;
	buffer: Buffer;
 
	constructor(file: Express.Multer.File | MFile) {
		this.originalname = file.originalname;
		this.buffer = file.buffer;
	}
}

Далее добавляем метод convertToWebP, который будет нам конвертировать изображения в WebP

src > files > files.service.ts

import { Injectable } from '@nestjs/common';
import { FileElementResponse } from './dto/file-element.response';
import { format } from 'date-fns';
import { path } from 'app-root-path';
import { ensureDir, writeFile } from 'fs-extra';
import * as sharp from 'sharp';
import { MFile } from './mfile.class';
 
@Injectable()
export class FilesService {
	// меняем тип принимаемого файла
	async saveFiles(files: MFile[]): Promise<FileElementResponse[]> {
		const dateFolder = format(new Date(), 'yyyy-MM-dd');
		const uploadFolder = `${path}/uploads/${dateFolder}`;
		await ensureDir(uploadFolder);
		const res: FileElementResponse[] = [];
		for (const file of files) {
			await writeFile(`${uploadFolder}/${file.originalname}`, file.buffer);
			res.push({
				url: `/uploads/${dateFolder}/${file.originalname}`,
				name: file.originalname,
			});
		}
		return res;
	}
 
	// функция конвертации
	convertToWebP(file: Buffer): Promise<Buffer> {
		return sharp(file).webp().toBuffer();
	}
}

Далее нам нужно положить файлы в массив и проверить, приходит ли нам изображение или нет. Если пришло изображение, то добавляем его в массив с использованием нашей типизации через MFile

src > files > files.controller.ts

import {
	Controller,
	HttpCode,
	Post,
	UploadedFile,
	UseGuards,
	UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtAuthGuard } from 'src/auth/guards/jwt.guard';
import { FileElementResponse } from './dto/file-element.response';
import { FilesService } from './files.service';
import { MFile } from './mfile.class';
 
@Controller('files')
export class FilesController {
	constructor(private readonly filesService: FilesService) {}
 
	@Post('upload')
	@HttpCode(200)
	@UseGuards(JwtAuthGuard)
	@UseInterceptors(FileInterceptor('files'))
	async uploadFiles(@UploadedFile() file: Express.Multer.File): Promise<FileElementResponse[]> {
		// сохраняем в контроллер массив изображений
		const saveArray: MFile[] = [new MFile(file)];
 
		// проверяем, является ли файл изображением
		if (file.mimetype.includes('image')) {
			const webp = await this.filesService.convertToWebP(file.buffer);
			saveArray.push(
				new MFile({
					originalname: `${file.originalname.split('.')[0]}.webp`,
					buffer: webp,
				}),
			);
		}
 
		// возвращаем ссылки на сохранённые файлы на клиент
		return this.filesService.saveFiles(saveArray);
	}
}

Так же у нас на сервере сохраняются изображения (ещё с прошлого урока) в двойном экземпляре

003 Serve файлов

Для сёрва наших файлов на клиент нам потребуется один модуль из неста:

npm i @nestjs/serve-static

Далее просто импортируем данный модуль и указываем корневую папку, из которой мы сможем получать нужные нам данные

src > files > files.module.ts

import { Module } from '@nestjs/common';
import { FilesController } from './files.controller';
import { FilesService } from './files.service';
import { ServeStaticModule } from '@nestjs/serve-static';
import { path } from 'app-root-path';
 
@Module({
	imports: [
		ServeStaticModule.forRoot({
			rootPath: `${path}/uploads`,
		}),
	],
	controllers: [FilesController],
	providers: [FilesService],
})
export class FilesModule {}

И по такому запросу (без api/upload) мы можем получить нужные нам данные

Так же мы можем указать кастомный корень для получения наших изображений (например, если у нас несколько модулей для раздачи файлов)

src > files > files.module.ts

@Module({  
   imports: [  
      ServeStaticModule.forRoot({  
         rootPath: `${path}/uploads`,  
         serveRoot: '/static',  
      }),  
   ],  
   controllers: [FilesController],  
   providers: [FilesService],  
})  
export class FilesModule {}

И теперь мы можем по нему получать данные с сервера