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