aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares/validators/videos
diff options
context:
space:
mode:
Diffstat (limited to 'server/middlewares/validators/videos')
-rw-r--r--server/middlewares/validators/videos/videos.ts184
1 files changed, 152 insertions, 32 deletions
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index bb617d77c..d26bcd4a6 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -1,9 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator' 2import { body, header, param, query, ValidationChain } from 'express-validator'
3import { getResumableUploadPath } from '@server/helpers/upload'
3import { isAbleToUploadVideo } from '@server/lib/user' 4import { isAbleToUploadVideo } from '@server/lib/user'
4import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
5import { ExpressPromiseHandler } from '@server/types/express' 6import { ExpressPromiseHandler } from '@server/types/express'
6import { MVideoWithRights } from '@server/types/models' 7import { MUserAccountId, MVideoWithRights } from '@server/types/models'
7import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' 8import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 9import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
9import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' 10import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
@@ -47,6 +48,7 @@ import {
47 doesVideoExist, 48 doesVideoExist,
48 doesVideoFileOfVideoExist 49 doesVideoFileOfVideoExist
49} from '../../../helpers/middlewares' 50} from '../../../helpers/middlewares'
51import { deleteFileAndCatch } from '../../../helpers/utils'
50import { getVideoWithAttributes } from '../../../helpers/video' 52import { getVideoWithAttributes } from '../../../helpers/video'
51import { CONFIG } from '../../../initializers/config' 53import { CONFIG } from '../../../initializers/config'
52import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' 54import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
@@ -57,7 +59,7 @@ import { VideoModel } from '../../../models/video/video'
57import { authenticatePromiseIfNeeded } from '../../auth' 59import { authenticatePromiseIfNeeded } from '../../auth'
58import { areValidationErrors } from '../utils' 60import { areValidationErrors } from '../utils'
59 61
60const videosAddValidator = getCommonVideoEditAttributes().concat([ 62const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
61 body('videofile') 63 body('videofile')
62 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile')) 64 .custom((value, { req }) => isFileFieldValid(req.files, 'videofile'))
63 .withMessage('Should have a file'), 65 .withMessage('Should have a file'),
@@ -73,54 +75,117 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([
73 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) 75 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
74 76
75 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 77 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
76 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
77 78
78 const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0] 79 const videoFile: express.VideoUploadFile = req.files['videofile'][0]
79 const user = res.locals.oauth.token.User 80 const user = res.locals.oauth.token.User
80 81
81 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 82 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) {
82
83 if (!isVideoFileMimeTypeValid(req.files)) {
84 res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
85 .json({
86 error: 'This file is not supported. Please, make sure it is of the following type: ' +
87 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
88 })
89
90 return cleanUpReqFiles(req) 83 return cleanUpReqFiles(req)
91 } 84 }
92 85
93 if (!isVideoFileSizeValid(videoFile.size.toString())) { 86 try {
94 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) 87 if (!videoFile.duration) await addDurationToVideo(videoFile)
95 .json({ 88 } catch (err) {
96 error: 'This file is too large.' 89 logger.error('Invalid input file in videosAddLegacyValidator.', { err })
97 }) 90 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
91 .json({ error: 'Video file unreadable.' })
98 92
99 return cleanUpReqFiles(req) 93 return cleanUpReqFiles(req)
100 } 94 }
101 95
102 if (await isAbleToUploadVideo(user.id, videoFile.size) === false) { 96 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
103 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
104 .json({ error: 'The user video quota is exceeded with this video.' })
105 97
106 return cleanUpReqFiles(req) 98 return next()
107 } 99 }
100])
101
102/**
103 * Gets called after the last PUT request
104 */
105const videosAddResumableValidator = [
106 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
107 const user = res.locals.oauth.token.User
108
109 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
110 const file = { ...body, duration: undefined, path: getResumableUploadPath(body.id), filename: body.metadata.filename }
111
112 const cleanup = () => deleteFileAndCatch(file.path)
108 113
109 let duration: number 114 if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup()
110 115
111 try { 116 try {
112 duration = await getDurationFromVideoFile(videoFile.path) 117 if (!file.duration) await addDurationToVideo(file)
113 } catch (err) { 118 } catch (err) {
114 logger.error('Invalid input file in videosAddValidator.', { err }) 119 logger.error('Invalid input file in videosAddResumableValidator.', { err })
115 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) 120 res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
116 .json({ error: 'Video file unreadable.' }) 121 .json({ error: 'Video file unreadable.' })
117 122
118 return cleanUpReqFiles(req) 123 return cleanup()
119 } 124 }
120 125
121 videoFile.duration = duration 126 if (!await isVideoAccepted(req, res, file)) return cleanup()
122 127
123 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req) 128 res.locals.videoFileResumable = file
129
130 return next()
131 }
132]
133
134/**
135 * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use.
136 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts
137 *
138 * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
139 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
140 *
141 */
142const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
143 body('filename')
144 .isString()
145 .exists()
146 .withMessage('Should have a valid filename'),
147 body('name')
148 .trim()
149 .custom(isVideoNameValid)
150 .withMessage('Should have a valid name'),
151 body('channelId')
152 .customSanitizer(toIntOrNull)
153 .custom(isIdValid).withMessage('Should have correct video channel id'),
154
155 header('x-upload-content-length')
156 .isNumeric()
157 .exists()
158 .withMessage('Should specify the file length'),
159 header('x-upload-content-type')
160 .isString()
161 .exists()
162 .withMessage('Should specify the file mimetype'),
163
164 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
165 const videoFileMetadata = {
166 mimetype: req.headers['x-upload-content-type'] as string,
167 size: +req.headers['x-upload-content-length'],
168 originalname: req.body.name
169 }
170
171 const user = res.locals.oauth.token.User
172 const cleanup = () => cleanUpReqFiles(req)
173
174 logger.debug('Checking videosAddResumableInitValidator parameters and headers', {
175 parameters: req.body,
176 headers: req.headers,
177 files: req.files
178 })
179
180 if (areValidationErrors(req, res)) return cleanup()
181
182 const files = { videofile: [ videoFileMetadata ] }
183 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup()
184
185 // multer required unsetting the Content-Type, now we can set it for node-uploadx
186 req.headers['content-type'] = 'application/json; charset=utf-8'
187 // place previewfile in metadata so that uploadx saves it in .META
188 if (req.files['previewfile']) req.body.previewfile = req.files['previewfile']
124 189
125 return next() 190 return next()
126 } 191 }
@@ -478,7 +543,10 @@ const commonVideosFiltersValidator = [
478// --------------------------------------------------------------------------- 543// ---------------------------------------------------------------------------
479 544
480export { 545export {
481 videosAddValidator, 546 videosAddLegacyValidator,
547 videosAddResumableValidator,
548 videosAddResumableInitValidator,
549
482 videosUpdateValidator, 550 videosUpdateValidator,
483 videosGetValidator, 551 videosGetValidator,
484 videoFileMetadataGetValidator, 552 videoFileMetadataGetValidator,
@@ -515,7 +583,51 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
515 return false 583 return false
516} 584}
517 585
518async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) { 586async function commonVideoChecksPass (parameters: {
587 req: express.Request
588 res: express.Response
589 user: MUserAccountId
590 videoFileSize: number
591 files: express.UploadFilesForCheck
592}): Promise<boolean> {
593 const { req, res, user, videoFileSize, files } = parameters
594
595 if (areErrorsInScheduleUpdate(req, res)) return false
596
597 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
598
599 if (!isVideoFileMimeTypeValid(files)) {
600 res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415)
601 .json({
602 error: 'This file is not supported. Please, make sure it is of the following type: ' +
603 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
604 })
605
606 return false
607 }
608
609 if (!isVideoFileSizeValid(videoFileSize.toString())) {
610 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
611 .json({ error: 'This file is too large. It exceeds the maximum file size authorized.' })
612
613 return false
614 }
615
616 if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
617 res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413)
618 .json({ error: 'The user video quota is exceeded with this video.' })
619
620 return false
621 }
622
623 return true
624}
625
626export async function isVideoAccepted (
627 req: express.Request,
628 res: express.Response,
629 videoFile: express.VideoUploadFile
630) {
519 // Check we accept this video 631 // Check we accept this video
520 const acceptParameters = { 632 const acceptParameters = {
521 videoBody: req.body, 633 videoBody: req.body,
@@ -538,3 +650,11 @@ async function isVideoAccepted (req: express.Request, res: express.Response, vid
538 650
539 return true 651 return true
540} 652}
653
654async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
655 const duration: number = await getDurationFromVideoFile(videoFile.path)
656
657 if (isNaN(duration)) throw new Error(`Couldn't get video duration`)
658
659 videoFile.duration = duration
660}