DEV Community

Leonardo Minora
Leonardo Minora

Posted on • Edited on

NestJS - Armazenamento local de upload

Informações gerais

objetivo

  • criar 1 endpoint de upload de 1 arquivo com armazenamento em disco local
  • criar 1 endpoint de download de 1 arquivo que foi armazenado em disco local

notas de aula

sumário

  1. pegar o código base
  2. acessar pasta do projeto e instalar bibliotecas do projeto
  3. executar a api
  4. incluir módulo nestjs para os novos endpoints
  5. codar o upload de arquivo com armazenamento local
  6. codar o download de arquivo com armazenamento local
  7. codar para adicionar exceção para arquivo não encontrado

1. pegar o código base

pode utilizar o seu próprio código, ou baixar o zip ou fazer o clone do repositório github com o código-fonte do projeto da nota de aula anterior.

lembrando que fazendo o clone do repositório github, precisará executar na pasta do projeto o comando git checkout -b 02-upload-arquivos-multiplos origin/02-upload-arquivos-multiplos.

2. acessar pasta do projeto e instalar bibliotecas do projeto

[upload-api] $ npm install

Enter fullscreen mode Exit fullscreen mode

3. executar a api

[upload-api] $ npm run start:dev

Enter fullscreen mode Exit fullscreen mode

4. incluir módulo nestjs para os novos endpoints

[upload-api] $ npx @nestjs/cli generate resource armazenamento --no-spec

Enter fullscreen mode Exit fullscreen mode
[15:08:33] Starting compilation in watch mode...

[15:08:35] Found 0 errors. Watching for file changes.

[Nest] 134297  - 17/09/2024, 15:08:35     LOG [NestFactory] Starting Nest application...
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [InstanceLoader] AppModule dependencies initialized +21ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [InstanceLoader] ArmazenamentoModule dependencies initialized +0ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [InstanceLoader] UploadModule dependencies initialized +0ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [RoutesResolver] AppController {/}: +18ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [RoutesResolver] UploadController {/upload}: +0ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [RouterExplorer] Mapped {/upload/exemplo-simples, POST} route +1ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [RouterExplorer] Mapped {/upload/arquivos, POST} route +0ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [RoutesResolver] ArmazenamentoController {/armazenamento}: +1ms
[Nest] 134297  - 17/09/2024, 15:08:35     LOG [NestApplication] Nest application successfully started +2ms

Enter fullscreen mode Exit fullscreen mode

5. codar o upload de arquivo com armazenamento local

objetivo: criar o endpoint para upload de 1 arquivo com documentação swagger.

modificar o arquivo src/armazenamento/armazenamento.controller.ts

--import { Controller } from '@nestjs/common';
++import {
++  Controller,
++  Post,
++  UploadedFile,
++  UseInterceptors,
++} from '@nestjs/common';
++import { ArmazenamentoService } from './armazenamento.service';
++import {
++  ApiBody,
++  ApiConsumes,
++  ApiOperation,
++  ApiResponse,
++  ApiTags,
++} from '@nestjs/swagger';
++import { FileInterceptor } from '@nestjs/platform-express';
++
++@Controller('armazenamento')
++@ApiTags('armazenamento')
export class ArmazenamentoController {
  constructor(private readonly armazenamentoService: ArmazenamentoService) {}
++
++  @Post()
++  @UseInterceptors(FileInterceptor('imagem'))
++  @ApiConsumes('multipart/form-data')
++  @ApiBody({
++    schema: {
++      type: 'object',
++      properties: {
++        imagem: {
++          type: 'string',
++          format: 'binary',
++        },
++      },
++    },
++  })
++  @ApiOperation({ summary: 'Upload de arquivo com armazenamento' })
++  @ApiResponse({ status: 201, description: 'Arquivo enviado com sucesso.' })
++  @ApiResponse({ status: 400, description: 'Erro no envio do arquivo.' })
++  salvar(@UploadedFile() arq: Express.Multer.File) {
++    return {estato: 'ok'};
++  }
}

Enter fullscreen mode Exit fullscreen mode

após salvar o arquivo src/armazenamento/armazenamento.controller.ts, o terminal onde esta executando a API deve parecer com o console abaixo.
Note que foi adicionado mais um endpoint Mapped {/armazenamento, POST}

