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

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more