Загрузка изображений в cloudinary- модуль для NestJS
Tags: cloud, nest, upload
Published: 2021-07-10 15:21:16

В работе над проектом потребовалось сделать загрузку картинок. Когда я писал "[Кулинарную книгу](https://github.com/SLKarol/cookbook)", то использовал сервис [Сloudinary](http://cloudinary.com/). Но там был express, как дела обстоят в Nest? Готового модуля нет, но зато есть документация по модулям: [Стартовая страница как написать свой модуль](https://docs.nestjs.com/modules). 
Постараюсь по шагам описать свои действия как написал модуль.

### Подготовка настроек
Ну, конечно, сперва нужно в недоступном для случайных зрителей месте сохранить параметры доступа к сервису. Вот что в файле .env:
```
CLOUDINARY_CLOUD_NAME=cloudinaryCloudName
CLOUDINARY_API_KEY=1029384756
CLOUDINARY_API_SECRET=secrectapi
```

### Написание провайдера
Модуль будет хранится в каталоге src/cloudinary .

Для модуля необходим [провайдер](https://docs.nestjs.com/providers). В моём случае всё, что нужно от провайдера- это получение вышенаписанных параметров и передача их сервису. Чуть далее, мы узнаем [как такой провайдер можно создать](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory), поэтому пишем вот такой файл **src/cloudinary/cloudinary.provider.ts**:
```ts
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

export const cloudinaryProvider: Provider = {
  provide: 'CLOUDINARY_MODULE_OPTIONS',
  inject: [ConfigService],
  useFactory: async (configService: ConfigService) => {
    return {
      cloudinaryCloudName: configService.get<string>('CLOUDINARY_CLOUD_NAME'),
      cloudinaryApiKey: configService.get<string>('CLOUDINARY_API_KEY'),
      cloudinaryApiSecret: configService.get<string>('CLOUDINARY_API_SECRET'),
    };
  },
};
```
Содержимое из этого файла должно попасть в [сервис](https://docs.nestjs.com/fundamentals/custom-providers#di-fundamentals), который будет отвечать за загрузку файлов.
Начало этого сервиса будет таким: 
 **src/cloudinary/cloudinary.service.ts**
```ts
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { UploadApiErrorResponse, UploadApiResponse, v2 } from 'cloudinary';

import { CloudinarySettings } from './types/types';


@Injectable()
export class CloudinaryService {
  constructor(@Inject('CLOUDINARY_MODULE_OPTIONS') options: CloudinarySettings) {
    this.cloudinary = v2;
    this.cloudinary.config({
      cloud_name: options.cloudinaryCloudName,
      api_key: options.cloudinaryApiKey,
      api_secret: options.cloudinaryApiSecret,
    });
  }
}
```
**CloudinarySettings** из src/cloudinary/types/types.ts описывает известные нам опции, которые приходят от провайдера:
```ts
export type CloudinarySettings = {
  cloudinaryCloudName: string;
  cloudinaryApiKey: string;
  cloudinaryApiSecret: string;
};
```
И как говорит документация [cloudinary](https://github.com/cloudinary/cloudinary_npm) для загрузки используется объект поток (Stream), но через upload приходят файлы в виде буфера. Вопросом о переводе буфера в поток будет заниматься пакет [buffer-to-stream](https://www.npmjs.com/package/buffer-to-stream), который нужно будет поставить. Естественно нужно поставить ещё пакет **cloudinary** и в dev-зависимость пакет **@types/buffer-to-stream**.
А дальше дело за малым: Написать метод, который будет загружать картинки в облако. У меня для тэгов к изображениям используется тип TypeUpload:
```ts
export type TypeUpload = 'avatar' | 'content';
```
В нём я в тэгах обозначаю, что за картинки загружены: аватар или содержимое. Вот этот сервис, который загружает картинки: **src/cloudinary/cloudinary.service.ts**
```ts
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { UploadApiErrorResponse, UploadApiResponse, v2 } from 'cloudinary';
import toStream = require('buffer-to-stream');

import { CloudinarySettings, TypeUpload } from './types/types';
import { CLOUDINARY_MODULE_OPTIONS } from './cloudinary.constants';
import { TAG_BASE_NAME } from './constants/tagFiles';

@Injectable()
export class CloudinaryService {
  constructor(@Inject(CLOUDINARY_MODULE_OPTIONS) options: CloudinarySettings) {
    v2.config({
      cloud_name: options.cloudinaryCloudName,
      api_key: options.cloudinaryApiKey,
      api_secret: options.cloudinaryApiSecret,
    });
  }

  /**
   * Метод загрузки изображений
   */
  async uploadImage({
    file,
    type = 'avatar',
  }: {
    file: Express.Multer.File;
    type: TypeUpload;
  }): Promise<UploadApiResponse> {
    // Загрузка картинки в облако
    const uploadResponse = await this.uploadFile(file);
    if (this.isUploadApiErrorResponse(uploadResponse)) {
      throw new HttpException(
        'Upload avatar error',
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }
    // После загрузки картинки назначить ей тэг:
    const tag = `${TAG_BASE_NAME}_${type}`;
    const { public_id } = uploadResponse;
    await v2.uploader.add_tag(tag, [public_id]);
    return uploadResponse;
  }

  /**
   * Загрузка файла в сервис cloudinary
   */
  private async uploadFile(
    file: Express.Multer.File,
  ): Promise<UploadApiResponse | UploadApiErrorResponse> {
    if (!file) {
      throw new HttpException(
        'File not present!',
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }
    return new Promise((resolve, reject) => {
      const upload = v2.uploader.upload_stream((error, result) => {
        if (error) return reject(error);
        resolve(result);
      });
      // Буфер в виде потока отправить в функцию upload
      toStream(file.buffer).pipe(upload);
    });
  }

  /**
   * Проверка результата загрузки на наличие ошибок
   * Т.е. - проверка типа: Это UploadApiErrorResponse ?
   */
  private isUploadApiErrorResponse(
    response: UploadApiErrorResponse | UploadApiResponse,
  ): response is UploadApiErrorResponse {
    return (<UploadApiErrorResponse>response).http_code !== undefined;
  }
}
```
Ну и модуль, который который всё сводит в единую сущность:
```ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

import { cloudinaryProvider } from './cloudinary.provider';
import { CloudinaryService } from './cloudinary.service';

@Module({
  imports: [ConfigModule],
  providers: [CloudinaryService, cloudinaryProvider],
  exports: [CloudinaryService],
})
export class CloudinaryModule {}
```
Исходный код можно увидеть на [гитхаб](https://github.com/SLKarol/post-life) - в этом репозитории есть этот самый модуль, он подключен и работает.