aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers/api/videos/upload.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/controllers/api/videos/upload.ts')
-rw-r--r--server/controllers/api/videos/upload.ts287
1 files changed, 0 insertions, 287 deletions
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
deleted file mode 100644
index e520bf4b5..000000000
--- a/server/controllers/api/videos/upload.ts
+++ /dev/null
@@ -1,287 +0,0 @@
1import express from 'express'
2import { move } from 'fs-extra'
3import { basename } from 'path'
4import { getResumableUploadPath } from '@server/helpers/upload'
5import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
6import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue'
7import { Redis } from '@server/lib/redis'
8import { uploadx } from '@server/lib/uploadx'
9import { buildLocalVideoFromReq, buildMoveToObjectStorageJob, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
10import { buildNewFile } from '@server/lib/video-file'
11import { VideoPathManager } from '@server/lib/video-path-manager'
12import { buildNextVideoState } from '@server/lib/video-state'
13import { openapiOperationDoc } from '@server/middlewares/doc'
14import { VideoPasswordModel } from '@server/models/video/video-password'
15import { VideoSourceModel } from '@server/models/video/video-source'
16import { MVideoFile, MVideoFullLight } from '@server/types/models'
17import { uuidToShort } from '@shared/extra-utils'
18import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models'
19import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
20import { createReqFiles } from '../../../helpers/express-utils'
21import { logger, loggerTagsFactory } from '../../../helpers/logger'
22import { MIMETYPES } from '../../../initializers/constants'
23import { sequelizeTypescript } from '../../../initializers/database'
24import { Hooks } from '../../../lib/plugins/hooks'
25import { generateLocalVideoMiniature } from '../../../lib/thumbnail'
26import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
27import {
28 asyncMiddleware,
29 asyncRetryTransactionMiddleware,
30 authenticate,
31 videosAddLegacyValidator,
32 videosAddResumableInitValidator,
33 videosAddResumableValidator
34} from '../../../middlewares'
35import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
36import { VideoModel } from '../../../models/video/video'
37
38const lTags = loggerTagsFactory('api', 'video')
39const auditLogger = auditLoggerFactory('videos')
40const uploadRouter = express.Router()
41
42const reqVideoFileAdd = createReqFiles(
43 [ 'videofile', 'thumbnailfile', 'previewfile' ],
44 { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT }
45)
46
47const reqVideoFileAddResumable = createReqFiles(
48 [ 'thumbnailfile', 'previewfile' ],
49 MIMETYPES.IMAGE.MIMETYPE_EXT,
50 getResumableUploadPath()
51)
52
53uploadRouter.post('/upload',
54 openapiOperationDoc({ operationId: 'uploadLegacy' }),
55 authenticate,
56 reqVideoFileAdd,
57 asyncMiddleware(videosAddLegacyValidator),
58 asyncRetryTransactionMiddleware(addVideoLegacy)
59)
60
61uploadRouter.post('/upload-resumable',
62 openapiOperationDoc({ operationId: 'uploadResumableInit' }),
63 authenticate,
64 reqVideoFileAddResumable,
65 asyncMiddleware(videosAddResumableInitValidator),
66 (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end
67)
68
69uploadRouter.delete('/upload-resumable',
70 authenticate,
71 asyncMiddleware(deleteUploadResumableCache),
72 (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end
73)
74
75uploadRouter.put('/upload-resumable',
76 openapiOperationDoc({ operationId: 'uploadResumable' }),
77 authenticate,
78 uploadx.upload, // uploadx doesn't next() before the file upload completes
79 asyncMiddleware(videosAddResumableValidator),
80 asyncMiddleware(addVideoResumable)
81)
82
83// ---------------------------------------------------------------------------
84
85export {
86 uploadRouter
87}
88
89// ---------------------------------------------------------------------------
90
91async function addVideoLegacy (req: express.Request, res: express.Response) {
92 // Uploading the video could be long
93 // Set timeout to 10 minutes, as Express's default is 2 minutes
94 req.setTimeout(1000 * 60 * 10, () => {
95 logger.error('Video upload has timed out.')
96 return res.fail({
97 status: HttpStatusCode.REQUEST_TIMEOUT_408,
98 message: 'Video upload has timed out.'
99 })
100 })
101
102 const videoPhysicalFile = req.files['videofile'][0]
103 const videoInfo: VideoCreate = req.body
104 const files = req.files
105
106 const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files })
107
108 return res.json(response)
109}
110
111async function addVideoResumable (req: express.Request, res: express.Response) {
112 const videoPhysicalFile = res.locals.uploadVideoFileResumable
113 const videoInfo = videoPhysicalFile.metadata
114 const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile }
115
116 const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files })
117 await Redis.Instance.setUploadSession(req.query.upload_id, response)
118
119 return res.json(response)
120}
121
122async function addVideo (options: {
123 req: express.Request
124 res: express.Response
125 videoPhysicalFile: express.VideoUploadFile
126 videoInfo: VideoCreate
127 files: express.UploadFiles
128}) {
129 const { req, res, videoPhysicalFile, videoInfo, files } = options
130 const videoChannel = res.locals.videoChannel
131 const user = res.locals.oauth.token.User
132
133 let videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
134 videoData = await Hooks.wrapObject(videoData, 'filter:api.video.upload.video-attribute.result')
135
136 videoData.state = buildNextVideoState()
137 videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
138
139 const video = new VideoModel(videoData) as MVideoFullLight
140 video.VideoChannel = videoChannel
141 video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
142
143 const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' })
144 const originalFilename = videoPhysicalFile.originalname
145
146 // Move physical file
147 const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
148 await move(videoPhysicalFile.path, destination)
149 // This is important in case if there is another attempt in the retry process
150 videoPhysicalFile.filename = basename(destination)
151 videoPhysicalFile.path = destination
152
153 const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
154 video,
155 files,
156 fallback: type => generateLocalVideoMiniature({ video, videoFile, type })
157 })
158
159 const { videoCreated } = await sequelizeTypescript.transaction(async t => {
160 const sequelizeOptions = { transaction: t }
161
162 const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
163
164 await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
165 await videoCreated.addAndSaveThumbnail(previewModel, t)
166
167 // Do not forget to add video channel information to the created video
168 videoCreated.VideoChannel = res.locals.videoChannel
169
170 videoFile.videoId = video.id
171 await videoFile.save(sequelizeOptions)
172
173 video.VideoFiles = [ videoFile ]
174
175 await VideoSourceModel.create({
176 filename: originalFilename,
177 videoId: video.id
178 }, { transaction: t })
179
180 await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
181
182 // Schedule an update in the future?
183 if (videoInfo.scheduleUpdate) {
184 await ScheduleVideoUpdateModel.create({
185 videoId: video.id,
186 updateAt: new Date(videoInfo.scheduleUpdate.updateAt),
187 privacy: videoInfo.scheduleUpdate.privacy || null
188 }, sequelizeOptions)
189 }
190
191 await autoBlacklistVideoIfNeeded({
192 video,
193 user,
194 isRemote: false,
195 isNew: true,
196 isNewFile: true,
197 transaction: t
198 })
199
200 if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
201 await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t)
202 }
203
204 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
205 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
206
207 return { videoCreated }
208 })
209
210 // Channel has a new content, set as updated
211 await videoCreated.VideoChannel.setAsUpdated()
212
213 addVideoJobsAfterUpload(videoCreated, videoFile)
214 .catch(err => logger.error('Cannot build new video jobs of %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) }))
215
216 Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res })
217
218 return {
219 video: {
220 id: videoCreated.id,
221 shortUUID: uuidToShort(videoCreated.uuid),
222 uuid: videoCreated.uuid
223 }
224 }
225}
226
227async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) {
228 const jobs: (CreateJobArgument & CreateJobOptions)[] = [
229 {
230 type: 'manage-video-torrent' as 'manage-video-torrent',
231 payload: {
232 videoId: video.id,
233 videoFileId: videoFile.id,
234 action: 'create'
235 }
236 },
237
238 {
239 type: 'generate-video-storyboard' as 'generate-video-storyboard',
240 payload: {
241 videoUUID: video.uuid,
242 // No need to federate, we process these jobs sequentially
243 federate: false
244 }
245 },
246
247 {
248 type: 'notify',
249 payload: {
250 action: 'new-video',
251 videoUUID: video.uuid
252 }
253 },
254
255 {
256 type: 'federate-video' as 'federate-video',
257 payload: {
258 videoUUID: video.uuid,
259 isNewVideo: true
260 }
261 }
262 ]
263
264 if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
265 jobs.push(await buildMoveToObjectStorageJob({ video, previousVideoState: undefined }))
266 }
267
268 if (video.state === VideoState.TO_TRANSCODE) {
269 jobs.push({
270 type: 'transcoding-job-builder' as 'transcoding-job-builder',
271 payload: {
272 videoUUID: video.uuid,
273 optimizeJob: {
274 isNewVideo: true
275 }
276 }
277 })
278 }
279
280 return JobQueue.Instance.createSequentialJobFlow(...jobs)
281}
282
283async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) {
284 await Redis.Instance.deleteUploadSession(req.query.upload_id)
285
286 return next()
287}