diff options
Diffstat (limited to 'server/controllers/api/videos/upload.ts')
-rw-r--r-- | server/controllers/api/videos/upload.ts | 287 |
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 @@ | |||
1 | import express from 'express' | ||
2 | import { move } from 'fs-extra' | ||
3 | import { basename } from 'path' | ||
4 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | ||
6 | import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue' | ||
7 | import { Redis } from '@server/lib/redis' | ||
8 | import { uploadx } from '@server/lib/uploadx' | ||
9 | import { buildLocalVideoFromReq, buildMoveToObjectStorageJob, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
10 | import { buildNewFile } from '@server/lib/video-file' | ||
11 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
12 | import { buildNextVideoState } from '@server/lib/video-state' | ||
13 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
14 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
15 | import { VideoSourceModel } from '@server/models/video/video-source' | ||
16 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | ||
17 | import { uuidToShort } from '@shared/extra-utils' | ||
18 | import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' | ||
19 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
20 | import { createReqFiles } from '../../../helpers/express-utils' | ||
21 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
22 | import { MIMETYPES } from '../../../initializers/constants' | ||
23 | import { sequelizeTypescript } from '../../../initializers/database' | ||
24 | import { Hooks } from '../../../lib/plugins/hooks' | ||
25 | import { generateLocalVideoMiniature } from '../../../lib/thumbnail' | ||
26 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
27 | import { | ||
28 | asyncMiddleware, | ||
29 | asyncRetryTransactionMiddleware, | ||
30 | authenticate, | ||
31 | videosAddLegacyValidator, | ||
32 | videosAddResumableInitValidator, | ||
33 | videosAddResumableValidator | ||
34 | } from '../../../middlewares' | ||
35 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
36 | import { VideoModel } from '../../../models/video/video' | ||
37 | |||
38 | const lTags = loggerTagsFactory('api', 'video') | ||
39 | const auditLogger = auditLoggerFactory('videos') | ||
40 | const uploadRouter = express.Router() | ||
41 | |||
42 | const reqVideoFileAdd = createReqFiles( | ||
43 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | ||
44 | { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT } | ||
45 | ) | ||
46 | |||
47 | const reqVideoFileAddResumable = createReqFiles( | ||
48 | [ 'thumbnailfile', 'previewfile' ], | ||
49 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
50 | getResumableUploadPath() | ||
51 | ) | ||
52 | |||
53 | uploadRouter.post('/upload', | ||
54 | openapiOperationDoc({ operationId: 'uploadLegacy' }), | ||
55 | authenticate, | ||
56 | reqVideoFileAdd, | ||
57 | asyncMiddleware(videosAddLegacyValidator), | ||
58 | asyncRetryTransactionMiddleware(addVideoLegacy) | ||
59 | ) | ||
60 | |||
61 | uploadRouter.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 | |||
69 | uploadRouter.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 | |||
75 | uploadRouter.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 | |||
85 | export { | ||
86 | uploadRouter | ||
87 | } | ||
88 | |||
89 | // --------------------------------------------------------------------------- | ||
90 | |||
91 | async 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 | |||
111 | async 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 | |||
122 | async 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 | |||
227 | async 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 | |||
283 | async 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 | } | ||