똑같은 삽질은 2번 하지 말자

tus + Cloudflare stream을 이용한 영상 업로드 (Vue + NestJS) 본문

카테고리 없음

tus + Cloudflare stream을 이용한 영상 업로드 (Vue + NestJS)

곽빵 2023. 6. 16. 21:31

개요

영상 업로드 문제를 해결하기 위해 tus라는 프로토콜을 이용해 해결했었던 것에 대해 글로 남기고자 한다.

 

tus란?

TUS는 HTTP 기반의 파일 업로드 프로토콜로, 큰 파일이나 불안정한 네트워크 연결 상황에서도 안정적인 파일 업로드를 가능하게 한다. 파일을 여러 부분으로 나누어 업로드하며, 실패한 업로드는 재시작할 수 있으며, 다양한 프로그래밍 언어와 클라우드 스토리지 서비스와 호환되는 라이브러리를 제공

 

그렇다! 이미 제공되는 라이브러리가 많으며, 이를 이용해 빠르게 개발해 보자. (참고로 클라이언트(Vue) + 서버(Nest) 이다)

 

(tus를 이용한 Cloudflare 비디어 업로드는 이하의 공식문서를 제일 많이 참조했다.) 

https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads#advanced-upload-flow-using-tus-for-large-videos

 

Direct creator uploads · Cloudflare Stream docs

Direct creator uploads let your end users to upload videos directly to Cloudflare Stream, without exposing your API token to clients.

developers.cloudflare.com

 

우선 클라이언트의 tus-js-client를 이용하기 때문에 서버는 Cloudflare와 클라이언트를 연결해주는 중간다리 역할을 한다는 걸 인지하자. 업로드를 하는 주체는 클라이언트(Vue)가 되는 느낌이다.

 

서버쪽

nest-cli를 통해 프로젝트를 생성하고 바로 작성 들어간다.

uploads.controller.ts

import { Controller, HttpStatus, Post, Req, Res } from '@nestjs/common';
import { UploadsService } from './uploads.service';

@Controller('uploads')
export class UploadsController {
  constructor(private uploadsService: UploadsService) {}
  @Post()
  async upload(@Req() req, @Res() res) {
    const uploadUrl = await this.uploadsService.upload(req);
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', '*');
    res.setHeader('Access-Control-Expose-Headers', 'Location');
    res.setHeader('Location', uploadUrl);

    return res.status(HttpStatus.OK).json();
  }
}
  • uploadService에서 Cloudflare의 API를 두들기고 받은 uploadUrl을 클라이언트에게 Response의 Header에 담아서 제공한다. (이부분이 핵심)
  • setHeader들의 부분들은 공식문서(위의 링크)를 보고 참조한 부분들이다. 

 

uploads.service.ts

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';

@Injectable()
export class UploadsService {
  constructor(private configService: ConfigService) {}
  async upload(req: Request) {
    const accountId = this.configService.get<string>('CLOUDFLARE_ACCOUNT_ID');
    const apiToken = this.configService.get<string>('CLOUDFLARE_API_TOKEN');

    const headers = {
      Authorization: `Bearer ${apiToken}`,
      'Tus-Resumable': '1.0.0',
      'Upload-Length': req.headers['upload-length'],
    };

    try {
      const response = await axios.post(
        `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream?direct_user=true`,
        {},
        {
          headers,
        },
      );

      const uploadUrl = response.headers['location'];
      return uploadUrl;
    } catch (err) {
      throw new HttpException(
        '비디오 업로드 중 오류가 발생했습니다.',
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }
  }
}
  • Cloudflare의 tus 프로토콜로 업로드를 제공하는 API를 request해서 UploadUrl을 취득하고 반환한다.
  • 해당 코드에서는 Upload-Metadata를 셋팅 안하고 있는데 추후에 이를 이용해 업로드한 비디오의 식별자 역할을 부여할 예정
  • 그리고 에러처리가 거의 안되어있다. 용량초과등등의 예외처리는 추가해 두는게 좋을듯하다.

클라이언트 쪽

App.vue

<script lang="ts" setup>
import { Upload } from 'tus-js-client';
const onChange = async (event: Event) => {
  const target = event.target as HTMLInputElement;
  const files = target.files as FileList;
  const file = files[0];
  const upload = new Upload(file, {
    endpoint: 'http://localhost:3000/uploads',
    chunkSize: 8 * 1024 ** 2, // 8MB
    // retryDelays: [0, 3000, 5000, 10000, 20000],
    metadata: {
      name: file.name,
    },
    headers: {
      Accept: 'application/json',
    },
    onProgress(bytesUploaded, bytesTotal) {
      console.log(bytesUploaded, bytesTotal);
    },
    onSuccess() {
      console.log('Upload finished:');
    },
  });
  upload.findPreviousUploads().then(previousUploads => {
    // Found previous uploads so we select the first one.
    if (previousUploads.length) {
      // upload.resumeFromPreviousUpload(previousUploads[0]);
    }
    // Start the upload
    upload.start();
  });
};
</script>

<template>
  <input type="file" @change="onChange" />
</template>
  • tus-js-client 패키지를 설치해 줘야 한다. (npm install tus-js-client)
  • 그 외 부분들은 청크사이즈 셋팅, 진척상황, 성공의 상태등을 취득할 수 있다는 코드들이며 거의 복붙했다.

업로드 성공!

 

라이브러리를 활용하면 쉽게 구현이 가능했다...!

 

Comments