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