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