返回 AiToEarn
assets-http.controller-base.ts
根目录 / project / aitoearn-backend / libs / assets / src / http / assets-http.controller-base.ts
1 import type { Response } from 'express'
2 import { BadRequestException, Body, Get, Inject, Param, Post, Query, Res } from '@nestjs/common'
3 import { GetToken, Public, TokenInfo } from '@yikart/aitoearn-auth'
4 import { ApiDoc, UserType } from '@yikart/common'
5 import { AssetStatus } from '@yikart/mongodb'
6 import * as mime from 'mime-types'
7 import { AssetsService } from '../assets.service'
8 import { VideoMetadataService } from '../video-metadata.service'
9 import { AssetVo } from '../vo/asset.vo'
10 import { ThumbnailResultVo } from '../vo/thumbnail-result.vo'
11 import { UploadResultVo } from '../vo/upload-result.vo'
12 import { CreateUploadSignDto, GetThumbnailQueryDto, OssCallbackDto } from './assets-http.dto'
13 import { ASSETS_HTTP_OPTIONS, AssetsHttpModuleOptions } from './assets-http.options'
14
15 const PUBLIC_UPLOAD_ID_PATTERN = /^[\w-]{8,64}$/
16
17 function toPublicUploadUserId(publicUploadId: string) {
18 if (!PUBLIC_UPLOAD_ID_PATTERN.test(publicUploadId)) {
19 throw new BadRequestException('Invalid public upload id')
20 }
21
22 return `public-${publicUploadId}`
23 }
24
25 export abstract class AssetsHttpControllerBase {
26 protected readonly userType: UserType
27
28 constructor(
29 protected readonly assetsService: AssetsService,
30 protected readonly videoMetadataService: VideoMetadataService,
31 @Inject(ASSETS_HTTP_OPTIONS) options: AssetsHttpModuleOptions,
32 ) {
33 this.userType = options.userType ?? UserType.User
34 }
35
36 @ApiDoc({
37 summary: 'Create Upload Signed URL',
38 description: 'Create a signed URL for direct upload. Path is auto-generated based on user and type.',
39 body: CreateUploadSignDto.schema,
40 response: UploadResultVo,
41 })
42 @Post('/uploadSign')
43 async createUploadSign(
44 @GetToken() token: TokenInfo,
45 @Body() body: CreateUploadSignDto,
46 ) {
47 const mimeType = mime.lookup(body.filename) || 'application/octet-stream'
48 const result = await this.assetsService.createUploadSign(token.id, {
49 type: body.type,
50 mimeType,
51 filename: body.filename,
52 size: body.size,
53 }, this.userType)
54 return UploadResultVo.create({
55 id: result.asset.id,
56 path: result.asset.path,
57 url: result.url,
58 uploadUrl: result.uploadUrl,
59 })
60 }
61
62 @Public()
63 @ApiDoc({
64 summary: 'Create Public Upload Signed URL',
65 description: 'Create a signed URL for public direct upload. Asset owner is fixed to public.',
66 body: CreateUploadSignDto.schema,
67 response: UploadResultVo,
68 })
69 @Post('/public/:publicUploadId/uploadSign')
70 async createPublicUploadSign(
71 @Param('publicUploadId') publicUploadId: string,
72 @Body() body: CreateUploadSignDto,
73 ) {
74 const userId = toPublicUploadUserId(publicUploadId)
75 const mimeType = mime.lookup(body.filename) || 'application/octet-stream'
76 const result = await this.assetsService.createUploadSign(userId, {
77 type: body.type,
78 mimeType,
79 filename: body.filename,
80 size: body.size,
81 }, this.userType)
82 return UploadResultVo.create({
83 id: result.asset.id,
84 path: result.asset.path,
85 url: result.url,
86 uploadUrl: result.uploadUrl,
87 })
88 }
89
90 @ApiDoc({
91 summary: 'Confirm Asset Upload',
92 description: 'Client confirms the asset upload after completing the upload to R2.',
93 response: AssetVo,
94 })
95 @Post('/:id/confirm')
96 async confirmUpload(
97 @GetToken() token: TokenInfo,
98 @Param('id') assetId: string,
99 ) {
100 const asset = await this.assetsService.confirmUploadByUser(token.id, assetId, this.userType)
101 return AssetVo.create(Object.assign(asset, { url: asset.path }))
102 }
103
104 @Public()
105 @ApiDoc({
106 summary: 'Confirm Public Asset Upload',
107 description: 'Client confirms a public asset upload after completing the upload to R2.',
108 response: AssetVo,
109 })
110 @Post('/public/:publicUploadId/:id/confirm')
111 async confirmPublicUpload(
112 @Param('publicUploadId') publicUploadId: string,
113 @Param('id') assetId: string,
114 ) {
115 const userId = toPublicUploadUserId(publicUploadId)
116 const asset = await this.assetsService.confirmUploadByUser(userId, assetId, this.userType)
117 return AssetVo.create(Object.assign(asset, { url: asset.path }))
118 }
119
120 @ApiDoc({
121 summary: 'Get Video Thumbnail',
122 description: 'Get or extract thumbnail from a video by URL. If thumbnail already exists in metadata.cover, returns it directly. Otherwise extracts a new thumbnail.',
123 query: GetThumbnailQueryDto.schema,
124 response: ThumbnailResultVo,
125 })
126 @Get('/thumbnail')
127 async getThumbnail(
128 @GetToken() token: TokenInfo,
129 @Query() query: GetThumbnailQueryDto,
130 @Res({ passthrough: true }) res: Response,
131 ) {
132 const path = this.assetsService.parsePathFromUrl(query.url)
133 const asset = await this.assetsService.getOrCreateAssetByPath(path, token.id, this.userType)
134
135 if (!asset.mimeType?.startsWith('video/')) {
136 throw new BadRequestException('Asset is not a video')
137 }
138
139 const existingCover = (asset.metadata as { cover?: string } | undefined)?.cover
140 if (existingCover) {
141 const thumbnailUrl = this.assetsService.buildUrl(existingCover)
142 if (query.redirect) {
143 res.redirect(302, thumbnailUrl)
144 return
145 }
146 return ThumbnailResultVo.create({ thumbnailUrl })
147 }
148
149 const result = await this.videoMetadataService.extractAndSaveThumbnail(
150 asset,
151 token.id,
152 this.userType,
153 { timeInSeconds: query.timeInSeconds },
154 )
155
156 if (query.redirect) {
157 res.redirect(302, result.thumbnailUrl)
158 return
159 }
160
161 return ThumbnailResultVo.create({ thumbnailUrl: result.thumbnailUrl })
162 }
163
164 @Public()
165 @ApiDoc({
166 summary: 'OSS Upload Callback',
167 description: 'AliOss 上传完成后的服务端回调',
168 })
169 @Post('/oss/callback')
170 async ossCallback(@Body() body: OssCallbackDto) {
171 const asset = await this.assetsService.getById(body.assetId)
172 if (asset && asset.status === AssetStatus.Pending) {
173 await this.assetsService.confirmUpload({
174 assetId: asset.id,
175 size: body.size,
176 })
177 }
178 }
179 }
180
180 lines TYPESCRIPT