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