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