aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares/validators/videos
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-02-11 10:51:33 +0100
committerChocobozzz <chocobozzz@cpy.re>2022-02-28 10:42:19 +0100
commitc729caf6cc34630877a0e5a1bda1719384cd0c8a (patch)
tree1d2e13722e518c73d2c9e6f0969615e29d51cf8c /server/middlewares/validators/videos
parenta24bf4dc659cebb65d887862bf21d7a35e9ec791 (diff)
downloadPeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.gz
PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.zst
PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.zip
Add basic video editor support
Diffstat (limited to 'server/middlewares/validators/videos')
-rw-r--r--server/middlewares/validators/videos/index.ts1
-rw-r--r--server/middlewares/validators/videos/video-captions.ts10
-rw-r--r--server/middlewares/validators/videos/video-comments.ts14
-rw-r--r--server/middlewares/validators/videos/video-editor.ts112
-rw-r--r--server/middlewares/validators/videos/video-ownership-changes.ts21
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts4
-rw-r--r--server/middlewares/validators/videos/videos.ts40
7 files changed, 138 insertions, 64 deletions
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
index f365d8ee1..faa082510 100644
--- a/server/middlewares/validators/videos/index.ts
+++ b/server/middlewares/validators/videos/index.ts
@@ -2,6 +2,7 @@ export * from './video-blacklist'
2export * from './video-captions' 2export * from './video-captions'
3export * from './video-channels' 3export * from './video-channels'
4export * from './video-comments' 4export * from './video-comments'
5export * from './video-editor'
5export * from './video-files' 6export * from './video-files'
6export * from './video-imports' 7export * from './video-imports'
7export * from './video-live' 8export * from './video-live'
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index a399871e1..441c6b4be 100644
--- a/server/middlewares/validators/videos/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -1,6 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { body, param } from 'express-validator' 2import { body, param } from 'express-validator'
3import { HttpStatusCode, UserRight } from '@shared/models' 3import { UserRight } from '@shared/models'
4import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' 4import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
5import { cleanUpReqFiles } from '../../../helpers/express-utils' 5import { cleanUpReqFiles } from '../../../helpers/express-utils'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
@@ -74,13 +74,7 @@ const listVideoCaptionsValidator = [
74 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 74 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
75 75
76 const video = res.locals.onlyVideo 76 const video = res.locals.onlyVideo
77 77 if (!await checkCanSeeVideoIfPrivate(req, res, video)) return
78 if (!await checkCanSeeVideoIfPrivate(req, res, video)) {
79 return res.fail({
80 status: HttpStatusCode.FORBIDDEN_403,
81 message: 'Cannot list captions of private/internal/blocklisted video'
82 })
83 }
84 78
85 return next() 79 return next()
86 } 80 }
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 91e85711d..96d956035 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -54,12 +54,7 @@ const listVideoCommentThreadsValidator = [
54 if (areValidationErrors(req, res)) return 54 if (areValidationErrors(req, res)) return
55 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 55 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
56 56
57 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) { 57 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return
58 return res.fail({
59 status: HttpStatusCode.FORBIDDEN_403,
60 message: 'Cannot list comments of private/internal/blocklisted video'
61 })
62 }
63 58
64 return next() 59 return next()
65 } 60 }
@@ -78,12 +73,7 @@ const listVideoThreadCommentsValidator = [
78 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 73 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
79 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return 74 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
80 75
81 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) { 76 if (!await checkCanSeeVideoIfPrivate(req, res, res.locals.onlyVideo)) return
82 return res.fail({
83 status: HttpStatusCode.FORBIDDEN_403,
84 message: 'Cannot list threads of private/internal/blocklisted video'
85 })
86 }
87 77
88 return next() 78 return next()
89 } 79 }
diff --git a/server/middlewares/validators/videos/video-editor.ts b/server/middlewares/validators/videos/video-editor.ts
new file mode 100644
index 000000000..9be97be93
--- /dev/null
+++ b/server/middlewares/validators/videos/video-editor.ts
@@ -0,0 +1,112 @@
1import express from 'express'
2import { body, param } from 'express-validator'
3import { isIdOrUUIDValid } from '@server/helpers/custom-validators/misc'
4import {
5 isEditorCutTaskValid,
6 isEditorTaskAddIntroOutroValid,
7 isEditorTaskAddWatermarkValid,
8 isValidEditorTasksArray
9} from '@server/helpers/custom-validators/video-editor'
10import { cleanUpReqFiles } from '@server/helpers/express-utils'
11import { CONFIG } from '@server/initializers/config'
12import { approximateIntroOutroAdditionalSize, getTaskFile } from '@server/lib/video-editor'
13import { isAudioFile } from '@shared/extra-utils'
14import { HttpStatusCode, UserRight, VideoEditorCreateEdition, VideoEditorTask, VideoState } from '@shared/models'
15import { logger } from '../../../helpers/logger'
16import { areValidationErrors, checkUserCanManageVideo, checkUserQuota, doesVideoExist } from '../shared'
17
18const videosEditorAddEditionValidator = [
19 param('videoId').custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
20
21 body('tasks').custom(isValidEditorTasksArray).withMessage('Should have a valid array of tasks'),
22
23 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
24 logger.debug('Checking videosEditorAddEditionValidator parameters.', { parameters: req.params, body: req.body, files: req.files })
25
26 if (CONFIG.VIDEO_EDITOR.ENABLED !== true) {
27 res.fail({
28 status: HttpStatusCode.BAD_REQUEST_400,
29 message: 'Video editor is disabled on this instance'
30 })
31
32 return cleanUpReqFiles(req)
33 }
34
35 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
36
37 const body: VideoEditorCreateEdition = req.body
38 const files = req.files as Express.Multer.File[]
39
40 for (let i = 0; i < body.tasks.length; i++) {
41 const task = body.tasks[i]
42
43 if (!checkTask(req, task, i)) {
44 res.fail({
45 status: HttpStatusCode.BAD_REQUEST_400,
46 message: `Task ${task.name} is invalid`
47 })
48
49 return cleanUpReqFiles(req)
50 }
51
52 if (task.name === 'add-intro' || task.name === 'add-outro') {
53 const filePath = getTaskFile(files, i).path
54
55 // Our concat filter needs a video stream
56 if (await isAudioFile(filePath)) {
57 res.fail({
58 status: HttpStatusCode.BAD_REQUEST_400,
59 message: `Task ${task.name} is invalid: file does not contain a video stream`
60 })
61
62 return cleanUpReqFiles(req)
63 }
64 }
65 }
66
67 if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req)
68
69 const video = res.locals.videoAll
70 if (video.state === VideoState.TO_TRANSCODE || video.state === VideoState.TO_EDIT) {
71 res.fail({
72 status: HttpStatusCode.CONFLICT_409,
73 message: 'Cannot edit video that is already waiting for transcoding/edition'
74 })
75
76 return cleanUpReqFiles(req)
77 }
78
79 const user = res.locals.oauth.token.User
80 if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
81
82 // Try to make an approximation of bytes added by the intro/outro
83 const additionalBytes = await approximateIntroOutroAdditionalSize(video, body.tasks, i => getTaskFile(files, i).path)
84 if (await checkUserQuota(user, additionalBytes, res) === false) return cleanUpReqFiles(req)
85
86 return next()
87 }
88]
89
90// ---------------------------------------------------------------------------
91
92export {
93 videosEditorAddEditionValidator
94}
95
96// ---------------------------------------------------------------------------
97
98const taskCheckers: {
99 [id in VideoEditorTask['name']]: (task: VideoEditorTask, indice?: number, files?: Express.Multer.File[]) => boolean
100} = {
101 'cut': isEditorCutTaskValid,
102 'add-intro': isEditorTaskAddIntroOutroValid,
103 'add-outro': isEditorTaskAddIntroOutroValid,
104 'add-watermark': isEditorTaskAddWatermarkValid
105}
106
107function checkTask (req: express.Request, task: VideoEditorTask, indice?: number) {
108 const checker = taskCheckers[task.name]
109 if (!checker) return false
110
111 return checker(task, indice, req.files as Express.Multer.File[])
112}
diff --git a/server/middlewares/validators/videos/video-ownership-changes.ts b/server/middlewares/validators/videos/video-ownership-changes.ts
index 95e4cebce..6dcdc05f5 100644
--- a/server/middlewares/validators/videos/video-ownership-changes.ts
+++ b/server/middlewares/validators/videos/video-ownership-changes.ts
@@ -3,20 +3,13 @@ import { param } from 'express-validator'
3import { isIdValid } from '@server/helpers/custom-validators/misc' 3import { isIdValid } from '@server/helpers/custom-validators/misc'
4import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership' 4import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership'
5import { logger } from '@server/helpers/logger' 5import { logger } from '@server/helpers/logger'
6import { isAbleToUploadVideo } from '@server/lib/user'
7import { AccountModel } from '@server/models/account/account' 6import { AccountModel } from '@server/models/account/account'
8import { MVideoWithAllFiles } from '@server/types/models' 7import { MVideoWithAllFiles } from '@server/types/models'
9import { 8import { HttpStatusCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@shared/models'
10 HttpStatusCode,
11 ServerErrorCode,
12 UserRight,
13 VideoChangeOwnershipAccept,
14 VideoChangeOwnershipStatus,
15 VideoState
16} from '@shared/models'
17import { 9import {
18 areValidationErrors, 10 areValidationErrors,
19 checkUserCanManageVideo, 11 checkUserCanManageVideo,
12 checkUserQuota,
20 doesChangeVideoOwnershipExist, 13 doesChangeVideoOwnershipExist,
21 doesVideoChannelOfAccountExist, 14 doesVideoChannelOfAccountExist,
22 doesVideoExist, 15 doesVideoExist,
@@ -113,15 +106,7 @@ async function checkCanAccept (video: MVideoWithAllFiles, res: express.Response)
113 106
114 const user = res.locals.oauth.token.User 107 const user = res.locals.oauth.token.User
115 108
116 if (!await isAbleToUploadVideo(user.id, video.getMaxQualityFile().size)) { 109 if (!await checkUserQuota(user, video.getMaxQualityFile().size, res)) return false
117 res.fail({
118 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
119 message: 'The user video quota is exceeded with this video.',
120 type: ServerErrorCode.QUOTA_REACHED
121 })
122
123 return false
124 }
125 110
126 return true 111 return true
127} 112}
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index f5fee845e..241b9ed7b 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -27,7 +27,7 @@ import {
27 isVideoPlaylistTimestampValid, 27 isVideoPlaylistTimestampValid,
28 isVideoPlaylistTypeValid 28 isVideoPlaylistTypeValid
29} from '../../../helpers/custom-validators/video-playlists' 29} from '../../../helpers/custom-validators/video-playlists'
30import { isVideoImage } from '../../../helpers/custom-validators/videos' 30import { isVideoImageValid } from '../../../helpers/custom-validators/videos'
31import { cleanUpReqFiles } from '../../../helpers/express-utils' 31import { cleanUpReqFiles } from '../../../helpers/express-utils'
32import { logger } from '../../../helpers/logger' 32import { logger } from '../../../helpers/logger'
33import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 33import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
@@ -390,7 +390,7 @@ export {
390function getCommonPlaylistEditAttributes () { 390function getCommonPlaylistEditAttributes () {
391 return [ 391 return [
392 body('thumbnailfile') 392 body('thumbnailfile')
393 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')) 393 .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile'))
394 .withMessage( 394 .withMessage(
395 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' + 395 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
396 CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ') 396 CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index b3ffb7007..26597cf7b 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -3,7 +3,6 @@ import { body, header, param, query, ValidationChain } from 'express-validator'
3import { isTestInstance } from '@server/helpers/core-utils' 3import { isTestInstance } from '@server/helpers/core-utils'
4import { getResumableUploadPath } from '@server/helpers/upload' 4import { getResumableUploadPath } from '@server/helpers/upload'
5import { Redis } from '@server/lib/redis' 5import { Redis } from '@server/lib/redis'
6import { isAbleToUploadVideo } from '@server/lib/user'
7import { getServerActor } from '@server/models/application/application' 6import { getServerActor } from '@server/models/application/application'
8import { ExpressPromiseHandler } from '@server/types/express-handler' 7import { ExpressPromiseHandler } from '@server/types/express-handler'
9import { MUserAccountId, MVideoFullLight } from '@server/types/models' 8import { MUserAccountId, MVideoFullLight } from '@server/types/models'
@@ -13,7 +12,7 @@ import {
13 exists, 12 exists,
14 isBooleanValid, 13 isBooleanValid,
15 isDateValid, 14 isDateValid,
16 isFileFieldValid, 15 isFileValid,
17 isIdValid, 16 isIdValid,
18 isUUIDValid, 17 isUUIDValid,
19 toArray, 18 toArray,
@@ -23,24 +22,24 @@ import {
23} from '../../../helpers/custom-validators/misc' 22} from '../../../helpers/custom-validators/misc'
24import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 23import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
25import { 24import {
25 areVideoTagsValid,
26 isScheduleVideoUpdatePrivacyValid, 26 isScheduleVideoUpdatePrivacyValid,
27 isVideoCategoryValid, 27 isVideoCategoryValid,
28 isVideoDescriptionValid, 28 isVideoDescriptionValid,
29 isVideoFileMimeTypeValid, 29 isVideoFileMimeTypeValid,
30 isVideoFileSizeValid, 30 isVideoFileSizeValid,
31 isVideoFilterValid, 31 isVideoFilterValid,
32 isVideoImage, 32 isVideoImageValid,
33 isVideoIncludeValid, 33 isVideoIncludeValid,
34 isVideoLanguageValid, 34 isVideoLanguageValid,
35 isVideoLicenceValid, 35 isVideoLicenceValid,
36 isVideoNameValid, 36 isVideoNameValid,
37 isVideoOriginallyPublishedAtValid, 37 isVideoOriginallyPublishedAtValid,
38 isVideoPrivacyValid, 38 isVideoPrivacyValid,
39 isVideoSupportValid, 39 isVideoSupportValid
40 isVideoTagsValid
41} from '../../../helpers/custom-validators/videos' 40} from '../../../helpers/custom-validators/videos'
42import { cleanUpReqFiles } from '../../../helpers/express-utils' 41import { cleanUpReqFiles } from '../../../helpers/express-utils'
43import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils' 42import { getVideoStreamDuration } from '../../../helpers/ffmpeg'
44import { logger } from '../../../helpers/logger' 43import { logger } from '../../../helpers/logger'
45import { deleteFileAndCatch } from '../../../helpers/utils' 44import { deleteFileAndCatch } from '../../../helpers/utils'
46import { getVideoWithAttributes } from '../../../helpers/video' 45import { getVideoWithAttributes } from '../../../helpers/video'
@@ -53,6 +52,7 @@ import {
53 areValidationErrors, 52 areValidationErrors,
54 checkCanSeePrivateVideo, 53 checkCanSeePrivateVideo,
55 checkUserCanManageVideo, 54 checkUserCanManageVideo,
55 checkUserQuota,
56 doesVideoChannelOfAccountExist, 56 doesVideoChannelOfAccountExist,
57 doesVideoExist, 57 doesVideoExist,
58 doesVideoFileOfVideoExist, 58 doesVideoFileOfVideoExist,
@@ -61,7 +61,7 @@ import {
61 61
62const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ 62const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
63 body('videofile') 63 body('videofile')
64 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile')) 64 .custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null }))
65 .withMessage('Should have a file'), 65 .withMessage('Should have a file'),
66 body('name') 66 body('name')
67 .trim() 67 .trim()
@@ -299,12 +299,11 @@ const videosCustomGetValidator = (
299 299
300 // Video private or blacklisted 300 // Video private or blacklisted
301 if (video.requiresAuth()) { 301 if (video.requiresAuth()) {
302 if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) return next() 302 if (await checkCanSeePrivateVideo(req, res, video, authenticateInQuery)) {
303 return next()
304 }
303 305
304 return res.fail({ 306 return
305 status: HttpStatusCode.FORBIDDEN_403,
306 message: 'Cannot get this private/internal or blocklisted video'
307 })
308 } 307 }
309 308
310 // Video is public, anyone can access it 309 // Video is public, anyone can access it
@@ -375,12 +374,12 @@ const videosOverviewValidator = [
375function getCommonVideoEditAttributes () { 374function getCommonVideoEditAttributes () {
376 return [ 375 return [
377 body('thumbnailfile') 376 body('thumbnailfile')
378 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage( 377 .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage(
379 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' + 378 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
380 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') 379 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
381 ), 380 ),
382 body('previewfile') 381 body('previewfile')
383 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage( 382 .custom((value, { req }) => isVideoImageValid(req.files, 'previewfile')).withMessage(
384 'This preview file is not supported or too large. Please, make sure it is of the following type: ' + 383 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
385 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') 384 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
386 ), 385 ),
@@ -420,7 +419,7 @@ function getCommonVideoEditAttributes () {
420 body('tags') 419 body('tags')
421 .optional() 420 .optional()
422 .customSanitizer(toValueOrNull) 421 .customSanitizer(toValueOrNull)
423 .custom(isVideoTagsValid) 422 .custom(areVideoTagsValid)
424 .withMessage( 423 .withMessage(
425 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` + 424 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
426 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each` 425 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
@@ -612,14 +611,7 @@ async function commonVideoChecksPass (parameters: {
612 return false 611 return false
613 } 612 }
614 613
615 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { 614 if (await checkUserQuota(user, videoFileSize, res) === false) return false
616 res.fail({
617 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
618 message: 'The user video quota is exceeded with this video.',
619 type: ServerErrorCode.QUOTA_REACHED
620 })
621 return false
622 }
623 615
624 return true 616 return true
625} 617}
@@ -654,7 +646,7 @@ export async function isVideoAccepted (
654} 646}
655 647
656async function addDurationToVideo (videoFile: { path: string, duration?: number }) { 648async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
657 const duration: number = await getDurationFromVideoFile(videoFile.path) 649 const duration: number = await getVideoStreamDuration(videoFile.path)
658 650
659 if (isNaN(duration)) throw new Error(`Couldn't get video duration`) 651 if (isNaN(duration)) throw new Error(`Couldn't get video duration`)
660 652