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