-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
presigned url 적용 #147
presigned url 적용 #147
Changes from 12 commits
51797dd
9a5204c
6e5ca6d
40f452f
3931c77
bd1aa22
7beca20
a574d7a
fe98104
78e6f58
1d1c5f1
8d5cba1
c09bf4b
722fa6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { HttpRequest } from '@smithy/protocol-http'; | ||
import { S3RequestPresigner } from '@aws-sdk/s3-request-presigner'; | ||
import { parseUrl } from '@smithy/url-parser'; | ||
import { formatUrl } from '@aws-sdk/util-format-url'; | ||
import { Hash } from '@smithy/hash-node'; | ||
|
||
export const createPresignedUrl = async ( | ||
bucketName: string, | ||
objectName: string, | ||
method: string, | ||
) => { | ||
const region = 'kr-standard'; | ||
const endPoint = 'https://kr.object.ncloudstorage.com'; | ||
const canonicalURI = `/${bucketName}/${objectName}`; | ||
const apiUrl = `${endPoint}${canonicalURI}`; | ||
const url = parseUrl(apiUrl); | ||
|
||
const presigner = new S3RequestPresigner({ | ||
credentials: { | ||
accessKeyId: process.env.ACCESS_KEY, | ||
secretAccessKey: process.env.SECRET_KEY, | ||
}, | ||
region, | ||
sha256: Hash.bind(null, 'sha256'), | ||
}); | ||
|
||
const signedUrlObject = await presigner.presign( | ||
new HttpRequest({ ...url, method }), | ||
{ expiresIn: 100 }, | ||
); | ||
return { name: objectName, url: formatUrl(signedUrlObject) }; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. url만 리턴하는게 자연스러울거 같은데 name을 같이 리턴해야만 하는 이유가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 광고 이미지를 재발급 받으려면 name이 필요한데 서비스에서 추가하기 귀찮아서 이렇게 했습니다..! |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export class AdvertisementPresignedUrlRequestDto { | ||
/** | ||
* 특정 광고 이미지의 presigned url만 받고 싶은 경우 | ||
* @example 'test.webp' | ||
Comment on lines
+3
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
*/ | ||
name?: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { IsUUID } from 'class-validator'; | ||
|
||
export class ProfilePresignedUrlRequestDto { | ||
/** | ||
* user uuid | ||
*/ | ||
@IsUUID() | ||
uuid: string; | ||
|
||
/** | ||
* 프로필 이미지 확장자 | ||
* @example 'webp' | ||
*/ | ||
profileExtension: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { IsEnum } from 'class-validator'; | ||
|
||
enum TypeEnum { | ||
thumbnail = 'thumbnail', | ||
profile = 'profile', | ||
} | ||
|
||
export class ReissuePresignedUrlRequestDto { | ||
/** | ||
* 요청하는 이미지 종류 | ||
* @example 'thumbnail' | ||
*/ | ||
@IsEnum(TypeEnum) | ||
type: TypeEnum; | ||
|
||
/** | ||
* 이미지 확장자 | ||
* @example 'webp' | ||
*/ | ||
extension: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export class VIdeoPresignedUrlRequestDto { | ||
/** | ||
* 비디오 확장자 | ||
* @example 'mp4' | ||
*/ | ||
videoExtension: string; | ||
|
||
/** | ||
* 썸네일 이미지 확장자 | ||
* @example 'webp' | ||
*/ | ||
thumbnailExtension: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common'; | ||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; | ||
import { ApiFailResponse } from 'src/decorators/api-fail-response'; | ||
import { AuthGuard } from 'src/auth/auth.guard'; | ||
import { InvalidTokenException } from 'src/exceptions/invalid-token.exception'; | ||
import { TokenExpiredException } from 'src/exceptions/token-expired.exception'; | ||
import { PresignedUrlService } from './presigned-url.service'; | ||
import { AdvertisementPresignedUrlRequestDto } from './dto/advertisement-presigned-url-request.dto'; | ||
import { ProfilePresignedUrlRequestDto } from './dto/profile-presigned-url-request.dto'; | ||
import { VIdeoPresignedUrlRequestDto } from './dto/video-presigned-url-request.dto'; | ||
import { ReissuePresignedUrlRequestDto } from './dto/reissue-presigned-url-request.dto'; | ||
|
||
@ApiTags('PRESIGNED URL') | ||
@ApiBearerAuth() | ||
@UseGuards(AuthGuard) | ||
@ApiFailResponse('인증 실패', [InvalidTokenException, TokenExpiredException]) | ||
@Controller('presigned-url') | ||
export class PresignedUrlController { | ||
constructor(private presignedUrlService: PresignedUrlService) {} | ||
|
||
/** | ||
* 광고 이미지를 GET하는 url 발급 | ||
*/ | ||
@Get('advertisements') | ||
getAdvertisementPresignedUrl( | ||
@Query() query: AdvertisementPresignedUrlRequestDto, | ||
) { | ||
return this.presignedUrlService.getAdvertisementPresignedUrl(query.name); | ||
} | ||
|
||
/** | ||
* 프로필 이미지를 PUT하는 url 발급 | ||
*/ | ||
@Get('profile') | ||
putProfilePresignedUrl(@Query() query: ProfilePresignedUrlRequestDto) { | ||
return this.presignedUrlService.putProfilePresignedUrl(query); | ||
} | ||
|
||
/** | ||
* 비디오, 썸네일 이미지를 PUT하는 url 발급 | ||
*/ | ||
@Get('video') | ||
putVideoPresignedUrl(@Query() query: VIdeoPresignedUrlRequestDto) { | ||
return this.presignedUrlService.putVideoPresignedUrl(query); | ||
} | ||
|
||
/** | ||
* 만료된 presigned url 재발급 | ||
*/ | ||
@Get('reissue/:id') | ||
getImagePresignedUrl( | ||
@Param('id') id: string, | ||
@Query() query: ReissuePresignedUrlRequestDto, | ||
) { | ||
return this.presignedUrlService.getImagePresignedUrl(id, query); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { Module } from '@nestjs/common'; | ||
|
||
@Module({}) | ||
export class PresignedUrlModule {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { createPresignedUrl } from 'src/ncpAPI/presignedURL'; | ||
import { listObjects } from 'src/ncpAPI/listObjects'; | ||
import * as _ from 'lodash'; | ||
import { xml2js } from 'xml-js'; | ||
import { Types } from 'mongoose'; | ||
|
||
@Injectable() | ||
export class PresignedUrlService { | ||
async getAdvertisementPresignedUrl(adName: string) { | ||
if (adName) | ||
return createPresignedUrl( | ||
process.env.ADVERTISEMENT_BUCKET, | ||
adName, | ||
'GET', | ||
); | ||
|
||
const xmlData = await listObjects(process.env.ADVERTISEMENT_BUCKET); | ||
const jsonData: any = xml2js(xmlData, { compact: true }); | ||
|
||
const adList = _.map(jsonData.ListBucketResult.Contents, 'Key._text'); | ||
const advertisements = await Promise.all( | ||
adList.map(async (advertisement: string) => { | ||
return createPresignedUrl( | ||
process.env.ADVERTISEMENT_BUCKET, | ||
advertisement, | ||
'GET', | ||
); | ||
}), | ||
); | ||
return { advertisements }; | ||
} | ||
|
||
async putProfilePresignedUrl({ uuid, profileExtension }) { | ||
const objectName = `${uuid}.${profileExtension}`; | ||
const presignedUrl = ( | ||
await createPresignedUrl(process.env.PROFILE_BUCKET, objectName, 'PUT') | ||
).url; | ||
return { presignedUrl }; | ||
} | ||
|
||
async putVideoPresignedUrl({ videoExtension, thumbnailExtension }) { | ||
const videoId = new Types.ObjectId(); | ||
const [videoUrl, thumbnailUrl] = await Promise.all([ | ||
( | ||
await createPresignedUrl( | ||
process.env.INPUT_BUCKET, | ||
`${videoId}.${videoExtension}`, | ||
'PUT', | ||
) | ||
).url, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아래 코드는 어떠신가요? createPresignedUrl(
process.env.INPUT_BUCKET,
`${videoId}.${videoExtension}`,
'PUT',
).then( data=> data.url) |
||
( | ||
await createPresignedUrl( | ||
process.env.THUMBNAIL_BUCKET, | ||
`${videoId}.${thumbnailExtension}`, | ||
'PUT', | ||
) | ||
).url, | ||
]); | ||
return { videoId, videoUrl, thumbnailUrl }; | ||
} | ||
|
||
async getImagePresignedUrl(id: string, { type, extension }) { | ||
// TODO 업로드 된 이미지인지 확인 | ||
const bucketName = { | ||
thumbnail: process.env.THUMBNAIL_BUCKET, | ||
profile: process.env.PROFILE_BUCKET, | ||
}[type]; | ||
const objectName = `${id}.${extension}`; | ||
const presignedUrl = ( | ||
await createPresignedUrl(bucketName, objectName, 'GET') | ||
).url; | ||
return { presignedUrl }; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export class ProfileResponseDto { | ||
/** | ||
* 닉네임 | ||
* @example 'honux' | ||
*/ | ||
nickname: string; | ||
|
||
/** | ||
* 상태 메세지 | ||
* @example 'web be 마스터' | ||
*/ | ||
statusMessage: string; | ||
|
||
/** | ||
* 프로필 이미지 가져오는 presigned url | ||
* @example 'https://ncloud.com/' | ||
*/ | ||
profileImageUrl: string; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LEGACY 태그로 기존 코드 구분하신게 좋은거 같아요!