diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-19 16:02:49 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-07-21 17:38:13 +0200 |
commit | 12dc3a942a13c7f1489822dae052da197ef15905 (patch) | |
tree | 7b87b6be692af0b62ebac17e720c80244fd8a7ec /server/middlewares/validators/videos | |
parent | c6867725fb8e3dfbc2018a37ed5a963103587cb6 (diff) | |
download | PeerTube-12dc3a942a13c7f1489822dae052da197ef15905.tar.gz PeerTube-12dc3a942a13c7f1489822dae052da197ef15905.tar.zst PeerTube-12dc3a942a13c7f1489822dae052da197ef15905.zip |
Implement replace file in server side
Diffstat (limited to 'server/middlewares/validators/videos')
-rw-r--r-- | server/middlewares/validators/videos/index.ts | 6 | ||||
-rw-r--r-- | server/middlewares/validators/videos/shared/index.ts | 2 | ||||
-rw-r--r-- | server/middlewares/validators/videos/shared/upload.ts | 39 | ||||
-rw-r--r-- | server/middlewares/validators/videos/shared/video-validators.ts | 104 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-source.ts | 108 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-studio.ts | 12 | ||||
-rw-r--r-- | server/middlewares/validators/videos/videos.ts | 112 |
7 files changed, 267 insertions, 116 deletions
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index 0c824c314..8c6fc49b1 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | export * from './video-blacklist' | 1 | export * from './video-blacklist' |
2 | export * from './video-captions' | 2 | export * from './video-captions' |
3 | export * from './video-channel-sync' | ||
3 | export * from './video-channels' | 4 | export * from './video-channels' |
4 | export * from './video-comments' | 5 | export * from './video-comments' |
5 | export * from './video-files' | 6 | export * from './video-files' |
6 | export * from './video-imports' | 7 | export * from './video-imports' |
7 | export * from './video-live' | 8 | export * from './video-live' |
8 | export * from './video-ownership-changes' | 9 | export * from './video-ownership-changes' |
9 | export * from './video-view' | 10 | export * from './video-passwords' |
10 | export * from './video-rates' | 11 | export * from './video-rates' |
11 | export * from './video-shares' | 12 | export * from './video-shares' |
12 | export * from './video-source' | 13 | export * from './video-source' |
@@ -14,6 +15,5 @@ export * from './video-stats' | |||
14 | export * from './video-studio' | 15 | export * from './video-studio' |
15 | export * from './video-token' | 16 | export * from './video-token' |
16 | export * from './video-transcoding' | 17 | export * from './video-transcoding' |
18 | export * from './video-view' | ||
17 | export * from './videos' | 19 | export * from './videos' |
18 | export * from './video-channel-sync' | ||
19 | export * from './video-passwords' | ||
diff --git a/server/middlewares/validators/videos/shared/index.ts b/server/middlewares/validators/videos/shared/index.ts new file mode 100644 index 000000000..eb11dcc6a --- /dev/null +++ b/server/middlewares/validators/videos/shared/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './upload' | ||
2 | export * from './video-validators' | ||
diff --git a/server/middlewares/validators/videos/shared/upload.ts b/server/middlewares/validators/videos/shared/upload.ts new file mode 100644 index 000000000..ea0dddc3c --- /dev/null +++ b/server/middlewares/validators/videos/shared/upload.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import express from 'express' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { getVideoStreamDuration } from '@shared/ffmpeg' | ||
4 | import { HttpStatusCode } from '@shared/models' | ||
5 | |||
6 | export async function addDurationToVideoFileIfNeeded (options: { | ||
7 | res: express.Response | ||
8 | videoFile: { path: string, duration?: number } | ||
9 | middlewareName: string | ||
10 | }) { | ||
11 | const { res, middlewareName, videoFile } = options | ||
12 | |||
13 | try { | ||
14 | if (!videoFile.duration) await addDurationToVideo(videoFile) | ||
15 | } catch (err) { | ||
16 | logger.error('Invalid input file in ' + middlewareName, { err }) | ||
17 | |||
18 | res.fail({ | ||
19 | status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, | ||
20 | message: 'Video file unreadable.' | ||
21 | }) | ||
22 | return false | ||
23 | } | ||
24 | |||
25 | return true | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | // Private | ||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
32 | async function addDurationToVideo (videoFile: { path: string, duration?: number }) { | ||
33 | const duration = await getVideoStreamDuration(videoFile.path) | ||
34 | |||
35 | // FFmpeg may not be able to guess video duration | ||
36 | // For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2 | ||
37 | if (isNaN(duration)) videoFile.duration = 0 | ||
38 | else videoFile.duration = duration | ||
39 | } | ||
diff --git a/server/middlewares/validators/videos/shared/video-validators.ts b/server/middlewares/validators/videos/shared/video-validators.ts new file mode 100644 index 000000000..72536011d --- /dev/null +++ b/server/middlewares/validators/videos/shared/video-validators.ts | |||
@@ -0,0 +1,104 @@ | |||
1 | import express from 'express' | ||
2 | import { isVideoFileMimeTypeValid, isVideoFileSizeValid } from '@server/helpers/custom-validators/videos' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' | ||
5 | import { isLocalVideoFileAccepted } from '@server/lib/moderation' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { MUserAccountId, MVideo } from '@server/types/models' | ||
8 | import { HttpStatusCode, ServerErrorCode, ServerFilterHookName, VideoState } from '@shared/models' | ||
9 | import { checkUserQuota } from '../../shared' | ||
10 | |||
11 | export async function commonVideoFileChecks (options: { | ||
12 | res: express.Response | ||
13 | user: MUserAccountId | ||
14 | videoFileSize: number | ||
15 | files: express.UploadFilesForCheck | ||
16 | }): Promise<boolean> { | ||
17 | const { res, user, videoFileSize, files } = options | ||
18 | |||
19 | if (!isVideoFileMimeTypeValid(files)) { | ||
20 | res.fail({ | ||
21 | status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415, | ||
22 | message: 'This file is not supported. Please, make sure it is of the following type: ' + | ||
23 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | ||
24 | }) | ||
25 | return false | ||
26 | } | ||
27 | |||
28 | if (!isVideoFileSizeValid(videoFileSize.toString())) { | ||
29 | res.fail({ | ||
30 | status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, | ||
31 | message: 'This file is too large. It exceeds the maximum file size authorized.', | ||
32 | type: ServerErrorCode.MAX_FILE_SIZE_REACHED | ||
33 | }) | ||
34 | return false | ||
35 | } | ||
36 | |||
37 | if (await checkUserQuota(user, videoFileSize, res) === false) return false | ||
38 | |||
39 | return true | ||
40 | } | ||
41 | |||
42 | export async function isVideoFileAccepted (options: { | ||
43 | req: express.Request | ||
44 | res: express.Response | ||
45 | videoFile: express.VideoUploadFile | ||
46 | hook: Extract<ServerFilterHookName, 'filter:api.video.upload.accept.result' | 'filter:api.video.update-file.accept.result'> | ||
47 | }) { | ||
48 | const { req, res, videoFile } = options | ||
49 | |||
50 | // Check we accept this video | ||
51 | const acceptParameters = { | ||
52 | videoBody: req.body, | ||
53 | videoFile, | ||
54 | user: res.locals.oauth.token.User | ||
55 | } | ||
56 | const acceptedResult = await Hooks.wrapFun( | ||
57 | isLocalVideoFileAccepted, | ||
58 | acceptParameters, | ||
59 | 'filter:api.video.upload.accept.result' | ||
60 | ) | ||
61 | |||
62 | if (!acceptedResult || acceptedResult.accepted !== true) { | ||
63 | logger.info('Refused local video file.', { acceptedResult, acceptParameters }) | ||
64 | res.fail({ | ||
65 | status: HttpStatusCode.FORBIDDEN_403, | ||
66 | message: acceptedResult.errorMessage || 'Refused local video file' | ||
67 | }) | ||
68 | return false | ||
69 | } | ||
70 | |||
71 | return true | ||
72 | } | ||
73 | |||
74 | export function checkVideoFileCanBeEdited (video: MVideo, res: express.Response) { | ||
75 | if (video.isLive) { | ||
76 | res.fail({ | ||
77 | status: HttpStatusCode.BAD_REQUEST_400, | ||
78 | message: 'Cannot edit a live video' | ||
79 | }) | ||
80 | |||
81 | return false | ||
82 | } | ||
83 | |||
84 | if (video.state === VideoState.TO_TRANSCODE || video.state === VideoState.TO_EDIT) { | ||
85 | res.fail({ | ||
86 | status: HttpStatusCode.CONFLICT_409, | ||
87 | message: 'Cannot edit video that is already waiting for transcoding/edition' | ||
88 | }) | ||
89 | |||
90 | return false | ||
91 | } | ||
92 | |||
93 | const validStates = new Set([ VideoState.PUBLISHED, VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED, VideoState.TRANSCODING_FAILED ]) | ||
94 | if (!validStates.has(video.state)) { | ||
95 | res.fail({ | ||
96 | status: HttpStatusCode.BAD_REQUEST_400, | ||
97 | message: 'Video state is not compatible with edition' | ||
98 | }) | ||
99 | |||
100 | return false | ||
101 | } | ||
102 | |||
103 | return true | ||
104 | } | ||
diff --git a/server/middlewares/validators/videos/video-source.ts b/server/middlewares/validators/videos/video-source.ts index c6d8f1a81..bbccb58b0 100644 --- a/server/middlewares/validators/videos/video-source.ts +++ b/server/middlewares/validators/videos/video-source.ts | |||
@@ -1,20 +1,31 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body, header } from 'express-validator' | ||
3 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
2 | import { getVideoWithAttributes } from '@server/helpers/video' | 4 | import { getVideoWithAttributes } from '@server/helpers/video' |
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | import { uploadx } from '@server/lib/uploadx' | ||
3 | import { VideoSourceModel } from '@server/models/video/video-source' | 7 | import { VideoSourceModel } from '@server/models/video/video-source' |
4 | import { MVideoFullLight } from '@server/types/models' | 8 | import { MVideoFullLight } from '@server/types/models' |
5 | import { HttpStatusCode, UserRight } from '@shared/models' | 9 | import { HttpStatusCode, UserRight } from '@shared/models' |
10 | import { Metadata as UploadXMetadata } from '@uploadx/core' | ||
11 | import { logger } from '../../../helpers/logger' | ||
6 | import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared' | 12 | import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared' |
13 | import { addDurationToVideoFileIfNeeded, checkVideoFileCanBeEdited, commonVideoFileChecks, isVideoFileAccepted } from './shared' | ||
7 | 14 | ||
8 | const videoSourceGetValidator = [ | 15 | export const videoSourceGetLatestValidator = [ |
9 | isValidVideoIdParam('id'), | 16 | isValidVideoIdParam('id'), |
10 | 17 | ||
11 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 18 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
12 | if (areValidationErrors(req, res)) return | 19 | if (areValidationErrors(req, res)) return |
13 | if (!await doesVideoExist(req.params.id, res, 'for-api')) return | 20 | if (!await doesVideoExist(req.params.id, res, 'all')) return |
14 | 21 | ||
15 | const video = getVideoWithAttributes(res) as MVideoFullLight | 22 | const video = getVideoWithAttributes(res) as MVideoFullLight |
16 | 23 | ||
17 | res.locals.videoSource = await VideoSourceModel.loadByVideoId(video.id) | 24 | const user = res.locals.oauth.token.User |
25 | if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return | ||
26 | |||
27 | res.locals.videoSource = await VideoSourceModel.loadLatest(video.id) | ||
28 | |||
18 | if (!res.locals.videoSource) { | 29 | if (!res.locals.videoSource) { |
19 | return res.fail({ | 30 | return res.fail({ |
20 | status: HttpStatusCode.NOT_FOUND_404, | 31 | status: HttpStatusCode.NOT_FOUND_404, |
@@ -22,13 +33,98 @@ const videoSourceGetValidator = [ | |||
22 | }) | 33 | }) |
23 | } | 34 | } |
24 | 35 | ||
36 | return next() | ||
37 | } | ||
38 | ] | ||
39 | |||
40 | export const replaceVideoSourceResumableValidator = [ | ||
41 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
42 | const body: express.CustomUploadXFile<UploadXMetadata> = req.body | ||
43 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } | ||
44 | const cleanup = () => uploadx.storage.delete(file).catch(err => logger.error('Cannot delete the file %s', file.name, { err })) | ||
45 | |||
46 | if (!await checkCanUpdateVideoFile({ req, res })) { | ||
47 | return cleanup() | ||
48 | } | ||
49 | |||
50 | if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'updateVideoFileResumableValidator' })) { | ||
51 | return cleanup() | ||
52 | } | ||
53 | |||
54 | if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.update-file.accept.result' })) { | ||
55 | return cleanup() | ||
56 | } | ||
57 | |||
58 | res.locals.updateVideoFileResumable = { ...file, originalname: file.filename } | ||
59 | |||
60 | return next() | ||
61 | } | ||
62 | ] | ||
63 | |||
64 | export const replaceVideoSourceResumableInitValidator = [ | ||
65 | body('filename') | ||
66 | .exists(), | ||
67 | |||
68 | header('x-upload-content-length') | ||
69 | .isNumeric() | ||
70 | .exists() | ||
71 | .withMessage('Should specify the file length'), | ||
72 | header('x-upload-content-type') | ||
73 | .isString() | ||
74 | .exists() | ||
75 | .withMessage('Should specify the file mimetype'), | ||
76 | |||
77 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
25 | const user = res.locals.oauth.token.User | 78 | const user = res.locals.oauth.token.User |
26 | if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return | 79 | |
80 | logger.debug('Checking updateVideoFileResumableInitValidator parameters and headers', { | ||
81 | parameters: req.body, | ||
82 | headers: req.headers | ||
83 | }) | ||
84 | |||
85 | if (areValidationErrors(req, res, { omitLog: true })) return | ||
86 | |||
87 | if (!await checkCanUpdateVideoFile({ req, res })) return | ||
88 | |||
89 | const videoFileMetadata = { | ||
90 | mimetype: req.headers['x-upload-content-type'] as string, | ||
91 | size: +req.headers['x-upload-content-length'], | ||
92 | originalname: req.body.filename | ||
93 | } | ||
94 | |||
95 | const files = { videofile: [ videoFileMetadata ] } | ||
96 | if (await commonVideoFileChecks({ res, user, videoFileSize: videoFileMetadata.size, files }) === false) return | ||
27 | 97 | ||
28 | return next() | 98 | return next() |
29 | } | 99 | } |
30 | ] | 100 | ] |
31 | 101 | ||
32 | export { | 102 | // --------------------------------------------------------------------------- |
33 | videoSourceGetValidator | 103 | // Private |
104 | // --------------------------------------------------------------------------- | ||
105 | |||
106 | async function checkCanUpdateVideoFile (options: { | ||
107 | req: express.Request | ||
108 | res: express.Response | ||
109 | }) { | ||
110 | const { req, res } = options | ||
111 | |||
112 | if (!CONFIG.VIDEO_FILE.UPDATE.ENABLED) { | ||
113 | res.fail({ | ||
114 | status: HttpStatusCode.FORBIDDEN_403, | ||
115 | message: 'Updating the file of an existing video is not allowed on this instance' | ||
116 | }) | ||
117 | return false | ||
118 | } | ||
119 | |||
120 | if (!await doesVideoExist(req.params.id, res)) return false | ||
121 | |||
122 | const user = res.locals.oauth.token.User | ||
123 | const video = res.locals.videoAll | ||
124 | |||
125 | if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return false | ||
126 | |||
127 | if (!checkVideoFileCanBeEdited(video, res)) return false | ||
128 | |||
129 | return true | ||
34 | } | 130 | } |
diff --git a/server/middlewares/validators/videos/video-studio.ts b/server/middlewares/validators/videos/video-studio.ts index 7a68f88e5..a375af60a 100644 --- a/server/middlewares/validators/videos/video-studio.ts +++ b/server/middlewares/validators/videos/video-studio.ts | |||
@@ -11,8 +11,9 @@ import { cleanUpReqFiles } from '@server/helpers/express-utils' | |||
11 | import { CONFIG } from '@server/initializers/config' | 11 | import { CONFIG } from '@server/initializers/config' |
12 | import { approximateIntroOutroAdditionalSize, getTaskFileFromReq } from '@server/lib/video-studio' | 12 | import { approximateIntroOutroAdditionalSize, getTaskFileFromReq } from '@server/lib/video-studio' |
13 | import { isAudioFile } from '@shared/ffmpeg' | 13 | import { isAudioFile } from '@shared/ffmpeg' |
14 | import { HttpStatusCode, UserRight, VideoState, VideoStudioCreateEdition, VideoStudioTask } from '@shared/models' | 14 | import { HttpStatusCode, UserRight, VideoStudioCreateEdition, VideoStudioTask } from '@shared/models' |
15 | import { areValidationErrors, checkUserCanManageVideo, checkUserQuota, doesVideoExist } from '../shared' | 15 | import { areValidationErrors, checkUserCanManageVideo, checkUserQuota, doesVideoExist } from '../shared' |
16 | import { checkVideoFileCanBeEdited } from './shared' | ||
16 | 17 | ||
17 | const videoStudioAddEditionValidator = [ | 18 | const videoStudioAddEditionValidator = [ |
18 | param('videoId') | 19 | param('videoId') |
@@ -66,14 +67,7 @@ const videoStudioAddEditionValidator = [ | |||
66 | if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req) | 67 | if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req) |
67 | 68 | ||
68 | const video = res.locals.videoAll | 69 | const video = res.locals.videoAll |
69 | if (video.state === VideoState.TO_TRANSCODE || video.state === VideoState.TO_EDIT) { | 70 | if (!checkVideoFileCanBeEdited(video, res)) return cleanUpReqFiles(req) |
70 | res.fail({ | ||
71 | status: HttpStatusCode.CONFLICT_409, | ||
72 | message: 'Cannot edit video that is already waiting for transcoding/edition' | ||
73 | }) | ||
74 | |||
75 | return cleanUpReqFiles(req) | ||
76 | } | ||
77 | 71 | ||
78 | const user = res.locals.oauth.token.User | 72 | const user = res.locals.oauth.token.User |
79 | if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) | 73 | if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index b39d13a23..aea3453b5 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -2,13 +2,12 @@ import express from 'express' | |||
2 | import { body, header, param, query, ValidationChain } from 'express-validator' | 2 | import { body, header, param, query, ValidationChain } from 'express-validator' |
3 | import { isTestInstance } from '@server/helpers/core-utils' | 3 | import { isTestInstance } from '@server/helpers/core-utils' |
4 | import { getResumableUploadPath } from '@server/helpers/upload' | 4 | import { getResumableUploadPath } from '@server/helpers/upload' |
5 | import { uploadx } from '@server/lib/uploadx' | ||
6 | import { Redis } from '@server/lib/redis' | 5 | import { Redis } from '@server/lib/redis' |
6 | import { uploadx } from '@server/lib/uploadx' | ||
7 | import { getServerActor } from '@server/models/application/application' | 7 | import { getServerActor } from '@server/models/application/application' |
8 | import { ExpressPromiseHandler } from '@server/types/express-handler' | 8 | import { ExpressPromiseHandler } from '@server/types/express-handler' |
9 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' | 9 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' |
10 | import { arrayify, getAllPrivacies } from '@shared/core-utils' | 10 | import { arrayify, getAllPrivacies } from '@shared/core-utils' |
11 | import { getVideoStreamDuration } from '@shared/ffmpeg' | ||
12 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoState } from '@shared/models' | 11 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoState } from '@shared/models' |
13 | import { | 12 | import { |
14 | exists, | 13 | exists, |
@@ -27,8 +26,6 @@ import { | |||
27 | isValidPasswordProtectedPrivacy, | 26 | isValidPasswordProtectedPrivacy, |
28 | isVideoCategoryValid, | 27 | isVideoCategoryValid, |
29 | isVideoDescriptionValid, | 28 | isVideoDescriptionValid, |
30 | isVideoFileMimeTypeValid, | ||
31 | isVideoFileSizeValid, | ||
32 | isVideoFilterValid, | 29 | isVideoFilterValid, |
33 | isVideoImageValid, | 30 | isVideoImageValid, |
34 | isVideoIncludeValid, | 31 | isVideoIncludeValid, |
@@ -44,21 +41,19 @@ import { logger } from '../../../helpers/logger' | |||
44 | import { getVideoWithAttributes } from '../../../helpers/video' | 41 | import { getVideoWithAttributes } from '../../../helpers/video' |
45 | import { CONFIG } from '../../../initializers/config' | 42 | import { CONFIG } from '../../../initializers/config' |
46 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | 43 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' |
47 | import { isLocalVideoAccepted } from '../../../lib/moderation' | ||
48 | import { Hooks } from '../../../lib/plugins/hooks' | ||
49 | import { VideoModel } from '../../../models/video/video' | 44 | import { VideoModel } from '../../../models/video/video' |
50 | import { | 45 | import { |
51 | areValidationErrors, | 46 | areValidationErrors, |
52 | checkCanAccessVideoStaticFiles, | 47 | checkCanAccessVideoStaticFiles, |
53 | checkCanSeeVideo, | 48 | checkCanSeeVideo, |
54 | checkUserCanManageVideo, | 49 | checkUserCanManageVideo, |
55 | checkUserQuota, | ||
56 | doesVideoChannelOfAccountExist, | 50 | doesVideoChannelOfAccountExist, |
57 | doesVideoExist, | 51 | doesVideoExist, |
58 | doesVideoFileOfVideoExist, | 52 | doesVideoFileOfVideoExist, |
59 | isValidVideoIdParam, | 53 | isValidVideoIdParam, |
60 | isValidVideoPasswordHeader | 54 | isValidVideoPasswordHeader |
61 | } from '../shared' | 55 | } from '../shared' |
56 | import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared' | ||
62 | 57 | ||
63 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | 58 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ |
64 | body('videofile') | 59 | body('videofile') |
@@ -83,26 +78,15 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
83 | const videoFile: express.VideoUploadFile = req.files['videofile'][0] | 78 | const videoFile: express.VideoUploadFile = req.files['videofile'][0] |
84 | const user = res.locals.oauth.token.User | 79 | const user = res.locals.oauth.token.User |
85 | 80 | ||
86 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) { | 81 | if ( |
82 | !await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files }) || | ||
83 | !isValidPasswordProtectedPrivacy(req, res) || | ||
84 | !await addDurationToVideoFileIfNeeded({ videoFile, res, middlewareName: 'videosAddvideosAddLegacyValidatorResumableValidator' }) || | ||
85 | !await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' }) | ||
86 | ) { | ||
87 | return cleanUpReqFiles(req) | 87 | return cleanUpReqFiles(req) |
88 | } | 88 | } |
89 | 89 | ||
90 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
91 | |||
92 | try { | ||
93 | if (!videoFile.duration) await addDurationToVideo(videoFile) | ||
94 | } catch (err) { | ||
95 | logger.error('Invalid input file in videosAddLegacyValidator.', { err }) | ||
96 | |||
97 | res.fail({ | ||
98 | status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, | ||
99 | message: 'Video file unreadable.' | ||
100 | }) | ||
101 | return cleanUpReqFiles(req) | ||
102 | } | ||
103 | |||
104 | if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req) | ||
105 | |||
106 | return next() | 90 | return next() |
107 | } | 91 | } |
108 | ]) | 92 | ]) |
@@ -146,22 +130,10 @@ const videosAddResumableValidator = [ | |||
146 | await Redis.Instance.setUploadSession(uploadId) | 130 | await Redis.Instance.setUploadSession(uploadId) |
147 | 131 | ||
148 | if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup() | 132 | if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup() |
133 | if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'videosAddResumableValidator' })) return cleanup() | ||
134 | if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.upload.accept.result' })) return cleanup() | ||
149 | 135 | ||
150 | try { | 136 | res.locals.uploadVideoFileResumable = { ...file, originalname: file.filename } |
151 | if (!file.duration) await addDurationToVideo(file) | ||
152 | } catch (err) { | ||
153 | logger.error('Invalid input file in videosAddResumableValidator.', { err }) | ||
154 | |||
155 | res.fail({ | ||
156 | status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, | ||
157 | message: 'Video file unreadable.' | ||
158 | }) | ||
159 | return cleanup() | ||
160 | } | ||
161 | |||
162 | if (!await isVideoAccepted(req, res, file)) return cleanup() | ||
163 | |||
164 | res.locals.videoFileResumable = { ...file, originalname: file.filename } | ||
165 | 137 | ||
166 | return next() | 138 | return next() |
167 | } | 139 | } |
@@ -604,76 +576,20 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) | |||
604 | return false | 576 | return false |
605 | } | 577 | } |
606 | 578 | ||
607 | async function commonVideoChecksPass (parameters: { | 579 | async function commonVideoChecksPass (options: { |
608 | req: express.Request | 580 | req: express.Request |
609 | res: express.Response | 581 | res: express.Response |
610 | user: MUserAccountId | 582 | user: MUserAccountId |
611 | videoFileSize: number | 583 | videoFileSize: number |
612 | files: express.UploadFilesForCheck | 584 | files: express.UploadFilesForCheck |
613 | }): Promise<boolean> { | 585 | }): Promise<boolean> { |
614 | const { req, res, user, videoFileSize, files } = parameters | 586 | const { req, res, user } = options |
615 | 587 | ||
616 | if (areErrorsInScheduleUpdate(req, res)) return false | 588 | if (areErrorsInScheduleUpdate(req, res)) return false |
617 | 589 | ||
618 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false | 590 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false |
619 | 591 | ||
620 | if (!isVideoFileMimeTypeValid(files)) { | 592 | if (!await commonVideoFileChecks(options)) return false |
621 | res.fail({ | ||
622 | status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415, | ||
623 | message: 'This file is not supported. Please, make sure it is of the following type: ' + | ||
624 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | ||
625 | }) | ||
626 | return false | ||
627 | } | ||
628 | |||
629 | if (!isVideoFileSizeValid(videoFileSize.toString())) { | ||
630 | res.fail({ | ||
631 | status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, | ||
632 | message: 'This file is too large. It exceeds the maximum file size authorized.', | ||
633 | type: ServerErrorCode.MAX_FILE_SIZE_REACHED | ||
634 | }) | ||
635 | return false | ||
636 | } | ||
637 | |||
638 | if (await checkUserQuota(user, videoFileSize, res) === false) return false | ||
639 | |||
640 | return true | ||
641 | } | ||
642 | |||
643 | export async function isVideoAccepted ( | ||
644 | req: express.Request, | ||
645 | res: express.Response, | ||
646 | videoFile: express.VideoUploadFile | ||
647 | ) { | ||
648 | // Check we accept this video | ||
649 | const acceptParameters = { | ||
650 | videoBody: req.body, | ||
651 | videoFile, | ||
652 | user: res.locals.oauth.token.User | ||
653 | } | ||
654 | const acceptedResult = await Hooks.wrapFun( | ||
655 | isLocalVideoAccepted, | ||
656 | acceptParameters, | ||
657 | 'filter:api.video.upload.accept.result' | ||
658 | ) | ||
659 | |||
660 | if (!acceptedResult || acceptedResult.accepted !== true) { | ||
661 | logger.info('Refused local video.', { acceptedResult, acceptParameters }) | ||
662 | res.fail({ | ||
663 | status: HttpStatusCode.FORBIDDEN_403, | ||
664 | message: acceptedResult.errorMessage || 'Refused local video' | ||
665 | }) | ||
666 | return false | ||
667 | } | ||
668 | 593 | ||
669 | return true | 594 | return true |
670 | } | 595 | } |
671 | |||
672 | async function addDurationToVideo (videoFile: { path: string, duration?: number }) { | ||
673 | const duration = await getVideoStreamDuration(videoFile.path) | ||
674 | |||
675 | // FFmpeg may not be able to guess video duration | ||
676 | // For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2 | ||
677 | if (isNaN(duration)) videoFile.duration = 0 | ||
678 | else videoFile.duration = duration | ||
679 | } | ||