[19:37:49] File change detected. Starting incremental compilation...

[19:37:49] Found 0 errors. Watching for file changes.

[Nest] 155823  - 17/09/2024, 19:37:50     LOG [NestFactory] Starting Nest application...
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [InstanceLoader] AppModule dependencies initialized +23ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [InstanceLoader] UploadModule dependencies initialized +0ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [InstanceLoader] ArmazenamentoModule dependencies initialized +0ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [RoutesResolver] AppController {/}: +16ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [RoutesResolver] UploadController {/upload}: +0ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [RouterExplorer] Mapped {/upload/exemplo-simples, POST} route +1ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [RouterExplorer] Mapped {/upload/arquivos, POST} route +1ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [RoutesResolver] ArmazenamentoController {/armazenamento}: +0ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [RouterExplorer] Mapped {/armazenamento, POST} route +0ms
[Nest] 155823  - 17/09/2024, 19:37:50     LOG [NestApplication] Nest application successfully started +2ms

Enter fullscreen mode Exit fullscreen mode

para essa versão do endpoint, o exemplo de teste com a documentação swagger na figura abaixo e o resultado no console, também, abaixo.

{
  "estato": "ok"
}
Enter fullscreen mode Exit fullscreen mode

objetivo: armazenar o arquivo recebido em disco local.

modificar o arquivo src/armazenamento/armazenamento.service.ts

import { Injectable } from '@nestjs/common';
++import { promises as fs } from 'fs';
++import * as path from 'path';
++
++class ImagemDto {
++  id: string;
++  nome: string;
++  tamanho: number;
++  mimetype: string;
++  encoding: string;
++  armazenamento: string;
++}

@Injectable()
export class ArmazenamentoService {
++  dir = path.join(__dirname, '..', '..', 'uploads');
++  imagens = new Array<ImagemDto>();

++  async salvarEmDisco(arquivo: Express.Multer.File) {
++    const nomeCompleto = path.join(this.dir, arquivo.originalname);
++
++    await fs.writeFile(nomeCompleto, arquivo.buffer);
++    this.imagens.push({
++      id: arquivo.originalname,
++      nome: arquivo.originalname,
++      tamanho: arquivo.size,
++      mimetype: arquivo.mimetype,
++      encoding: arquivo.encoding,
++      armazenamento: nomeCompleto,
++    });
++    return {
++      estado: 'ok',
++      dados: {
++        id: arquivo.originalname,
++        nome: arquivo.originalname,
++      },
++    };
++  }
}

Enter fullscreen mode Exit fullscreen mode

objetivo: ligar o endpoint (controller) ao processamento de armazenamento (service).

