]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/videos/videos.ts
Merge branch 'release/4.0.0' into develop
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
1 import express from 'express'
2 import { body, header, param, query, ValidationChain } from 'express-validator'
3 import { isTestInstance } from '@server/helpers/core-utils'
4 import { getResumableUploadPath } from '@server/helpers/upload'
5 import { Redis } from '@server/lib/redis'
6 import { isAbleToUploadVideo } from '@server/lib/user'
7 import { getServerActor } from '@server/models/application/application'
8 import { ExpressPromiseHandler } from '@server/types/express-handler'
9 import { MUserAccountId, MVideoFullLight } from '@server/types/models'
10 import { getAllPrivacies } from '@shared/core-utils'
11 import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoPrivacy } from '@shared/models'
12 import {
13 exists,
14 isBooleanValid,
15 isDateValid,
16 isFileFieldValid,
17 isIdValid,
18 isUUIDValid,
19 toArray,
20 toBooleanOrNull,
21 toIntOrNull,
22 toValueOrNull
23 } from '../../../helpers/custom-validators/misc'
24 import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
25 import {
26 isScheduleVideoUpdatePrivacyValid,
27 isVideoCategoryValid,
28 isVideoDescriptionValid,
29 isVideoFileMimeTypeValid,
30 isVideoFileSizeValid,
31 isVideoFilterValid,
32 isVideoImage,
33 isVideoIncludeValid,
34 isVideoLanguageValid,
35 isVideoLicenceValid,
36 isVideoNameValid,
37 isVideoOriginallyPublishedAtValid,
38 isVideoPrivacyValid,
39 isVideoSupportValid,
40 isVideoTagsValid
41 } from '../../../helpers/custom-validators/videos'
42 import { cleanUpReqFiles } from '../../../helpers/express-utils'
43 import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
44 import { logger } from '../../../helpers/logger'
45 import { deleteFileAndCatch } from '../../../helpers/utils'
46 import { getVideoWithAttributes } from '../../../helpers/video'
47 import { CONFIG } from '../../../initializers/config'
48 import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
49 import { isLocalVideoAccepted } from '../../../lib/moderation'
50 import { Hooks } from '../../../lib/plugins/hooks'
51 import { VideoModel } from '../../../models/video/video'
52 import {
53 areValidationErrors,
54 checkCanSeePrivateVideo,
55 checkUserCanManageVideo,
56 doesVideoChannelOfAccountExist,
57 doesVideoExist,
58 doesVideoFileOfVideoExist,
59 isValidVideoIdParam
60 } from '../shared'
61
62 const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
63 body('videofile')
64 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile'))
65 .withMessage('Should have a file'),
66 body('name')
67 .trim()
68 .custom(isVideoNameValid).withMessage(
69 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
70 ),
71 body('channelId')
72 .customSanitizer(toIntOrNull)
73 .custom(isIdValid).withMessage('Should have correct video channel id'),
74
75 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
77
78 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
79
80 const videoFile: express.VideoUploadFile = req.files['videofile'][0]
81 const user = res.locals.oauth.token.User
82
83 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) {
84 return cleanUpReqFiles(req)
85 }
86
87 try {
88 if (!videoFile.duration) await addDurationToVideo(videoFile)
89 } catch (err) {
90 logger.error('Invalid input file in videosAddLegacyValidator.', { err })
91
92 res.fail({
93 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
94 message: 'Video file unreadable.'
95 })
96 return cleanUpReqFiles(req)
97 }
98
99 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
100
101 return next()
102 }
103 ])
104
105 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 if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) return next()
319
320 return res.fail({
321 status: HttpStatusCode.FORBIDDEN_403,
322 message: 'Cannot get this private/internal or blocklisted video'
323 })
324 }
325
326 // Video is public, anyone can access it
327 if (video.privacy === VideoPrivacy.PUBLIC) return next()
328
329 // Video is unlisted, check we used the uuid to fetch it
330 if (video.privacy === VideoPrivacy.UNLISTED) {
331 if (isUUIDValid(req.params.id)) return next()
332
333 // Don't leak this unlisted video
334 return res.fail({
335 status: HttpStatusCode.NOT_FOUND_404,
336 message: 'Video not found'
337 })
338 }
339 }
340 ]
341 }
342
343 const videosGetValidator = videosCustomGetValidator('all')
344 const videosDownloadValidator = videosCustomGetValidator('all', true)
345
346 const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
347 isValidVideoIdParam('id'),
348
349 param('videoFileId')
350 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
351
352 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
353 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
354
355 if (areValidationErrors(req, res)) return
356 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
357
358 return next()
359 }
360 ])
361
362 const videosRemoveValidator = [
363 isValidVideoIdParam('id'),
364
365 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
366 logger.debug('Checking videosRemove parameters', { parameters: req.params })
367
368 if (areValidationErrors(req, res)) return
369 if (!await doesVideoExist(req.params.id, res)) return
370
371 // Check if the user who did the request is able to delete the video
372 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
373
374 return next()
375 }
376 ]
377
378 const videosOverviewValidator = [
379 query('page')
380 .optional()
381 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
382 .withMessage('Should have a valid pagination'),
383
384 (req: express.Request, res: express.Response, next: express.NextFunction) => {
385 if (areValidationErrors(req, res)) return
386
387 return next()
388 }
389 ]
390
391 function getCommonVideoEditAttributes () {
392 return [
393 body('thumbnailfile')
394 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
395 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
396 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
397 ),
398 body('previewfile')
399 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
400 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
401 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
402 ),
403
404 body('category')
405 .optional()
406 .customSanitizer(toIntOrNull)
407 .custom(isVideoCategoryValid).withMessage('Should have a valid category'),
408 body('licence')
409 .optional()
410 .customSanitizer(toIntOrNull)
411 .custom(isVideoLicenceValid).withMessage('Should have a valid licence'),
412 body('language')
413 .optional()
414 .customSanitizer(toValueOrNull)
415 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
416 body('nsfw')
417 .optional()
418 .customSanitizer(toBooleanOrNull)
419 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
420 body('waitTranscoding')
421 .optional()
422 .customSanitizer(toBooleanOrNull)
423 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
424 body('privacy')
425 .optional()
426 .customSanitizer(toValueOrNull)
427 .custom(isVideoPrivacyValid).withMessage('Should have correct video privacy'),
428 body('description')
429 .optional()
430 .customSanitizer(toValueOrNull)
431 .custom(isVideoDescriptionValid).withMessage('Should have a valid description'),
432 body('support')
433 .optional()
434 .customSanitizer(toValueOrNull)
435 .custom(isVideoSupportValid).withMessage('Should have a valid support text'),
436 body('tags')
437 .optional()
438 .customSanitizer(toValueOrNull)
439 .custom(isVideoTagsValid)
440 .withMessage(
441 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
442 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
443 ),
444 body('commentsEnabled')
445 .optional()
446 .customSanitizer(toBooleanOrNull)
447 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
448 body('downloadEnabled')
449 .optional()
450 .customSanitizer(toBooleanOrNull)
451 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
452 body('originallyPublishedAt')
453 .optional()
454 .customSanitizer(toValueOrNull)
455 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
456 body('scheduleUpdate')
457 .optional()
458 .customSanitizer(toValueOrNull),
459 body('scheduleUpdate.updateAt')
460 .optional()
461 .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
462 body('scheduleUpdate.privacy')
463 .optional()
464 .customSanitizer(toIntOrNull)
465 .custom(isScheduleVideoUpdatePrivacyValid).withMessage('Should have correct schedule update privacy')
466 ] as (ValidationChain | ExpressPromiseHandler)[]
467 }
468
469 const commonVideosFiltersValidator = [
470 query('categoryOneOf')
471 .optional()
472 .customSanitizer(toArray)
473 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
474 query('licenceOneOf')
475 .optional()
476 .customSanitizer(toArray)
477 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
478 query('languageOneOf')
479 .optional()
480 .customSanitizer(toArray)
481 .custom(isStringArray).withMessage('Should have a valid one of language array'),
482 query('privacyOneOf')
483 .optional()
484 .customSanitizer(toArray)
485 .custom(isNumberArray).withMessage('Should have a valid one of privacy array'),
486 query('tagsOneOf')
487 .optional()
488 .customSanitizer(toArray)
489 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
490 query('tagsAllOf')
491 .optional()
492 .customSanitizer(toArray)
493 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
494 query('nsfw')
495 .optional()
496 .custom(isBooleanBothQueryValid).withMessage('Should have a valid NSFW attribute'),
497 query('isLive')
498 .optional()
499 .customSanitizer(toBooleanOrNull)
500 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
501 query('filter')
502 .optional()
503 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
504 query('include')
505 .optional()
506 .custom(isVideoIncludeValid).withMessage('Should have a valid include attribute'),
507 query('isLocal')
508 .optional()
509 .customSanitizer(toBooleanOrNull)
510 .custom(isBooleanValid).withMessage('Should have a valid local boolean'),
511 query('hasHLSFiles')
512 .optional()
513 .customSanitizer(toBooleanOrNull)
514 .custom(isBooleanValid).withMessage('Should have a valid has hls boolean'),
515 query('hasWebtorrentFiles')
516 .optional()
517 .customSanitizer(toBooleanOrNull)
518 .custom(isBooleanValid).withMessage('Should have a valid has webtorrent boolean'),
519 query('skipCount')
520 .optional()
521 .customSanitizer(toBooleanOrNull)
522 .custom(isBooleanValid).withMessage('Should have a valid skip count boolean'),
523 query('search')
524 .optional()
525 .custom(exists).withMessage('Should have a valid search'),
526
527 (req: express.Request, res: express.Response, next: express.NextFunction) => {
528 logger.debug('Checking commons video filters query', { parameters: req.query })
529
530 if (areValidationErrors(req, res)) return
531
532 // FIXME: deprecated in 4.0, to remove
533 {
534 if (req.query.filter === 'all-local') {
535 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
536 req.query.isLocal = true
537 req.query.privacyOneOf = getAllPrivacies()
538 } else if (req.query.filter === 'all') {
539 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
540 req.query.privacyOneOf = getAllPrivacies()
541 } else if (req.query.filter === 'local') {
542 req.query.isLocal = true
543 }
544
545 req.query.filter = undefined
546 }
547
548 const user = res.locals.oauth?.token.User
549
550 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
551 if (req.query.include || req.query.privacyOneOf) {
552 return res.fail({
553 status: HttpStatusCode.UNAUTHORIZED_401,
554 message: 'You are not allowed to see all videos.'
555 })
556 }
557 }
558
559 return next()
560 }
561 ]
562
563 // ---------------------------------------------------------------------------
564
565 export {
566 videosAddLegacyValidator,
567 videosAddResumableValidator,
568 videosAddResumableInitValidator,
569 videosResumableUploadIdValidator,
570
571 videosUpdateValidator,
572 videosGetValidator,
573 videoFileMetadataGetValidator,
574 videosDownloadValidator,
575 checkVideoFollowConstraints,
576 videosCustomGetValidator,
577 videosRemoveValidator,
578
579 getCommonVideoEditAttributes,
580
581 commonVideosFiltersValidator,
582
583 videosOverviewValidator
584 }
585
586 // ---------------------------------------------------------------------------
587
588 function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
589 if (req.body.scheduleUpdate) {
590 if (!req.body.scheduleUpdate.updateAt) {
591 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
592
593 res.fail({ message: 'Schedule update at is mandatory.' })
594 return true
595 }
596 }
597
598 return false
599 }
600
601 async function commonVideoChecksPass (parameters: {
602 req: express.Request
603 res: express.Response
604 user: MUserAccountId
605 videoFileSize: number
606 files: express.UploadFilesForCheck
607 }): Promise<boolean> {
608 const { req, res, user, videoFileSize, files } = parameters
609
610 if (areErrorsInScheduleUpdate(req, res)) return false
611
612 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
613
614 if (!isVideoFileMimeTypeValid(files)) {
615 res.fail({
616 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
617 message: 'This file is not supported. Please, make sure it is of the following type: ' +
618 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
619 })
620 return false
621 }
622
623 if (!isVideoFileSizeValid(videoFileSize.toString())) {
624 res.fail({
625 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
626 message: 'This file is too large. It exceeds the maximum file size authorized.',
627 type: ServerErrorCode.MAX_FILE_SIZE_REACHED
628 })
629 return false
630 }
631
632 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
633 res.fail({
634 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
635 message: 'The user video quota is exceeded with this video.',
636 type: ServerErrorCode.QUOTA_REACHED
637 })
638 return false
639 }
640
641 return true
642 }
643
644 export async function isVideoAccepted (
645 req: express.Request,
646 res: express.Response,
647 videoFile: express.VideoUploadFile
648 ) {
649 // Check we accept this video
650 const acceptParameters = {
651 videoBody: req.body,
652 videoFile,
653 user: res.locals.oauth.token.User
654 }
655 const acceptedResult = await Hooks.wrapFun(
656 isLocalVideoAccepted,
657 acceptParameters,
658 'filter:api.video.upload.accept.result'
659 )
660
661 if (!acceptedResult || acceptedResult.accepted !== true) {
662 logger.info('Refused local video.', { acceptedResult, acceptParameters })
663 res.fail({
664 status: HttpStatusCode.FORBIDDEN_403,
665 message: acceptedResult.errorMessage || 'Refused local video'
666 })
667 return false
668 }
669
670 return true
671 }
672
673 async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
674 const duration: number = await getDurationFromVideoFile(videoFile.path)
675
676 if (isNaN(duration)) throw new Error(`Couldn't get video duration`)
677
678 videoFile.duration = duration
679 }