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