modificar o arquivo src/armazenamento/armazenamento.controller.ts

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { ArmazenamentoService } from './armazenamento.service';
import {
  ApiBody,
  ApiConsumes,
  ApiOperation,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('armazenamento')
@ApiTags('armazenamento')
export class ArmazenamentoController {
  constructor(private readonly armazenamentoService: ArmazenamentoService) {}

  @Post()
  @UseInterceptors(FileInterceptor('imagem'))
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        imagem: {
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  @ApiOperation({ summary: 'Upload de arquivo com armazenamento' })
  @ApiResponse({ status: 201, description: 'Arquivo enviado com sucesso.' })
  @ApiResponse({ status: 400, description: 'Erro no envio do arquivo.' })
  salvar(@UploadedFile() arq: Express.Multer.File) {
--    return {estato: 'ok'};
++    return this.armazenamentoService.salvarEmDisco(arq);
  }
}
Enter fullscreen mode Exit fullscreen mode

para a versão final do endpoint, o exemplo de teste com a documentação swagger na figura abaixo e o resultado no console, também, abaixo.

{
  "estado": "ok",
  "dados": {
    "id": "Captura de tela de 2024-09-17 15-12-17.png",
    "nome": "Captura de tela de 2024-09-17 15-12-17.png"
  }
}
Enter fullscreen mode Exit fullscreen mode

6. codar o download de arquivo com armazenamento local

objetivo: criar o endpoint para upload de 1 arquivo com documentação swagger.

modificar o arquivo src/armazenamento/armazenamento.controller.ts

import {
  Controller,
++  Get,
++  Param,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { ArmazenamentoService } from './armazenamento.service';
import {
  ApiBody,
  ApiConsumes,
  ApiOperation,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('armazenamento')
@ApiTags('armazenamento')
export class ArmazenamentoController {
  constructor(private readonly armazenamentoService: ArmazenamentoService) {}

  @Post()
  @UseInterceptors(FileInterceptor('imagem'))
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        imagem: {
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  @ApiOperation({ summary: 'Upload de arquivo com armazenamento' })
  @ApiResponse({ status: 201, description: 'Arquivo enviado com sucesso.' })
  @ApiResponse({ status: 400, description: 'Erro no envio do arquivo.' })
  salvar(@UploadedFile() arq: Express.Multer.File) {
    return this.armazenamentoService.salvarEmDisco(arq);
  }
++
++  @Get(':nome')
++  @ApiOperation({ summary: 'Endpoint para receber arquivo' })
++  @ApiResponse({ status: 201, description: 'Arquivo enviado com sucesso.' })
++  @ApiResponse({ status: 400, description: 'Erro no envio do arquivo.' })
++  ler(@Param('nome') nome: string) {
++    return { estado: 'ok' };
++  }
}

Enter fullscreen mode Exit fullscreen mode

após salvar o arquivo src/armazenamento/armazenamento.controller.ts, o terminal onde esta executando a API deve parecer com o console abaixo.
Note que foi adicionado mais um endpoint Mapped {/armazenamento/:nome, GET}

[19:51:57] File change detected. Starting incremental compilation...

[19:51:57] Found 0 errors. Watching for file changes.

[Nest] 156968  - 17/09/2024, 19:51:58     LOG [NestFactory] Starting Nest application...
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [InstanceLoader] AppModule dependencies initialized +12ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [InstanceLoader] UploadModule dependencies initialized +1ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [InstanceLoader] ArmazenamentoModule dependencies initialized +0ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [RoutesResolver] AppController {/}: +15ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [RoutesResolver] UploadController {/upload}: +0ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [RouterExplorer] Mapped {/upload/exemplo-simples, POST} route +1ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [RouterExplorer] Mapped {/upload/arquivos, POST} route +0ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [RoutesResolver] ArmazenamentoController {/armazenamento}: +1ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [RouterExplorer] Mapped {/armazenamento, POST} route +0ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [RouterExplorer] Mapped {/armazenamento/:nome, GET} route +0ms
[Nest] 156968  - 17/09/2024, 19:51:58     LOG [NestApplication] Nest application successfully started +5ms

Enter fullscreen mode Exit fullscreen mode

para essa versão do endpoint, o exemplo de teste com a documentação swagger na figura abaixo e o resultado no console, também, abaixo.

{
  "estato": "ok"
}
Enter fullscreen mode Exit fullscreen mode

objetivo: ler o arquivo do disco local.

modificar o arquivo src/armazenamento/armazenamento.service.ts

import { Injectable } from '@nestjs/common';
import { promises as fs } from 'fs';
import * as path from 'path';

class ImagemDto {
  id: string;
  nome: string;
  tamanho: number;
  mimetype: string;
  encoding: string;
  armazenamento: string;
}

@Injectable()
export class ArmazenamentoService {
  dir = path.join(__dirname, '..', '..', 'uploads');
  imagens = new Array<ImagemDto>();

  async salvarEmDisco(arquivo: Express.Multer.File) {
    const nomeCompleto = path.join(this.dir, arquivo.originalname);

    await fs.writeFile(nomeCompleto, arquivo.buffer);
    this.imagens.push({
      id: arquivo.originalname,
      nome: arquivo.originalname,
      tamanho: arquivo.size,
      mimetype: arquivo.mimetype,
      encoding: arquivo.encoding,
      armazenamento: nomeCompleto,
    });
    return {
      estado: 'ok',
      dados: {
        id: arquivo.originalname,
        nome: arquivo.originalname,
      },
    };
  }
++
++  async pegar(nome: string) {
++    const imagem = this.imagens.filter((item) => item.id === nome)[0];
++    try {
++      const arquivo = await fs.readFile(imagem.armazenamento);
++      return {
++        estado: 'ok',
++        dados: {
++          informacao: imagem,
++          buffer: arquivo,
++        },
++      };
++    } catch (erro) {
++      return null;
++    }
++  }
}

Enter fullscreen mode Exit fullscreen mode

objetivo: ligar o endpoint (controller) ao processamento de armazenamento (service).

modificar o arquivo src/armazenamento/armazenamento.controller.ts

import {
  Controller,
  Get,
  Param,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { ArmazenamentoService } from './armazenamento.service';
import {
  ApiBody,
  ApiConsumes,
  ApiOperation,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('armazenamento')
@ApiTags('armazenamento')
export class ArmazenamentoController {
  constructor(private readonly armazenamentoService: ArmazenamentoService) {}

  @Post()
  @UseInterceptors(FileInterceptor('imagem'))
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        imagem: {
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  @ApiOperation({ summary: 'Upload de arquivo com armazenamento' })
  @ApiResponse({ status: 201, description: 'Arquivo enviado com sucesso.' })
  @ApiResponse({ status: 400, description: 'Erro no envio do arquivo.' })
  salvar(@UploadedFile() arq: Express.Multer.File) {
    return this.armazenamentoService.salvarEmDisco(arq);
  }

  @Get(':nome')
  @ApiOperation({ summary: 'Endpoint para receber arquivo' })
  @ApiResponse({ status: 200, description: 'Arquivo enviado com sucesso.' })
  @ApiResponse({ status: 404, description: 'Erro no envio do arquivo.' })
  ler(@Param('nome') nome: string) {
--    return { estado: 'ok' };
++    return this.armazenamentoService.pegar(nome);
  }
}

Enter fullscreen mode Exit fullscreen mode

para a versão final do endpoint, o exemplo de teste com a documentação swagger na figura abaixo e o resultado no console, também, abaixo.

{
  "estado":"ok",
  "dados": {
      "informacao": {
        "id":"diatinf.png",
        "nome":"diatinf.png",
        "tamanho":132200,
        "mimetype":"image/png",
        "encoding":"7bit",
        "armazenamento":"/home/minora/minora/2024/upload-api/uploads/diatinf.png"},
        "buffer":{
          "type":"Buffer",
          "data": [137,80,78,71,13...]
        }
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

7. codar para adicionar exceção para arquivo não encontrado

a imagem abaixo mostra como é respondido pela API quando um arquivo não foi encontrado.
isso ocorre porque em service retorna null mas não é tratado devidamende em controller.

objetivo: lançar exceção quando o arquivo requisitado não for encontrado.

modificar o arquivo src/armazenamento/armazenamento.controller.ts

import {
  Controller,
  Get,
++  NotFoundException,
  Param,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import {
  ApiBody,
  ApiConsumes,
  ApiOperation,
  ApiResponse,
  ApiTags,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { ArmazenamentoService } from './armazenamento.service';

@Controller('armazenamento')
@ApiTags('armazenamento')
export class ArmazenamentoController {
  constructor(private readonly armazenamentoService: ArmazenamentoService) {}

  @Post()
  @UseInterceptors(FileInterceptor('imagem'))
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        imagem: {
          type: 'string',
          format: 'binary',
        },
      },
    },
  })
  @ApiOperation({ summary: 'Upload de arquivo com armazenamento' })
  @ApiResponse({ status: 201, description: 'Arquivo enviado com sucesso.' })
  @ApiResponse({ status: 400, description: 'Erro no envio do arquivo.' })
  salvar(@UploadedFile() arq: Express.Multer.File) {
    return this.armazenamentoService.salvarEmDisco(arq);
  }

  @Get(':nome')
  @ApiOperation({ summary: 'Endpoint para receber arquivo' })
  @ApiResponse({ status: 200, description: 'Arquivo enviado com sucesso.' })
  @ApiResponse({ status: 404, description: 'Arquivo não encontrado.' })
--  ler(@Param('nome') nome: string) {
++  async ler(@Param('nome') nome: string) {
--    return this.armazenamentoService.pegar(nome);
++    const resposta = await this.armazenamentoService.pegar(nome);
++    if (resposta) return resposta;
++    throw new NotFoundException('Arquivo não encontrado.');
  }
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)