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