]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/videos/videos.ts
We don't need to import mocha
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / videos / videos.ts
CommitLineData
41fb13c3 1import express from 'express'
f6d6e7f8 2import { body, header, param, query, ValidationChain } from 'express-validator'
276250f0 3import { isTestInstance } from '@server/helpers/core-utils'
f6d6e7f8 4import { getResumableUploadPath } from '@server/helpers/upload'
276250f0 5import { Redis } from '@server/lib/redis'
e6abf95e 6import { getServerActor } from '@server/models/application/application'
f8360396 7import { ExpressPromiseHandler } from '@server/types/express-handler'
71d4af1e 8import { MUserAccountId, MVideoFullLight } from '@server/types/models'
527a52ac 9import { getAllPrivacies } from '@shared/core-utils'
ff9d43f6 10import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude } from '@shared/models'
b60e5f38 11import {
37024082 12 exists,
2baea0c7
C
13 isBooleanValid,
14 isDateValid,
c729caf6 15 isFileValid,
2baea0c7 16 isIdValid,
1cd3facc 17 toArray,
c8861d5d 18 toBooleanOrNull,
2baea0c7
C
19 toIntOrNull,
20 toValueOrNull
6e46de09 21} from '../../../helpers/custom-validators/misc'
1fd61899 22import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
2baea0c7 23import {
c729caf6 24 areVideoTagsValid,
2baea0c7 25 isScheduleVideoUpdatePrivacyValid,
ac81d1a0
C
26 isVideoCategoryValid,
27 isVideoDescriptionValid,
f2eb23cd
RK
28 isVideoFileMimeTypeValid,
29 isVideoFileSizeValid,
1cd3facc 30 isVideoFilterValid,
c729caf6 31 isVideoImageValid,
2760b454 32 isVideoIncludeValid,
ac81d1a0
C
33 isVideoLanguageValid,
34 isVideoLicenceValid,
35 isVideoNameValid,
fd8710b8 36 isVideoOriginallyPublishedAtValid,
ac81d1a0 37 isVideoPrivacyValid,
c729caf6 38 isVideoSupportValid
6e46de09 39} from '../../../helpers/custom-validators/videos'
e6abf95e 40import { cleanUpReqFiles } from '../../../helpers/express-utils'
c729caf6 41import { getVideoStreamDuration } from '../../../helpers/ffmpeg'
6e46de09 42import { logger } from '../../../helpers/logger'
f6d6e7f8 43import { deleteFileAndCatch } from '../../../helpers/utils'
0283eaac 44import { getVideoWithAttributes } from '../../../helpers/video'
e6abf95e
C
45import { CONFIG } from '../../../initializers/config'
46import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
47import { isLocalVideoAccepted } from '../../../lib/moderation'
48import { Hooks } from '../../../lib/plugins/hooks'
e6abf95e 49import { VideoModel } from '../../../models/video/video'
10363c74
C
50import {
51 areValidationErrors,
ff9d43f6 52 checkCanSeeVideo,
10363c74 53 checkUserCanManageVideo,
c729caf6 54 checkUserQuota,
10363c74
C
55 doesVideoChannelOfAccountExist,
56 doesVideoExist,
d4a8e7a6
C
57 doesVideoFileOfVideoExist,
58 isValidVideoIdParam
10363c74 59} from '../shared'
34ca3b52 60
f6d6e7f8 61const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
0c237b19 62 body('videofile')
c729caf6 63 .custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null }))
f2eb23cd
RK
64 .withMessage('Should have a file'),
65 body('name')
0221f8c9 66 .trim()
7dab0bd6
RK
67 .custom(isVideoNameValid).withMessage(
68 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
69 ),
0f320037 70 body('channelId')
c8861d5d 71 .customSanitizer(toIntOrNull)
396f6f01 72 .custom(isIdValid),
b60e5f38 73
a2431b7d 74 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
cf7a61b5 75 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
a2431b7d 76
f6d6e7f8 77 const videoFile: express.VideoUploadFile = req.files['videofile'][0]
a2431b7d 78 const user = res.locals.oauth.token.User
b60e5f38 79
f6d6e7f8 80 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) {
f2eb23cd
RK
81 return cleanUpReqFiles(req)
82 }
83
f6d6e7f8 84 try {
85 if (!videoFile.duration) await addDurationToVideo(videoFile)
86 } catch (err) {
87 logger.error('Invalid input file in videosAddLegacyValidator.', { err })
f2eb23cd 88
76148b27
RK
89 res.fail({
90 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
91 message: 'Video file unreadable.'
92 })
f2eb23cd
RK
93 return cleanUpReqFiles(req)
94 }
95
f6d6e7f8 96 if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req)
a2431b7d 97
f6d6e7f8 98 return next()
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
f6d6e7f8 108 const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
020d3d3d 109 const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename }
f6d6e7f8 110 const cleanup = () => deleteFileAndCatch(file.path)
a2431b7d 111
276250f0
RK
112 const uploadId = req.query.upload_id
113 const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId)
114
115 if (sessionExists) {
116 const sessionResponse = await Redis.Instance.getUploadSession(uploadId)
117
118 if (!sessionResponse) {
119 res.setHeader('Retry-After', 300) // ask to retry after 5 min, knowing the upload_id is kept for up to 15 min after completion
120
121 return res.fail({
122 status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
123 message: 'The upload is already being processed'
124 })
125 }
126
127 if (isTestInstance()) {
128 res.setHeader('x-resumable-upload-cached', 'true')
129 }
130
131 return res.json(sessionResponse)
132 }
133
134 await Redis.Instance.setUploadSession(uploadId)
135
f6d6e7f8 136 if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup()
a2431b7d
C
137
138 try {
f6d6e7f8 139 if (!file.duration) await addDurationToVideo(file)
a2431b7d 140 } catch (err) {
f6d6e7f8 141 logger.error('Invalid input file in videosAddResumableValidator.', { err })
a2431b7d 142
76148b27
RK
143 res.fail({
144 status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
145 message: 'Video file unreadable.'
146 })
f6d6e7f8 147 return cleanup()
a2431b7d
C
148 }
149
f6d6e7f8 150 if (!await isVideoAccepted(req, res, file)) return cleanup()
b4055e1c 151
2e401e85 152 res.locals.videoFileResumable = { ...file, originalname: file.filename }
f6d6e7f8 153
154 return next()
155 }
156]
157
158/**
159 * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use.
160 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts
161 *
162 * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
163 * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
164 *
165 */
166const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
167 body('filename')
396f6f01 168 .exists(),
f6d6e7f8 169 body('name')
170 .trim()
7dab0bd6
RK
171 .custom(isVideoNameValid).withMessage(
172 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
173 ),
f6d6e7f8 174 body('channelId')
175 .customSanitizer(toIntOrNull)
396f6f01 176 .custom(isIdValid),
f6d6e7f8 177
178 header('x-upload-content-length')
179 .isNumeric()
180 .exists()
181 .withMessage('Should specify the file length'),
182 header('x-upload-content-type')
183 .isString()
184 .exists()
185 .withMessage('Should specify the file mimetype'),
186
187 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
188 const videoFileMetadata = {
189 mimetype: req.headers['x-upload-content-type'] as string,
190 size: +req.headers['x-upload-content-length'],
45353742 191 originalname: req.body.filename
f6d6e7f8 192 }
193
194 const user = res.locals.oauth.token.User
195 const cleanup = () => cleanUpReqFiles(req)
196
197 logger.debug('Checking videosAddResumableInitValidator parameters and headers', {
198 parameters: req.body,
199 headers: req.headers,
200 files: req.files
201 })
202
a85d5303 203 if (areValidationErrors(req, res, { omitLog: true })) return cleanup()
f6d6e7f8 204
205 const files = { videofile: [ videoFileMetadata ] }
206 if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup()
207
208 // multer required unsetting the Content-Type, now we can set it for node-uploadx
209 req.headers['content-type'] = 'application/json; charset=utf-8'
210 // place previewfile in metadata so that uploadx saves it in .META
45570e93 211 if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile']
a2431b7d
C
212
213 return next()
b60e5f38 214 }
a920fef1 215])
b60e5f38 216
418d092a 217const videosUpdateValidator = getCommonVideoEditAttributes().concat([
d4a8e7a6
C
218 isValidVideoIdParam('id'),
219
360329cc
C
220 body('name')
221 .optional()
0221f8c9 222 .trim()
7dab0bd6
RK
223 .custom(isVideoNameValid).withMessage(
224 `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
225 ),
0f320037
C
226 body('channelId')
227 .optional()
c8861d5d 228 .customSanitizer(toIntOrNull)
396f6f01 229 .custom(isIdValid),
b60e5f38 230
a2431b7d 231 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
cf7a61b5
C
232 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
233 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
0f6acda1 234 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
a2431b7d 235
6221f311 236 // Check if the user who did the request is able to update the video
0f320037 237 const user = res.locals.oauth.token.User
453e83ea 238 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
a2431b7d 239
0f6acda1 240 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
0f320037 241
a2431b7d 242 return next()
b60e5f38 243 }
a920fef1 244])
c173e565 245
8d427346 246async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
0283eaac 247 const video = getVideoWithAttributes(res)
8d427346
C
248
249 // Anybody can watch local videos
250 if (video.isOwned() === true) return next()
251
252 // Logged user
253 if (res.locals.oauth) {
254 // Users can search or watch remote videos
255 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
256 }
257
258 // Anybody can search or watch remote videos
259 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
260
261 // Check our instance follows an actor that shared this video
262 const serverActor = await getServerActor()
263 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
264
76148b27
RK
265 return res.fail({
266 status: HttpStatusCode.FORBIDDEN_403,
81628e50 267 message: 'Cannot get this video regarding follow constraints',
3866ea02 268 type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
76148b27
RK
269 data: {
270 originUrl: video.url
271 }
272 })
8d427346
C
273}
274
7eba5e1f 275const videosCustomGetValidator = (
71d4af1e 276 fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes',
7eba5e1f
C
277 authenticateInQuery = false
278) => {
96f29c0f 279 return [
d4a8e7a6 280 isValidVideoIdParam('id'),
7b1f49de 281
96f29c0f 282 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
96f29c0f 283 if (areValidationErrors(req, res)) return
0f6acda1 284 if (!await doesVideoExist(req.params.id, res, fetchType)) return
191764f3 285
943e5193
C
286 // Controllers does not need to check video rights
287 if (fetchType === 'only-immutable-attributes') return next()
288
71d4af1e 289 const video = getVideoWithAttributes(res) as MVideoFullLight
191764f3 290
ff9d43f6 291 if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id, authenticateInQuery })) return
8d427346 292
ff9d43f6 293 return next()
81ebea48 294 }
96f29c0f
C
295 ]
296}
297
298const videosGetValidator = videosCustomGetValidator('all')
eccf70f0 299const videosDownloadValidator = videosCustomGetValidator('all', true)
34ca3b52 300
8319d6ae 301const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
d4a8e7a6
C
302 isValidVideoIdParam('id'),
303
304 param('videoFileId')
305 .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
8319d6ae
RK
306
307 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
8319d6ae
RK
308 if (areValidationErrors(req, res)) return
309 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
310
311 return next()
312 }
313])
314
b60e5f38 315const videosRemoveValidator = [
d4a8e7a6 316 isValidVideoIdParam('id'),
34ca3b52 317
a2431b7d 318 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
a2431b7d 319 if (areValidationErrors(req, res)) return
0f6acda1 320 if (!await doesVideoExist(req.params.id, res)) return
a2431b7d
C
321
322 // Check if the user who did the request is able to delete the video
453e83ea 323 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
a2431b7d
C
324
325 return next()
b60e5f38
C
326 }
327]
34ca3b52 328
764a9657
C
329const videosOverviewValidator = [
330 query('page')
331 .optional()
396f6f01 332 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT }),
764a9657
C
333
334 (req: express.Request, res: express.Response, next: express.NextFunction) => {
335 if (areValidationErrors(req, res)) return
336
337 return next()
338 }
339]
340
418d092a 341function getCommonVideoEditAttributes () {
a920fef1
C
342 return [
343 body('thumbnailfile')
c729caf6 344 .custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage(
a1587156
C
345 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
346 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
347 ),
a920fef1 348 body('previewfile')
c729caf6 349 .custom((value, { req }) => isVideoImageValid(req.files, 'previewfile')).withMessage(
a1587156
C
350 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
351 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
352 ),
a920fef1
C
353
354 body('category')
355 .optional()
356 .customSanitizer(toIntOrNull)
396f6f01 357 .custom(isVideoCategoryValid),
a920fef1
C
358 body('licence')
359 .optional()
360 .customSanitizer(toIntOrNull)
396f6f01 361 .custom(isVideoLicenceValid),
a920fef1
C
362 body('language')
363 .optional()
364 .customSanitizer(toValueOrNull)
396f6f01 365 .custom(isVideoLanguageValid),
a920fef1
C
366 body('nsfw')
367 .optional()
c8861d5d 368 .customSanitizer(toBooleanOrNull)
396f6f01 369 .custom(isBooleanValid).withMessage('Should have a valid nsfw boolean'),
a920fef1
C
370 body('waitTranscoding')
371 .optional()
c8861d5d 372 .customSanitizer(toBooleanOrNull)
396f6f01 373 .custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'),
a920fef1
C
374 body('privacy')
375 .optional()
c8861d5d 376 .customSanitizer(toValueOrNull)
396f6f01 377 .custom(isVideoPrivacyValid),
a920fef1
C
378 body('description')
379 .optional()
380 .customSanitizer(toValueOrNull)
396f6f01 381 .custom(isVideoDescriptionValid),
a920fef1
C
382 body('support')
383 .optional()
384 .customSanitizer(toValueOrNull)
396f6f01 385 .custom(isVideoSupportValid),
a920fef1
C
386 body('tags')
387 .optional()
388 .customSanitizer(toValueOrNull)
c729caf6 389 .custom(areVideoTagsValid)
7dab0bd6
RK
390 .withMessage(
391 `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
392 `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
393 ),
a920fef1
C
394 body('commentsEnabled')
395 .optional()
c8861d5d 396 .customSanitizer(toBooleanOrNull)
396f6f01 397 .custom(isBooleanValid).withMessage('Should have commentsEnabled boolean'),
7f2cfe3a 398 body('downloadEnabled')
1e74f19a 399 .optional()
c8861d5d 400 .customSanitizer(toBooleanOrNull)
396f6f01 401 .custom(isBooleanValid).withMessage('Should have downloadEnabled boolean'),
b718fd22 402 body('originallyPublishedAt')
c8861d5d
C
403 .optional()
404 .customSanitizer(toValueOrNull)
396f6f01 405 .custom(isVideoOriginallyPublishedAtValid),
a920fef1
C
406 body('scheduleUpdate')
407 .optional()
408 .customSanitizer(toValueOrNull),
409 body('scheduleUpdate.updateAt')
410 .optional()
70330f63 411 .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
a920fef1
C
412 body('scheduleUpdate.privacy')
413 .optional()
2b65c4e5 414 .customSanitizer(toIntOrNull)
396f6f01 415 .custom(isScheduleVideoUpdatePrivacyValid)
ba5a8d89 416 ] as (ValidationChain | ExpressPromiseHandler)[]
a920fef1 417}
fbad87b0 418
1cd3facc
C
419const commonVideosFiltersValidator = [
420 query('categoryOneOf')
421 .optional()
422 .customSanitizer(toArray)
396f6f01 423 .custom(isNumberArray).withMessage('Should have a valid categoryOneOf array'),
1cd3facc
C
424 query('licenceOneOf')
425 .optional()
426 .customSanitizer(toArray)
396f6f01 427 .custom(isNumberArray).withMessage('Should have a valid licenceOneOf array'),
1cd3facc
C
428 query('languageOneOf')
429 .optional()
430 .customSanitizer(toArray)
396f6f01 431 .custom(isStringArray).withMessage('Should have a valid languageOneOf array'),
527a52ac
C
432 query('privacyOneOf')
433 .optional()
434 .customSanitizer(toArray)
396f6f01 435 .custom(isNumberArray).withMessage('Should have a valid privacyOneOf array'),
1cd3facc
C
436 query('tagsOneOf')
437 .optional()
438 .customSanitizer(toArray)
396f6f01 439 .custom(isStringArray).withMessage('Should have a valid tagsOneOf array'),
1cd3facc
C
440 query('tagsAllOf')
441 .optional()
442 .customSanitizer(toArray)
396f6f01 443 .custom(isStringArray).withMessage('Should have a valid tagsAllOf array'),
1cd3facc
C
444 query('nsfw')
445 .optional()
396f6f01 446 .custom(isBooleanBothQueryValid),
1fd61899
C
447 query('isLive')
448 .optional()
449 .customSanitizer(toBooleanOrNull)
396f6f01 450 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
1cd3facc
C
451 query('filter')
452 .optional()
396f6f01 453 .custom(isVideoFilterValid),
2760b454
C
454 query('include')
455 .optional()
396f6f01 456 .custom(isVideoIncludeValid),
2760b454
C
457 query('isLocal')
458 .optional()
459 .customSanitizer(toBooleanOrNull)
396f6f01 460 .custom(isBooleanValid).withMessage('Should have a valid isLocal boolean'),
d324756e
C
461 query('hasHLSFiles')
462 .optional()
463 .customSanitizer(toBooleanOrNull)
396f6f01 464 .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'),
d324756e
C
465 query('hasWebtorrentFiles')
466 .optional()
467 .customSanitizer(toBooleanOrNull)
396f6f01 468 .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'),
fe987656
C
469 query('skipCount')
470 .optional()
471 .customSanitizer(toBooleanOrNull)
396f6f01 472 .custom(isBooleanValid).withMessage('Should have a valid skipCount boolean'),
37024082
RK
473 query('search')
474 .optional()
396f6f01 475 .custom(exists),
1cd3facc
C
476
477 (req: express.Request, res: express.Response, next: express.NextFunction) => {
1cd3facc
C
478 if (areValidationErrors(req, res)) return
479
2760b454
C
480 // FIXME: deprecated in 4.0, to remove
481 {
482 if (req.query.filter === 'all-local') {
527a52ac 483 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
2760b454 484 req.query.isLocal = true
527a52ac 485 req.query.privacyOneOf = getAllPrivacies()
2760b454 486 } else if (req.query.filter === 'all') {
527a52ac
C
487 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
488 req.query.privacyOneOf = getAllPrivacies()
2760b454
C
489 } else if (req.query.filter === 'local') {
490 req.query.isLocal = true
491 }
492
493 req.query.filter = undefined
494 }
495
496 const user = res.locals.oauth?.token.User
497
d324756e 498 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
527a52ac 499 if (req.query.include || req.query.privacyOneOf) {
d324756e
C
500 return res.fail({
501 status: HttpStatusCode.UNAUTHORIZED_401,
502 message: 'You are not allowed to see all videos.'
503 })
504 }
1cd3facc
C
505 }
506
507 return next()
508 }
509]
510
fbad87b0
C
511// ---------------------------------------------------------------------------
512
513export {
f6d6e7f8 514 videosAddLegacyValidator,
515 videosAddResumableValidator,
516 videosAddResumableInitValidator,
517
fbad87b0
C
518 videosUpdateValidator,
519 videosGetValidator,
8319d6ae 520 videoFileMetadataGetValidator,
eccf70f0 521 videosDownloadValidator,
8d427346 522 checkVideoFollowConstraints,
96f29c0f 523 videosCustomGetValidator,
fbad87b0 524 videosRemoveValidator,
fbad87b0 525
418d092a 526 getCommonVideoEditAttributes,
1cd3facc 527
764a9657
C
528 commonVideosFiltersValidator,
529
530 videosOverviewValidator
fbad87b0
C
531}
532
533// ---------------------------------------------------------------------------
534
535function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
536 if (req.body.scheduleUpdate) {
537 if (!req.body.scheduleUpdate.updateAt) {
7373507f
C
538 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
539
76148b27 540 res.fail({ message: 'Schedule update at is mandatory.' })
fbad87b0
C
541 return true
542 }
543 }
544
545 return false
546}
b4055e1c 547
f6d6e7f8 548async function commonVideoChecksPass (parameters: {
549 req: express.Request
550 res: express.Response
551 user: MUserAccountId
552 videoFileSize: number
553 files: express.UploadFilesForCheck
554}): Promise<boolean> {
555 const { req, res, user, videoFileSize, files } = parameters
556
557 if (areErrorsInScheduleUpdate(req, res)) return false
558
559 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
560
561 if (!isVideoFileMimeTypeValid(files)) {
76148b27
RK
562 res.fail({
563 status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
564 message: 'This file is not supported. Please, make sure it is of the following type: ' +
565 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
566 })
f6d6e7f8 567 return false
568 }
569
570 if (!isVideoFileSizeValid(videoFileSize.toString())) {
76148b27
RK
571 res.fail({
572 status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
c756bae0
RK
573 message: 'This file is too large. It exceeds the maximum file size authorized.',
574 type: ServerErrorCode.MAX_FILE_SIZE_REACHED
76148b27 575 })
f6d6e7f8 576 return false
577 }
578
c729caf6 579 if (await checkUserQuota(user, videoFileSize, res) === false) return false
f6d6e7f8 580
581 return true
582}
583
584export async function isVideoAccepted (
585 req: express.Request,
586 res: express.Response,
587 videoFile: express.VideoUploadFile
588) {
b4055e1c
C
589 // Check we accept this video
590 const acceptParameters = {
591 videoBody: req.body,
592 videoFile,
593 user: res.locals.oauth.token.User
594 }
89cd1275
C
595 const acceptedResult = await Hooks.wrapFun(
596 isLocalVideoAccepted,
597 acceptParameters,
b4055e1c
C
598 'filter:api.video.upload.accept.result'
599 )
600
601 if (!acceptedResult || acceptedResult.accepted !== true) {
602 logger.info('Refused local video.', { acceptedResult, acceptParameters })
76148b27
RK
603 res.fail({
604 status: HttpStatusCode.FORBIDDEN_403,
605 message: acceptedResult.errorMessage || 'Refused local video'
606 })
b4055e1c
C
607 return false
608 }
609
610 return true
611}
f6d6e7f8 612
613async function addDurationToVideo (videoFile: { path: string, duration?: number }) {
f692fc8d 614 const duration = await getVideoStreamDuration(videoFile.path)
f6d6e7f8 615
f692fc8d
C
616 // FFmpeg may not be able to guess video duration
617 // For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2
618 if (isNaN(duration)) videoFile.duration = 0
619 else videoFile.duration = duration
f6d6e7f8 620}