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