Federated video streaming platform using ActivityPub and P2P in the web browser with Angular. https://joinpeertube.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

687 lines
23 KiB

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