]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
b3ffb70079b23c33e82763ac6258e15ae07643d4
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
1 import express from 'express'
2 import { body, header, param, query, ValidationChain } from 'express-validator'
3 import { isTestInstance } from '@server/helpers/core-utils'
4 import { getResumableUploadPath } from '@server/helpers/upload'
5 import { Redis } from '@server/lib/redis'
6 import { isAbleToUploadVideo } from '@server/lib/user'
7 import { getServerActor } from '@server/models/application/application'
8 import { ExpressPromiseHandler } from '@server/types/express-handler'
9 import { MUserAccountId, MVideoFullLight } from '@server/types/models'
10 import { getAllPrivacies } from '@shared/core-utils'
11 import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoPrivacy } from '@shared/models'
12 import {
13 exists,
14 isBooleanValid,
15 isDateValid,
16 isFileFieldValid,
17 isIdValid,
18 isUUIDValid,
19 toArray,
20 toBooleanOrNull,
21 toIntOrNull,
22 toValueOrNull
23 } from '../../../helpers/custom-validators/misc'
24 import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
25 import {
26 isScheduleVideoUpdatePrivacyValid,
27 isVideoCategoryValid,
28 isVideoDescriptionValid,
29 isVideoFileMimeTypeValid,
30 isVideoFileSizeValid,
31 isVideoFilterValid,
32 isVideoImage,
33 isVideoIncludeValid,
34 isVideoLanguageValid,
35 isVideoLicenceValid,
36 isVideoNameValid,
37 isVideoOriginallyPublishedAtValid,
38 isVideoPrivacyValid,
39 isVideoSupportValid,
40 isVideoTagsValid
41 } from '../../../helpers/custom-validators/videos'
42 import { cleanUpReqFiles } from '../../../helpers/express-utils'
43 import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
44 import { logger } from '../../../helpers/logger'
45 import { deleteFileAndCatch } from '../../../helpers/utils'
46 import { getVideoWithAttributes } from '../../../helpers/video'
47 import { CONFIG } from '../../../initializers/config'
48 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
49 import { isLocalVideoAccepted } from '../../../lib/moderation'
50 import { Hooks } from '../../../lib/plugins/hooks'
51 import { VideoModel } from '../../../models/video/video'
52 import {
53 areValidationErrors,
54 checkCanSeePrivateVideo,
55 checkUserCanManageVideo,
56 doesVideoChannelOfAccountExist,
57 doesVideoExist,
58 doesVideoFileOfVideoExist,
59 isValidVideoIdParam
60 } from '../shared'
61
62 const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
63 body('videofile')
64 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile'))
65 .withMessage('Should have a file'),
66 body('name')
67 .trim()
68 .custom(isVideoNameValid).withMessage(
69 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
70 ),
71 body('channelId')
72 .customSanitizer(toIntOrNull)
73 .custom(isIdValid).withMessage('Should have correct video channel id'),
74
75 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
77
78 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
79
80 const videoFile: express.VideoUploadFile = req.files['videofile'][0]
81 const user = res.locals.oauth.token.User
82
83 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) {
84 return cleanUpReqFiles(req)
85 }
86
87 try {
88 if (!videoFile.duration) await addDurationToVideo(videoFile)
89 } catch (err) {
90 logger.error('Invalid input file in videosAddLegacyValidator.', { err })
91
92 res.fail({
93 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
94 message: 'Video file unreadable.'
95 })
96 return cleanUpReqFiles(req)
97 }
98
99 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
100
101 return next()
102 }
103 ])
104
105 /**
106 * Gets called after the last PUT request
107 */
108 const videosAddResumableValidator = [
109 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
110 const user = res.locals.oauth.token.User
111 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
112 const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename }
113 const cleanup = () => deleteFileAndCatch(file.path)
114
115 const uploadId = req.query.upload_id
116 const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId)
117
118 if (sessionExists) {
119 const sessionResponse = await Redis.Instance.getUploadSession(uploadId)
120
121 if (!sessionResponse) {
122 res.setHeader('Retry-After', 300) // ask to retry after 5 min, knowing the upload_id is kept for up to 15 min after completion
123
124 return res.fail({
125 status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
126 message: 'The upload is already being processed'
127 })
128 }
129
130 if (isTestInstance()) {
131 res.setHeader('x-resumable-upload-cached', 'true')
132 }
133
134 return res.json(sessionResponse)
135 }
136
137 await Redis.Instance.setUploadSession(uploadId)
138
139 if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup()
140
141 try {
142 if (!file.duration) await addDurationToVideo(file)
143 } catch (err) {
144 logger.error('Invalid input file in videosAddResumableValidator.', { err })
145
146 res.fail({
147 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
148 message: 'Video file unreadable.'
149 })
150 return cleanup()
151 }
152
153 if (!await isVideoAccepted(req, res, file)) return cleanup()
154
155 res.locals.videoFileResumable = file
156
157 return next()
158 }
159 ]
160
161 /**
162 * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use.
163 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts
164 *
165 * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
166 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
167 *
168 */
169 const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
170 body('filename')
171 .isString()
172 .exists()
173 .withMessage('Should have a valid filename'),
174 body('name')
175 .trim()
176 .custom(isVideoNameValid).withMessage(
177 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
178 ),
179 body('channelId')
180 .customSanitizer(toIntOrNull)
181 .custom(isIdValid).withMessage('Should have correct video channel id'),
182
183 header('x-upload-content-length')
184 .isNumeric()
185 .exists()
186 .withMessage('Should specify the file length'),
187 header('x-upload-content-type')
188 .isString()
189 .exists()
190 .withMessage('Should specify the file mimetype'),
191
192 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
193 const videoFileMetadata = {
194 mimetype: req.headers['x-upload-content-type'] as string,
195 size: +req.headers['x-upload-content-length'],
196 originalname: req.body.filename
197 }
198
199 const user = res.locals.oauth.token.User
200 const cleanup = () => cleanUpReqFiles(req)
201
202 logger.debug('Checking videosAddResumableInitValidator parameters and headers', {
203 parameters: req.body,
204 headers: req.headers,
205 files: req.files
206 })
207
208 if (areValidationErrors(req, res)) return cleanup()
209
210 const files = { videofile: [ videoFileMetadata ] }
211 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup()
212
213 // multer required unsetting the Content-Type, now we can set it for node-uploadx
214 req.headers['content-type'] = 'application/json; charset=utf-8'
215 // place previewfile in metadata so that uploadx saves it in .META
216 if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile']
217
218 return next()
219 }
220 ])
221
222 const videosUpdateValidator = getCommonVideoEditAttributes().concat([
223 isValidVideoIdParam('id'),
224
225 body('name')
226 .optional()
227 .trim()
228 .custom(isVideoNameValid).withMessage(
229 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
230 ),
231 body('channelId')
232 .optional()
233 .customSanitizer(toIntOrNull)
234 .custom(isIdValid).withMessage('Should have correct video channel id'),
235
236 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
237 logger.debug('Checking videosUpdate parameters', { parameters: req.body })
238
239 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
240 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
241 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
242
243 // Check if the user who did the request is able to update the video
244 const user = res.locals.oauth.token.User
245 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
246
247 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
248
249 return next()
250 }
251 ])
252
253 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
254 const video = getVideoWithAttributes(res)
255
256 // Anybody can watch local videos
257 if (video.isOwned() === true) return next()
258
259 // Logged user
260 if (res.locals.oauth) {
261 // Users can search or watch remote videos
262 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
263 }
264
265 // Anybody can search or watch remote videos
266 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
267
268 // Check our instance follows an actor that shared this video
269 const serverActor = await getServerActor()
270 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
271
272 return res.fail({
273 status: HttpStatusCode.FORBIDDEN_403,
274 message: 'Cannot get this video regarding follow constraints',
275 type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
276 data: {
277 originUrl: video.url
278 }
279 })
280 }
281
282 const videosCustomGetValidator = (
283 fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes',
284 authenticateInQuery = false
285 ) => {
286 return [
287 isValidVideoIdParam('id'),
288
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 logger.debug('Checking videosGet parameters', { parameters: req.params })
291
292 if (areValidationErrors(req, res)) return
293 if (!await doesVideoExist(req.params.id, res, fetchType)) return
294
295 // Controllers does not need to check video rights
296 if (fetchType === 'only-immutable-attributes') return next()
297
298 const video = getVideoWithAttributes(res) as MVideoFullLight
299
300 // Video private or blacklisted
301 if (video.requiresAuth()) {
302 if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) return next()
303
304 return res.fail({
305 status: HttpStatusCode.FORBIDDEN_403,
306 message: 'Cannot get this private/internal or blocklisted video'
307 })
308 }
309
310 // Video is public, anyone can access it
311 if (video.privacy === VideoPrivacy.PUBLIC) return next()
312
313 // Video is unlisted, check we used the uuid to fetch it
314 if (video.privacy === VideoPrivacy.UNLISTED) {
315 if (isUUIDValid(req.params.id)) return next()
316
317 // Don't leak this unlisted video
318 return res.fail({
319 status: HttpStatusCode.NOT_FOUND_404,
320 message: 'Video not found'
321 })
322 }
323 }
324 ]
325 }
326
327 const videosGetValidator = videosCustomGetValidator('all')
328 const videosDownloadValidator = videosCustomGetValidator('all', true)
329
330 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
331 isValidVideoIdParam('id'),
332
333 param('videoFileId')
334 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
335
336 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
337 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
338
339 if (areValidationErrors(req, res)) return
340 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
341
342 return next()
343 }
344 ])
345
346 const videosRemoveValidator = [
347 isValidVideoIdParam('id'),
348
349 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
350 logger.debug('Checking videosRemove parameters', { parameters: req.params })
351
352 if (areValidationErrors(req, res)) return
353 if (!await doesVideoExist(req.params.id, res)) return
354
355 // Check if the user who did the request is able to delete the video
356 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
357
358 return next()
359 }
360 ]
361
362 const videosOverviewValidator = [
363 query('page')
364 .optional()
365 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
366 .withMessage('Should have a valid pagination'),
367
368 (req: express.Request, res: express.Response, next: express.NextFunction) => {
369 if (areValidationErrors(req, res)) return
370
371 return next()
372 }
373 ]
374
375 function getCommonVideoEditAttributes () {
376 return [
377 body('thumbnailfile')
378 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
379 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
380 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
381 ),
382 body('previewfile')
383 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
384 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
385 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
386 ),
387
388 body('category')
389 .optional()
390 .customSanitizer(toIntOrNull)
391 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
392 body('licence')
393 .optional()
394 .customSanitizer(toIntOrNull)
395 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
396 body('language')
397 .optional()
398 .customSanitizer(toValueOrNull)
399 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
400 body('nsfw')
401 .optional()
402 .customSanitizer(toBooleanOrNull)
403 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
404 body('waitTranscoding')
405 .optional()
406 .customSanitizer(toBooleanOrNull)
407 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
408 body('privacy')
409 .optional()
410 .customSanitizer(toValueOrNull)
411 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
412 body('description')
413 .optional()
414 .customSanitizer(toValueOrNull)
415 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
416 body('support')
417 .optional()
418 .customSanitizer(toValueOrNull)
419 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
420 body('tags')
421 .optional()
422 .customSanitizer(toValueOrNull)
423 .custom(isVideoTagsValid)
424 .withMessage(
425 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
426 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
427 ),
428 body('commentsEnabled')
429 .optional()
430 .customSanitizer(toBooleanOrNull)
431 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
432 body('downloadEnabled')
433 .optional()
434 .customSanitizer(toBooleanOrNull)
435 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
436 body('originallyPublishedAt')
437 .optional()
438 .customSanitizer(toValueOrNull)
439 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
440 body('scheduleUpdate')
441 .optional()
442 .customSanitizer(toValueOrNull),
443 body('scheduleUpdate.updateAt')
444 .optional()
445 .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
446 body('scheduleUpdate.privacy')
447 .optional()
448 .customSanitizer(toIntOrNull)
449 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
450 ] as (ValidationChain | ExpressPromiseHandler)[]
451 }
452
453 const commonVideosFiltersValidator = [
454 query('categoryOneOf')
455 .optional()
456 .customSanitizer(toArray)
457 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
458 query('licenceOneOf')
459 .optional()
460 .customSanitizer(toArray)
461 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
462 query('languageOneOf')
463 .optional()
464 .customSanitizer(toArray)
465 .custom(isStringArray).withMessage('Should have a valid one of language array'),
466 query('privacyOneOf')
467 .optional()
468 .customSanitizer(toArray)
469 .custom(isNumberArray).withMessage('Should have a valid one of privacy array'),
470 query('tagsOneOf')
471 .optional()
472 .customSanitizer(toArray)
473 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
474 query('tagsAllOf')
475 .optional()
476 .customSanitizer(toArray)
477 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
478 query('nsfw')
479 .optional()
480 .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
481 query('isLive')
482 .optional()
483 .customSanitizer(toBooleanOrNull)
484 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
485 query('filter')
486 .optional()
487 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
488 query('include')
489 .optional()
490 .custom(isVideoIncludeValid).withMessage('Should have a valid include attribute'),
491 query('isLocal')
492 .optional()
493 .customSanitizer(toBooleanOrNull)
494 .custom(isBooleanValid).withMessage('Should have a valid local boolean'),
495 query('hasHLSFiles')
496 .optional()
497 .customSanitizer(toBooleanOrNull)
498 .custom(isBooleanValid).withMessage('Should have a valid has hls boolean'),
499 query('hasWebtorrentFiles')
500 .optional()
501 .customSanitizer(toBooleanOrNull)
502 .custom(isBooleanValid).withMessage('Should have a valid has webtorrent boolean'),
503 query('skipCount')
504 .optional()
505 .customSanitizer(toBooleanOrNull)
506 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
507 query('search')
508 .optional()
509 .custom(exists).withMessage('Should have a valid search'),
510
511 (req: express.Request, res: express.Response, next: express.NextFunction) => {
512 logger.debug('Checking commons video filters query', { parameters: req.query })
513
514 if (areValidationErrors(req, res)) return
515
516 // FIXME: deprecated in 4.0, to remove
517 {
518 if (req.query.filter === 'all-local') {
519 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
520 req.query.isLocal = true
521 req.query.privacyOneOf = getAllPrivacies()
522 } else if (req.query.filter === 'all') {
523 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
524 req.query.privacyOneOf = getAllPrivacies()
525 } else if (req.query.filter === 'local') {
526 req.query.isLocal = true
527 }
528
529 req.query.filter = undefined
530 }
531
532 const user = res.locals.oauth?.token.User
533
534 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
535 if (req.query.include || req.query.privacyOneOf) {
536 return res.fail({
537 status: HttpStatusCode.UNAUTHORIZED_401,
538 message: 'You are not allowed to see all videos.'
539 })
540 }
541 }
542
543 return next()
544 }
545 ]
546
547 // ---------------------------------------------------------------------------
548
549 export {
550 videosAddLegacyValidator,
551 videosAddResumableValidator,
552 videosAddResumableInitValidator,
553
554 videosUpdateValidator,
555 videosGetValidator,
556 videoFileMetadataGetValidator,
557 videosDownloadValidator,
558 checkVideoFollowConstraints,
559 videosCustomGetValidator,
560 videosRemoveValidator,
561
562 getCommonVideoEditAttributes,
563
564 commonVideosFiltersValidator,
565
566 videosOverviewValidator
567 }
568
569 // ---------------------------------------------------------------------------
570
571 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
572 if (req.body.scheduleUpdate) {
573 if (!req.body.scheduleUpdate.updateAt) {
574 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
575
576 res.fail({ message: 'Schedule update at is mandatory.' })
577 return true
578 }
579 }
580
581 return false
582 }
583
584 async function commonVideoChecksPass (parameters: {
585 req: express.Request
586 res: express.Response
587 user: MUserAccountId
588 videoFileSize: number
589 files: express.UploadFilesForCheck
590 }): Promise<boolean> {
591 const { req, res, user, videoFileSize, files } = parameters
592
593 if (areErrorsInScheduleUpdate(req, res)) return false
594
595 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
596
597 if (!isVideoFileMimeTypeValid(files)) {
598 res.fail({
599 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
600 message: 'This file is not supported. Please, make sure it is of the following type: ' +
601 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
602 })
603 return false
604 }
605
606 if (!isVideoFileSizeValid(videoFileSize.toString())) {
607 res.fail({
608 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
609 message: 'This file is too large. It exceeds the maximum file size authorized.',
610 type: ServerErrorCode.MAX_FILE_SIZE_REACHED
611 })
612 return false
613 }
614
615 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
616 res.fail({
617 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
618 message: 'The user video quota is exceeded with this video.',
619 type: ServerErrorCode.QUOTA_REACHED
620 })
621 return false
622 }
623
624 return true
625 }
626
627 export async function isVideoAccepted (
628 req: express.Request,
629 res: express.Response,
630 videoFile: express.VideoUploadFile
631 ) {
632 // Check we accept this video
633 const acceptParameters = {
634 videoBody: req.body,
635 videoFile,
636 user: res.locals.oauth.token.User
637 }
638 const acceptedResult = await Hooks.wrapFun(
639 isLocalVideoAccepted,
640 acceptParameters,
641 'filter:api.video.upload.accept.result'
642 )
643
644 if (!acceptedResult || acceptedResult.accepted !== true) {
645 logger.info('Refused local video.', { acceptedResult, acceptParameters })
646 res.fail({
647 status: HttpStatusCode.FORBIDDEN_403,
648 message: acceptedResult.errorMessage || 'Refused local video'
649 })
650 return false
651 }
652
653 return true
654 }
655
656 async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
657 const duration: number = await getDurationFromVideoFile(videoFile.path)
658
659 if (isNaN(duration)) throw new Error(`Couldn't get video duration`)
660
661 videoFile.duration = duration
662 }