똑같은 삽질은 2번 하지 말자
tus + Cloudflare stream을 이용한 영상 업로드 (Vue + NestJS) 본문
개요
영상 업로드 문제를 해결하기 위해 tus라는 프로토콜을 이용해 해결했었던 것에 대해 글로 남기고자 한다.
tus란?
TUS는 HTTP 기반의 파일 업로드 프로토콜로, 큰 파일이나 불안정한 네트워크 연결 상황에서도 안정적인 파일 업로드를 가능하게 한다. 파일을 여러 부분으로 나누어 업로드하며, 실패한 업로드는 재시작할 수 있으며, 다양한 프로그래밍 언어와 클라우드 스토리지 서비스와 호환되는 라이브러리를 제공
그렇다! 이미 제공되는 라이브러리가 많으며, 이를 이용해 빠르게 개발해 보자. (참고로 클라이언트(Vue) + 서버(Nest) 이다)
(tus를 이용한 Cloudflare 비디어 업로드는 이하의 공식문서를 제일 많이 참조했다.)
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)
- 그 외 부분들은 청크사이즈 셋팅, 진척상황, 성공의 상태등을 취득할 수 있다는 코드들이며 거의 복붙했다.
업로드 성공!
라이브러리를 활용하면 쉽게 구현이 가능했다...!