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