diff options
author | kontrollanten <6680299+kontrollanten@users.noreply.github.com> | 2021-05-10 11:13:41 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-10 11:13:41 +0200 |
commit | f6d6e7f861189a4446f406efb775a29688764b48 (patch) | |
tree | c3dda9958c3f189d4c39e8743c738d8c1fef4c2d /server | |
parent | d29ced1a8582d99b776f664475a157adcf555d98 (diff) | |
download | PeerTube-f6d6e7f861189a4446f406efb775a29688764b48.tar.gz PeerTube-f6d6e7f861189a4446f406efb775a29688764b48.tar.zst PeerTube-f6d6e7f861189a4446f406efb775a29688764b48.zip |
Resumable video uploads (#3933)
* WIP: resumable video uploads
relates to #324
* fix review comments
* video upload: error handling
* fix audio upload
* fixes after self review
* Update server/controllers/api/videos/index.ts
Co-authored-by: Rigel Kent <par@rigelk.eu>
* Update server/middlewares/validators/videos/videos.ts
Co-authored-by: Rigel Kent <par@rigelk.eu>
* Update server/controllers/api/videos/index.ts
Co-authored-by: Rigel Kent <par@rigelk.eu>
* update after code review
* refactor upload route
- restore multipart upload route
- move resumable to dedicated upload-resumable route
- move checks to middleware
- do not leak internal fs structure in response
* fix yarn.lock upon rebase
* factorize addVideo for reuse in both endpoints
* add resumable upload API to openapi spec
* add initial test and test helper for resumable upload
* typings for videoAddResumable middleware
* avoid including aws and google packages via node-uploadx, by only including uploadx/core
* rename ex-isAudioBg to more explicit name mentioning it is a preview file for audio
* add video-upload-tmp-folder-cleaner job
* stronger typing of video upload middleware
* reduce dependency to @uploadx/core
* add audio upload test
* refactor resumable uploads cleanup from job to scheduler
* refactor resumable uploads scheduler to compare to last execution time
* make resumable upload validator to always cleanup on failure
* move legacy upload request building outside of uploadVideo test helper
* filter upload-resumable middlewares down to POST, PUT, DELETE
also begin to type metadata
* merge add duration functions
* stronger typings and documentation for uploadx behaviour, move init validator up
* refactor(client/video-edit): options > uploadxOptions
* refactor(client/video-edit): remove obsolete else
* scheduler/remove-dangling-resum: rename tag
* refactor(server/video): add UploadVideoFiles type
* refactor(mw/validators): restructure eslint disable
* refactor(mw/validators/videos): rename import
* refactor(client/vid-upload): rename html elem id
* refactor(sched/remove-dangl): move fn to method
* refactor(mw/async): add method typing
* refactor(mw/vali/video): double quote > single
* refactor(server/upload-resum): express use > all
* proper http methud enum server/middlewares/async.ts
* properly type http methods
* factorize common video upload validation steps
* add check for maximum partially uploaded file size
* fix audioBg use
* fix extname(filename) in addVideo
* document parameters for uploadx's resumable protocol
* clear META files in scheduler
* last audio refactor before cramming preview in the initial POST form data
* refactor as mulitpart/form-data initial post request
this allows preview/thumbnail uploads alongside the initial request,
and cleans up the upload form
* Add more tests for resumable uploads
* Refactor remove dangling resumable uploads
* Prepare changelog
* Add more resumable upload tests
* Remove user quota check for resumable uploads
* Fix upload error handler
* Update nginx template for upload-resumable
* Cleanup comment
* Remove unused express methods
* Prefer to use got instead of raw http
* Don't retry on error 500
Co-authored-by: Rigel Kent <par@rigelk.eu>
Co-authored-by: Rigel Kent <sendmemail@rigelk.eu>
Co-authored-by: Chocobozzz <me@florianbigard.com>
Diffstat (limited to 'server')
24 files changed, 1444 insertions, 855 deletions
diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts index 7787186be..ff0d9ca3c 100644 --- a/server/controllers/api/server/debug.ts +++ b/server/controllers/api/server/debug.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { InboxManager } from '@server/lib/activitypub/inbox-manager' | 1 | import { InboxManager } from '@server/lib/activitypub/inbox-manager' |
2 | import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' | ||
3 | import { SendDebugCommand } from '@shared/models' | ||
2 | import * as express from 'express' | 4 | import * as express from 'express' |
3 | import { UserRight } from '../../../../shared/models/users' | 5 | import { UserRight } from '../../../../shared/models/users' |
4 | import { authenticate, ensureUserHasRight } from '../../../middlewares' | 6 | import { authenticate, ensureUserHasRight } from '../../../middlewares' |
@@ -11,6 +13,12 @@ debugRouter.get('/debug', | |||
11 | getDebug | 13 | getDebug |
12 | ) | 14 | ) |
13 | 15 | ||
16 | debugRouter.post('/debug/run-command', | ||
17 | authenticate, | ||
18 | ensureUserHasRight(UserRight.MANAGE_DEBUG), | ||
19 | runCommand | ||
20 | ) | ||
21 | |||
14 | // --------------------------------------------------------------------------- | 22 | // --------------------------------------------------------------------------- |
15 | 23 | ||
16 | export { | 24 | export { |
@@ -25,3 +33,13 @@ function getDebug (req: express.Request, res: express.Response) { | |||
25 | activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting() | 33 | activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting() |
26 | }) | 34 | }) |
27 | } | 35 | } |
36 | |||
37 | async function runCommand (req: express.Request, res: express.Response) { | ||
38 | const body: SendDebugCommand = req.body | ||
39 | |||
40 | if (body.command === 'remove-dandling-resumable-uploads') { | ||
41 | await RemoveDanglingResumableUploadsScheduler.Instance.execute() | ||
42 | } | ||
43 | |||
44 | return res.sendStatus(204) | ||
45 | } | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index fbdb0f776..c32626d30 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -2,6 +2,7 @@ import * as express from 'express' | |||
2 | import { move } from 'fs-extra' | 2 | import { move } from 'fs-extra' |
3 | import { extname } from 'path' | 3 | import { extname } from 'path' |
4 | import toInt from 'validator/lib/toInt' | 4 | import toInt from 'validator/lib/toInt' |
5 | import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 6 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
6 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | 7 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' |
7 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 8 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
@@ -10,8 +11,9 @@ import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnail | |||
10 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 11 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
11 | import { getServerActor } from '@server/models/application/application' | 12 | import { getServerActor } from '@server/models/application/application' |
12 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 13 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
14 | import { uploadx } from '@uploadx/core' | ||
13 | import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' | 15 | import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' |
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 16 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' |
15 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 17 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
16 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 18 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' |
17 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' | 19 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' |
@@ -47,7 +49,9 @@ import { | |||
47 | setDefaultPagination, | 49 | setDefaultPagination, |
48 | setDefaultVideosSort, | 50 | setDefaultVideosSort, |
49 | videoFileMetadataGetValidator, | 51 | videoFileMetadataGetValidator, |
50 | videosAddValidator, | 52 | videosAddLegacyValidator, |
53 | videosAddResumableInitValidator, | ||
54 | videosAddResumableValidator, | ||
51 | videosCustomGetValidator, | 55 | videosCustomGetValidator, |
52 | videosGetValidator, | 56 | videosGetValidator, |
53 | videosRemoveValidator, | 57 | videosRemoveValidator, |
@@ -69,6 +73,7 @@ import { watchingRouter } from './watching' | |||
69 | const lTags = loggerTagsFactory('api', 'video') | 73 | const lTags = loggerTagsFactory('api', 'video') |
70 | const auditLogger = auditLoggerFactory('videos') | 74 | const auditLogger = auditLoggerFactory('videos') |
71 | const videosRouter = express.Router() | 75 | const videosRouter = express.Router() |
76 | const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() }) | ||
72 | 77 | ||
73 | const reqVideoFileAdd = createReqFiles( | 78 | const reqVideoFileAdd = createReqFiles( |
74 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | 79 | [ 'videofile', 'thumbnailfile', 'previewfile' ], |
@@ -79,6 +84,16 @@ const reqVideoFileAdd = createReqFiles( | |||
79 | previewfile: CONFIG.STORAGE.TMP_DIR | 84 | previewfile: CONFIG.STORAGE.TMP_DIR |
80 | } | 85 | } |
81 | ) | 86 | ) |
87 | |||
88 | const reqVideoFileAddResumable = createReqFiles( | ||
89 | [ 'thumbnailfile', 'previewfile' ], | ||
90 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
91 | { | ||
92 | thumbnailfile: getResumableUploadPath(), | ||
93 | previewfile: getResumableUploadPath() | ||
94 | } | ||
95 | ) | ||
96 | |||
82 | const reqVideoFileUpdate = createReqFiles( | 97 | const reqVideoFileUpdate = createReqFiles( |
83 | [ 'thumbnailfile', 'previewfile' ], | 98 | [ 'thumbnailfile', 'previewfile' ], |
84 | MIMETYPES.IMAGE.MIMETYPE_EXT, | 99 | MIMETYPES.IMAGE.MIMETYPE_EXT, |
@@ -111,18 +126,39 @@ videosRouter.get('/', | |||
111 | commonVideosFiltersValidator, | 126 | commonVideosFiltersValidator, |
112 | asyncMiddleware(listVideos) | 127 | asyncMiddleware(listVideos) |
113 | ) | 128 | ) |
129 | |||
130 | videosRouter.post('/upload', | ||
131 | authenticate, | ||
132 | reqVideoFileAdd, | ||
133 | asyncMiddleware(videosAddLegacyValidator), | ||
134 | asyncRetryTransactionMiddleware(addVideoLegacy) | ||
135 | ) | ||
136 | |||
137 | videosRouter.post('/upload-resumable', | ||
138 | authenticate, | ||
139 | reqVideoFileAddResumable, | ||
140 | asyncMiddleware(videosAddResumableInitValidator), | ||
141 | uploadxMiddleware | ||
142 | ) | ||
143 | |||
144 | videosRouter.delete('/upload-resumable', | ||
145 | authenticate, | ||
146 | uploadxMiddleware | ||
147 | ) | ||
148 | |||
149 | videosRouter.put('/upload-resumable', | ||
150 | authenticate, | ||
151 | uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes | ||
152 | asyncMiddleware(videosAddResumableValidator), | ||
153 | asyncMiddleware(addVideoResumable) | ||
154 | ) | ||
155 | |||
114 | videosRouter.put('/:id', | 156 | videosRouter.put('/:id', |
115 | authenticate, | 157 | authenticate, |
116 | reqVideoFileUpdate, | 158 | reqVideoFileUpdate, |
117 | asyncMiddleware(videosUpdateValidator), | 159 | asyncMiddleware(videosUpdateValidator), |
118 | asyncRetryTransactionMiddleware(updateVideo) | 160 | asyncRetryTransactionMiddleware(updateVideo) |
119 | ) | 161 | ) |
120 | videosRouter.post('/upload', | ||
121 | authenticate, | ||
122 | reqVideoFileAdd, | ||
123 | asyncMiddleware(videosAddValidator), | ||
124 | asyncRetryTransactionMiddleware(addVideo) | ||
125 | ) | ||
126 | 162 | ||
127 | videosRouter.get('/:id/description', | 163 | videosRouter.get('/:id/description', |
128 | asyncMiddleware(videosGetValidator), | 164 | asyncMiddleware(videosGetValidator), |
@@ -157,23 +193,23 @@ export { | |||
157 | 193 | ||
158 | // --------------------------------------------------------------------------- | 194 | // --------------------------------------------------------------------------- |
159 | 195 | ||
160 | function listVideoCategories (req: express.Request, res: express.Response) { | 196 | function listVideoCategories (_req: express.Request, res: express.Response) { |
161 | res.json(VIDEO_CATEGORIES) | 197 | res.json(VIDEO_CATEGORIES) |
162 | } | 198 | } |
163 | 199 | ||
164 | function listVideoLicences (req: express.Request, res: express.Response) { | 200 | function listVideoLicences (_req: express.Request, res: express.Response) { |
165 | res.json(VIDEO_LICENCES) | 201 | res.json(VIDEO_LICENCES) |
166 | } | 202 | } |
167 | 203 | ||
168 | function listVideoLanguages (req: express.Request, res: express.Response) { | 204 | function listVideoLanguages (_req: express.Request, res: express.Response) { |
169 | res.json(VIDEO_LANGUAGES) | 205 | res.json(VIDEO_LANGUAGES) |
170 | } | 206 | } |
171 | 207 | ||
172 | function listVideoPrivacies (req: express.Request, res: express.Response) { | 208 | function listVideoPrivacies (_req: express.Request, res: express.Response) { |
173 | res.json(VIDEO_PRIVACIES) | 209 | res.json(VIDEO_PRIVACIES) |
174 | } | 210 | } |
175 | 211 | ||
176 | async function addVideo (req: express.Request, res: express.Response) { | 212 | async function addVideoLegacy (req: express.Request, res: express.Response) { |
177 | // Uploading the video could be long | 213 | // Uploading the video could be long |
178 | // Set timeout to 10 minutes, as Express's default is 2 minutes | 214 | // Set timeout to 10 minutes, as Express's default is 2 minutes |
179 | req.setTimeout(1000 * 60 * 10, () => { | 215 | req.setTimeout(1000 * 60 * 10, () => { |
@@ -183,13 +219,42 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
183 | 219 | ||
184 | const videoPhysicalFile = req.files['videofile'][0] | 220 | const videoPhysicalFile = req.files['videofile'][0] |
185 | const videoInfo: VideoCreate = req.body | 221 | const videoInfo: VideoCreate = req.body |
222 | const files = req.files | ||
223 | |||
224 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
225 | } | ||
226 | |||
227 | async function addVideoResumable (_req: express.Request, res: express.Response) { | ||
228 | const videoPhysicalFile = res.locals.videoFileResumable | ||
229 | const videoInfo = videoPhysicalFile.metadata | ||
230 | const files = { previewfile: videoInfo.previewfile } | ||
231 | |||
232 | // Don't need the meta file anymore | ||
233 | await deleteResumableUploadMetaFile(videoPhysicalFile.path) | ||
234 | |||
235 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
236 | } | ||
186 | 237 | ||
187 | const videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id) | 238 | async function addVideo (options: { |
188 | videoData.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED | 239 | res: express.Response |
189 | videoData.duration = videoPhysicalFile['duration'] // duration was added by a previous middleware | 240 | videoPhysicalFile: express.VideoUploadFile |
241 | videoInfo: VideoCreate | ||
242 | files: express.UploadFiles | ||
243 | }) { | ||
244 | const { res, videoPhysicalFile, videoInfo, files } = options | ||
245 | const videoChannel = res.locals.videoChannel | ||
246 | const user = res.locals.oauth.token.User | ||
247 | |||
248 | const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) | ||
249 | |||
250 | videoData.state = CONFIG.TRANSCODING.ENABLED | ||
251 | ? VideoState.TO_TRANSCODE | ||
252 | : VideoState.PUBLISHED | ||
253 | |||
254 | videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware | ||
190 | 255 | ||
191 | const video = new VideoModel(videoData) as MVideoFullLight | 256 | const video = new VideoModel(videoData) as MVideoFullLight |
192 | video.VideoChannel = res.locals.videoChannel | 257 | video.VideoChannel = videoChannel |
193 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | 258 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object |
194 | 259 | ||
195 | const videoFile = new VideoFileModel({ | 260 | const videoFile = new VideoFileModel({ |
@@ -217,7 +282,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
217 | 282 | ||
218 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | 283 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ |
219 | video, | 284 | video, |
220 | files: req.files, | 285 | files, |
221 | fallback: type => generateVideoMiniature({ video, videoFile, type }) | 286 | fallback: type => generateVideoMiniature({ video, videoFile, type }) |
222 | }) | 287 | }) |
223 | 288 | ||
@@ -253,7 +318,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
253 | 318 | ||
254 | await autoBlacklistVideoIfNeeded({ | 319 | await autoBlacklistVideoIfNeeded({ |
255 | video, | 320 | video, |
256 | user: res.locals.oauth.token.User, | 321 | user, |
257 | isRemote: false, | 322 | isRemote: false, |
258 | isNew: true, | 323 | isNew: true, |
259 | transaction: t | 324 | transaction: t |
@@ -282,7 +347,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
282 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) | 347 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) |
283 | 348 | ||
284 | if (video.state === VideoState.TO_TRANSCODE) { | 349 | if (video.state === VideoState.TO_TRANSCODE) { |
285 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) | 350 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) |
286 | } | 351 | } |
287 | 352 | ||
288 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) | 353 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index effdd98cb..fd3b45804 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import validator from 'validator' | 2 | import { UploadFilesForCheck } from 'express' |
3 | import { sep } from 'path' | 3 | import { sep } from 'path' |
4 | import validator from 'validator' | ||
4 | 5 | ||
5 | function exists (value: any) { | 6 | function exists (value: any) { |
6 | return value !== undefined && value !== null | 7 | return value !== undefined && value !== null |
@@ -108,7 +109,7 @@ function isFileFieldValid ( | |||
108 | } | 109 | } |
109 | 110 | ||
110 | function isFileMimeTypeValid ( | 111 | function isFileMimeTypeValid ( |
111 | files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], | 112 | files: UploadFilesForCheck, |
112 | mimeTypeRegex: string, | 113 | mimeTypeRegex: string, |
113 | field: string, | 114 | field: string, |
114 | optional = false | 115 | optional = false |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 87966798f..b33e088eb 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { UploadFilesForCheck } from 'express' | ||
1 | import { values } from 'lodash' | 2 | import { values } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | ||
2 | import validator from 'validator' | 4 | import validator from 'validator' |
3 | import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' | 5 | import { VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' |
4 | import { | 6 | import { |
@@ -6,13 +8,12 @@ import { | |||
6 | MIMETYPES, | 8 | MIMETYPES, |
7 | VIDEO_CATEGORIES, | 9 | VIDEO_CATEGORIES, |
8 | VIDEO_LICENCES, | 10 | VIDEO_LICENCES, |
11 | VIDEO_LIVE, | ||
9 | VIDEO_PRIVACIES, | 12 | VIDEO_PRIVACIES, |
10 | VIDEO_RATE_TYPES, | 13 | VIDEO_RATE_TYPES, |
11 | VIDEO_STATES, | 14 | VIDEO_STATES |
12 | VIDEO_LIVE | ||
13 | } from '../../initializers/constants' | 15 | } from '../../initializers/constants' |
14 | import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc' | 16 | import { exists, isArray, isDateValid, isFileMimeTypeValid, isFileValid } from './misc' |
15 | import * as magnetUtil from 'magnet-uri' | ||
16 | 17 | ||
17 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 18 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
18 | 19 | ||
@@ -81,7 +82,7 @@ function isVideoFileExtnameValid (value: string) { | |||
81 | return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined) | 82 | return exists(value) && (value === VIDEO_LIVE.EXTENSION || MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined) |
82 | } | 83 | } |
83 | 84 | ||
84 | function isVideoFileMimeTypeValid (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 85 | function isVideoFileMimeTypeValid (files: UploadFilesForCheck) { |
85 | return isFileMimeTypeValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile') | 86 | return isFileMimeTypeValid(files, MIMETYPES.VIDEO.MIMETYPES_REGEX, 'videofile') |
86 | } | 87 | } |
87 | 88 | ||
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index c0d3f8f32..ede22a3cc 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { REMOTE_SCHEME } from '../initializers/constants' | 3 | import { REMOTE_SCHEME } from '../initializers/constants' |
4 | import { logger } from './logger' | 4 | import { logger } from './logger' |
5 | import { deleteFileAsync, generateRandomString } from './utils' | 5 | import { deleteFileAndCatch, generateRandomString } from './utils' |
6 | import { extname } from 'path' | 6 | import { extname } from 'path' |
7 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
8 | import { CONFIG } from '../initializers/config' | 8 | import { CONFIG } from '../initializers/config' |
@@ -36,15 +36,15 @@ function cleanUpReqFiles (req: { files: { [fieldname: string]: Express.Multer.Fi | |||
36 | if (!files) return | 36 | if (!files) return |
37 | 37 | ||
38 | if (isArray(files)) { | 38 | if (isArray(files)) { |
39 | (files as Express.Multer.File[]).forEach(f => deleteFileAsync(f.path)) | 39 | (files as Express.Multer.File[]).forEach(f => deleteFileAndCatch(f.path)) |
40 | return | 40 | return |
41 | } | 41 | } |
42 | 42 | ||
43 | for (const key of Object.keys(files)) { | 43 | for (const key of Object.keys(files)) { |
44 | const file = files[key] | 44 | const file = files[key] |
45 | 45 | ||
46 | if (isArray(file)) file.forEach(f => deleteFileAsync(f.path)) | 46 | if (isArray(file)) file.forEach(f => deleteFileAndCatch(f.path)) |
47 | else deleteFileAsync(file.path) | 47 | else deleteFileAndCatch(file.path) |
48 | } | 48 | } |
49 | } | 49 | } |
50 | 50 | ||
diff --git a/server/helpers/upload.ts b/server/helpers/upload.ts new file mode 100644 index 000000000..030a6b7d5 --- /dev/null +++ b/server/helpers/upload.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { METAFILE_EXTNAME } from '@uploadx/core' | ||
2 | import { remove } from 'fs-extra' | ||
3 | import { join } from 'path' | ||
4 | import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' | ||
5 | |||
6 | function getResumableUploadPath (filename?: string) { | ||
7 | if (filename) return join(RESUMABLE_UPLOAD_DIRECTORY, filename) | ||
8 | |||
9 | return RESUMABLE_UPLOAD_DIRECTORY | ||
10 | } | ||
11 | |||
12 | function deleteResumableUploadMetaFile (filepath: string) { | ||
13 | return remove(filepath + METAFILE_EXTNAME) | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | getResumableUploadPath, | ||
20 | deleteResumableUploadMetaFile | ||
21 | } | ||
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 0545e8996..6c95a43b6 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -6,7 +6,7 @@ import { CONFIG } from '../initializers/config' | |||
6 | import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils' | 6 | import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils' |
7 | import { logger } from './logger' | 7 | import { logger } from './logger' |
8 | 8 | ||
9 | function deleteFileAsync (path: string) { | 9 | function deleteFileAndCatch (path: string) { |
10 | remove(path) | 10 | remove(path) |
11 | .catch(err => logger.error('Cannot delete the file %s asynchronously.', path, { err })) | 11 | .catch(err => logger.error('Cannot delete the file %s asynchronously.', path, { err })) |
12 | } | 12 | } |
@@ -83,7 +83,7 @@ function getUUIDFromFilename (filename: string) { | |||
83 | // --------------------------------------------------------------------------- | 83 | // --------------------------------------------------------------------------- |
84 | 84 | ||
85 | export { | 85 | export { |
86 | deleteFileAsync, | 86 | deleteFileAndCatch, |
87 | generateRandomString, | 87 | generateRandomString, |
88 | getFormattedObjects, | 88 | getFormattedObjects, |
89 | getSecureTorrentName, | 89 | getSecureTorrentName, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index f807a1e58..6f388420e 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -208,7 +208,8 @@ const SCHEDULER_INTERVALS_MS = { | |||
208 | autoFollowIndexInstances: 60000 * 60 * 24, // 1 day | 208 | autoFollowIndexInstances: 60000 * 60 * 24, // 1 day |
209 | removeOldViews: 60000 * 60 * 24, // 1 day | 209 | removeOldViews: 60000 * 60 * 24, // 1 day |
210 | removeOldHistory: 60000 * 60 * 24, // 1 day | 210 | removeOldHistory: 60000 * 60 * 24, // 1 day |
211 | updateInboxStats: 1000 * 60// 1 minute | 211 | updateInboxStats: 1000 * 60, // 1 minute |
212 | removeDanglingResumableUploads: 60000 * 60 * 16 // 16 hours | ||
212 | } | 213 | } |
213 | 214 | ||
214 | // --------------------------------------------------------------------------- | 215 | // --------------------------------------------------------------------------- |
@@ -285,6 +286,7 @@ const CONSTRAINTS_FIELDS = { | |||
285 | LIKES: { min: 0 }, | 286 | LIKES: { min: 0 }, |
286 | DISLIKES: { min: 0 }, | 287 | DISLIKES: { min: 0 }, |
287 | FILE_SIZE: { min: -1 }, | 288 | FILE_SIZE: { min: -1 }, |
289 | PARTIAL_UPLOAD_SIZE: { max: 50 * 1024 * 1024 * 1024 }, // 50GB | ||
288 | URL: { min: 3, max: 2000 } // Length | 290 | URL: { min: 3, max: 2000 } // Length |
289 | }, | 291 | }, |
290 | VIDEO_PLAYLISTS: { | 292 | VIDEO_PLAYLISTS: { |
@@ -645,6 +647,7 @@ const LRU_CACHE = { | |||
645 | } | 647 | } |
646 | } | 648 | } |
647 | 649 | ||
650 | const RESUMABLE_UPLOAD_DIRECTORY = join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads') | ||
648 | const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls') | 651 | const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls') |
649 | const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | 652 | const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') |
650 | 653 | ||
@@ -819,6 +822,7 @@ export { | |||
819 | PEERTUBE_VERSION, | 822 | PEERTUBE_VERSION, |
820 | LAZY_STATIC_PATHS, | 823 | LAZY_STATIC_PATHS, |
821 | SEARCH_INDEX, | 824 | SEARCH_INDEX, |
825 | RESUMABLE_UPLOAD_DIRECTORY, | ||
822 | HLS_REDUNDANCY_DIRECTORY, | 826 | HLS_REDUNDANCY_DIRECTORY, |
823 | P2P_MEDIA_LOADER_PEER_VERSION, | 827 | P2P_MEDIA_LOADER_PEER_VERSION, |
824 | ACTOR_IMAGES_SIZE, | 828 | ACTOR_IMAGES_SIZE, |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index cb58454cb..8dcff64e2 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -6,7 +6,7 @@ import { UserModel } from '../models/account/user' | |||
6 | import { ApplicationModel } from '../models/application/application' | 6 | import { ApplicationModel } from '../models/application/application' |
7 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 7 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' | 8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' |
9 | import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants' | 9 | import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION, RESUMABLE_UPLOAD_DIRECTORY } from './constants' |
10 | import { sequelizeTypescript } from './database' | 10 | import { sequelizeTypescript } from './database' |
11 | import { ensureDir, remove } from 'fs-extra' | 11 | import { ensureDir, remove } from 'fs-extra' |
12 | import { CONFIG } from './config' | 12 | import { CONFIG } from './config' |
@@ -79,6 +79,9 @@ function createDirectoriesIfNotExist () { | |||
79 | // Playlist directories | 79 | // Playlist directories |
80 | tasks.push(ensureDir(HLS_STREAMING_PLAYLIST_DIRECTORY)) | 80 | tasks.push(ensureDir(HLS_STREAMING_PLAYLIST_DIRECTORY)) |
81 | 81 | ||
82 | // Resumable upload directory | ||
83 | tasks.push(ensureDir(RESUMABLE_UPLOAD_DIRECTORY)) | ||
84 | |||
82 | return Promise.all(tasks) | 85 | return Promise.all(tasks) |
83 | } | 86 | } |
84 | 87 | ||
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index 5180b3299..925d64902 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { VideoUploadFile } from 'express' | ||
1 | import { PathLike } from 'fs-extra' | 2 | import { PathLike } from 'fs-extra' |
2 | import { Transaction } from 'sequelize/types' | 3 | import { Transaction } from 'sequelize/types' |
3 | import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger' | 4 | import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger' |
5 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
4 | import { logger } from '@server/helpers/logger' | 6 | import { logger } from '@server/helpers/logger' |
5 | import { AbuseModel } from '@server/models/abuse/abuse' | 7 | import { AbuseModel } from '@server/models/abuse/abuse' |
6 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' | 8 | import { VideoAbuseModel } from '@server/models/abuse/video-abuse' |
@@ -28,7 +30,6 @@ import { VideoModel } from '../models/video/video' | |||
28 | import { VideoCommentModel } from '../models/video/video-comment' | 30 | import { VideoCommentModel } from '../models/video/video-comment' |
29 | import { sendAbuse } from './activitypub/send/send-flag' | 31 | import { sendAbuse } from './activitypub/send/send-flag' |
30 | import { Notifier } from './notifier' | 32 | import { Notifier } from './notifier' |
31 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
32 | 33 | ||
33 | export type AcceptResult = { | 34 | export type AcceptResult = { |
34 | accepted: boolean | 35 | accepted: boolean |
@@ -38,7 +39,7 @@ export type AcceptResult = { | |||
38 | // Can be filtered by plugins | 39 | // Can be filtered by plugins |
39 | function isLocalVideoAccepted (object: { | 40 | function isLocalVideoAccepted (object: { |
40 | videoBody: VideoCreate | 41 | videoBody: VideoCreate |
41 | videoFile: Express.Multer.File & { duration?: number } | 42 | videoFile: VideoUploadFile |
42 | user: UserModel | 43 | user: UserModel |
43 | }): AcceptResult { | 44 | }): AcceptResult { |
44 | return { accepted: true } | 45 | return { accepted: true } |
diff --git a/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts new file mode 100644 index 000000000..1acea7998 --- /dev/null +++ b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts | |||
@@ -0,0 +1,61 @@ | |||
1 | import * as bluebird from 'bluebird' | ||
2 | import { readdir, remove, stat } from 'fs-extra' | ||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
4 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants' | ||
6 | import { METAFILE_EXTNAME } from '@uploadx/core' | ||
7 | import { AbstractScheduler } from './abstract-scheduler' | ||
8 | |||
9 | const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner') | ||
10 | |||
11 | export class RemoveDanglingResumableUploadsScheduler extends AbstractScheduler { | ||
12 | |||
13 | private static instance: AbstractScheduler | ||
14 | private lastExecutionTimeMs: number | ||
15 | |||
16 | protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.removeDanglingResumableUploads | ||
17 | |||
18 | private constructor () { | ||
19 | super() | ||
20 | |||
21 | this.lastExecutionTimeMs = new Date().getTime() | ||
22 | } | ||
23 | |||
24 | protected async internalExecute () { | ||
25 | const path = getResumableUploadPath() | ||
26 | const files = await readdir(path) | ||
27 | |||
28 | const metafiles = files.filter(f => f.endsWith(METAFILE_EXTNAME)) | ||
29 | |||
30 | if (metafiles.length === 0) return | ||
31 | |||
32 | logger.debug('Reading resumable video upload folder %s with %d files', path, metafiles.length, lTags()) | ||
33 | |||
34 | try { | ||
35 | await bluebird.map(metafiles, metafile => { | ||
36 | return this.deleteIfOlderThan(metafile, this.lastExecutionTimeMs) | ||
37 | }, { concurrency: 5 }) | ||
38 | } catch (error) { | ||
39 | logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() }) | ||
40 | } finally { | ||
41 | this.lastExecutionTimeMs = new Date().getTime() | ||
42 | } | ||
43 | } | ||
44 | |||
45 | private async deleteIfOlderThan (metafile: string, olderThan: number) { | ||
46 | const metafilePath = getResumableUploadPath(metafile) | ||
47 | const statResult = await stat(metafilePath) | ||
48 | |||
49 | // Delete uploads that started since a long time | ||
50 | if (statResult.ctimeMs < olderThan) { | ||
51 | await remove(metafilePath) | ||
52 | |||
53 | const datafile = metafilePath.replace(new RegExp(`${METAFILE_EXTNAME}$`), '') | ||
54 | await remove(datafile) | ||
55 | } | ||
56 | } | ||
57 | |||
58 | static get Instance () { | ||
59 | return this.instance || (this.instance = new this()) | ||
60 | } | ||
61 | } | ||
diff --git a/server/lib/video.ts b/server/lib/video.ts index 9469b8178..21e4b7ff2 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { UploadFiles } from 'express' | ||
1 | import { Transaction } from 'sequelize/types' | 2 | import { Transaction } from 'sequelize/types' |
2 | import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants' | 3 | import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants' |
3 | import { sequelizeTypescript } from '@server/initializers/database' | 4 | import { sequelizeTypescript } from '@server/initializers/database' |
@@ -32,7 +33,7 @@ function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): Fil | |||
32 | 33 | ||
33 | async function buildVideoThumbnailsFromReq (options: { | 34 | async function buildVideoThumbnailsFromReq (options: { |
34 | video: MVideoThumbnail | 35 | video: MVideoThumbnail |
35 | files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] | 36 | files: UploadFiles |
36 | fallback: (type: ThumbnailType) => Promise<MThumbnail> | 37 | fallback: (type: ThumbnailType) => Promise<MThumbnail> |
37 | automaticallyGenerated?: boolean | 38 | automaticallyGenerated?: boolean |
38 | }) { | 39 | }) { |
diff --git a/server/middlewares/async.ts b/server/middlewares/async.ts index 3d6e38809..0faa4fb8c 100644 --- a/server/middlewares/async.ts +++ b/server/middlewares/async.ts | |||
@@ -3,6 +3,7 @@ import { NextFunction, Request, RequestHandler, Response } from 'express' | |||
3 | import { ValidationChain } from 'express-validator' | 3 | import { ValidationChain } from 'express-validator' |
4 | import { ExpressPromiseHandler } from '@server/types/express' | 4 | import { ExpressPromiseHandler } from '@server/types/express' |
5 | import { retryTransactionWrapper } from '../helpers/database-utils' | 5 | import { retryTransactionWrapper } from '../helpers/database-utils' |
6 | import { HttpMethod, HttpStatusCode } from '@shared/core-utils' | ||
6 | 7 | ||
7 | // Syntactic sugar to avoid try/catch in express controllers | 8 | // Syntactic sugar to avoid try/catch in express controllers |
8 | // Thanks: https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016 | 9 | // Thanks: https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016 |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index bb617d77c..d26bcd4a6 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query, ValidationChain } from 'express-validator' | 2 | import { body, header, param, query, ValidationChain } from 'express-validator' |
3 | import { getResumableUploadPath } from '@server/helpers/upload' | ||
3 | import { isAbleToUploadVideo } from '@server/lib/user' | 4 | import { isAbleToUploadVideo } from '@server/lib/user' |
4 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
5 | import { ExpressPromiseHandler } from '@server/types/express' | 6 | import { ExpressPromiseHandler } from '@server/types/express' |
6 | import { MVideoWithRights } from '@server/types/models' | 7 | import { MUserAccountId, MVideoWithRights } from '@server/types/models' |
7 | import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' | 8 | import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' |
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 9 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
9 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' | 10 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' |
@@ -47,6 +48,7 @@ import { | |||
47 | doesVideoExist, | 48 | doesVideoExist, |
48 | doesVideoFileOfVideoExist | 49 | doesVideoFileOfVideoExist |
49 | } from '../../../helpers/middlewares' | 50 | } from '../../../helpers/middlewares' |
51 | import { deleteFileAndCatch } from '../../../helpers/utils' | ||
50 | import { getVideoWithAttributes } from '../../../helpers/video' | 52 | import { getVideoWithAttributes } from '../../../helpers/video' |
51 | import { CONFIG } from '../../../initializers/config' | 53 | import { CONFIG } from '../../../initializers/config' |
52 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | 54 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' |
@@ -57,7 +59,7 @@ import { VideoModel } from '../../../models/video/video' | |||
57 | import { authenticatePromiseIfNeeded } from '../../auth' | 59 | import { authenticatePromiseIfNeeded } from '../../auth' |
58 | import { areValidationErrors } from '../utils' | 60 | import { areValidationErrors } from '../utils' |
59 | 61 | ||
60 | const videosAddValidator = getCommonVideoEditAttributes().concat([ | 62 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ |
61 | body('videofile') | 63 | body('videofile') |
62 | .custom((value, { req }) => isFileFieldValid(req.files, 'videofile')) | 64 | .custom((value, { req }) => isFileFieldValid(req.files, 'videofile')) |
63 | .withMessage('Should have a file'), | 65 | .withMessage('Should have a file'), |
@@ -73,54 +75,117 @@ const videosAddValidator = getCommonVideoEditAttributes().concat([ | |||
73 | logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) | 75 | logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) |
74 | 76 | ||
75 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 77 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
76 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | ||
77 | 78 | ||
78 | const videoFile: Express.Multer.File & { duration?: number } = req.files['videofile'][0] | 79 | const videoFile: express.VideoUploadFile = req.files['videofile'][0] |
79 | const user = res.locals.oauth.token.User | 80 | const user = res.locals.oauth.token.User |
80 | 81 | ||
81 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | 82 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files })) { |
82 | |||
83 | if (!isVideoFileMimeTypeValid(req.files)) { | ||
84 | res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) | ||
85 | .json({ | ||
86 | error: 'This file is not supported. Please, make sure it is of the following type: ' + | ||
87 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | ||
88 | }) | ||
89 | |||
90 | return cleanUpReqFiles(req) | 83 | return cleanUpReqFiles(req) |
91 | } | 84 | } |
92 | 85 | ||
93 | if (!isVideoFileSizeValid(videoFile.size.toString())) { | 86 | try { |
94 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | 87 | if (!videoFile.duration) await addDurationToVideo(videoFile) |
95 | .json({ | 88 | } catch (err) { |
96 | error: 'This file is too large.' | 89 | logger.error('Invalid input file in videosAddLegacyValidator.', { err }) |
97 | }) | 90 | res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) |
91 | .json({ error: 'Video file unreadable.' }) | ||
98 | 92 | ||
99 | return cleanUpReqFiles(req) | 93 | return cleanUpReqFiles(req) |
100 | } | 94 | } |
101 | 95 | ||
102 | if (await isAbleToUploadVideo(user.id, videoFile.size) === false) { | 96 | if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req) |
103 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
104 | .json({ error: 'The user video quota is exceeded with this video.' }) | ||
105 | 97 | ||
106 | return cleanUpReqFiles(req) | 98 | return next() |
107 | } | 99 | } |
100 | ]) | ||
101 | |||
102 | /** | ||
103 | * Gets called after the last PUT request | ||
104 | */ | ||
105 | const videosAddResumableValidator = [ | ||
106 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
107 | const user = res.locals.oauth.token.User | ||
108 | |||
109 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body | ||
110 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.id), filename: body.metadata.filename } | ||
111 | |||
112 | const cleanup = () => deleteFileAndCatch(file.path) | ||
108 | 113 | ||
109 | let duration: number | 114 | if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup() |
110 | 115 | ||
111 | try { | 116 | try { |
112 | duration = await getDurationFromVideoFile(videoFile.path) | 117 | if (!file.duration) await addDurationToVideo(file) |
113 | } catch (err) { | 118 | } catch (err) { |
114 | logger.error('Invalid input file in videosAddValidator.', { err }) | 119 | logger.error('Invalid input file in videosAddResumableValidator.', { err }) |
115 | res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) | 120 | res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) |
116 | .json({ error: 'Video file unreadable.' }) | 121 | .json({ error: 'Video file unreadable.' }) |
117 | 122 | ||
118 | return cleanUpReqFiles(req) | 123 | return cleanup() |
119 | } | 124 | } |
120 | 125 | ||
121 | videoFile.duration = duration | 126 | if (!await isVideoAccepted(req, res, file)) return cleanup() |
122 | 127 | ||
123 | if (!await isVideoAccepted(req, res, videoFile)) return cleanUpReqFiles(req) | 128 | res.locals.videoFileResumable = file |
129 | |||
130 | return next() | ||
131 | } | ||
132 | ] | ||
133 | |||
134 | /** | ||
135 | * File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use. | ||
136 | * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts | ||
137 | * | ||
138 | * Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx | ||
139 | * see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts | ||
140 | * | ||
141 | */ | ||
142 | const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | ||
143 | body('filename') | ||
144 | .isString() | ||
145 | .exists() | ||
146 | .withMessage('Should have a valid filename'), | ||
147 | body('name') | ||
148 | .trim() | ||
149 | .custom(isVideoNameValid) | ||
150 | .withMessage('Should have a valid name'), | ||
151 | body('channelId') | ||
152 | .customSanitizer(toIntOrNull) | ||
153 | .custom(isIdValid).withMessage('Should have correct video channel id'), | ||
154 | |||
155 | header('x-upload-content-length') | ||
156 | .isNumeric() | ||
157 | .exists() | ||
158 | .withMessage('Should specify the file length'), | ||
159 | header('x-upload-content-type') | ||
160 | .isString() | ||
161 | .exists() | ||
162 | .withMessage('Should specify the file mimetype'), | ||
163 | |||
164 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
165 | const videoFileMetadata = { | ||
166 | mimetype: req.headers['x-upload-content-type'] as string, | ||
167 | size: +req.headers['x-upload-content-length'], | ||
168 | originalname: req.body.name | ||
169 | } | ||
170 | |||
171 | const user = res.locals.oauth.token.User | ||
172 | const cleanup = () => cleanUpReqFiles(req) | ||
173 | |||
174 | logger.debug('Checking videosAddResumableInitValidator parameters and headers', { | ||
175 | parameters: req.body, | ||
176 | headers: req.headers, | ||
177 | files: req.files | ||
178 | }) | ||
179 | |||
180 | if (areValidationErrors(req, res)) return cleanup() | ||
181 | |||
182 | const files = { videofile: [ videoFileMetadata ] } | ||
183 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() | ||
184 | |||
185 | // multer required unsetting the Content-Type, now we can set it for node-uploadx | ||
186 | req.headers['content-type'] = 'application/json; charset=utf-8' | ||
187 | // place previewfile in metadata so that uploadx saves it in .META | ||
188 | if (req.files['previewfile']) req.body.previewfile = req.files['previewfile'] | ||
124 | 189 | ||
125 | return next() | 190 | return next() |
126 | } | 191 | } |
@@ -478,7 +543,10 @@ const commonVideosFiltersValidator = [ | |||
478 | // --------------------------------------------------------------------------- | 543 | // --------------------------------------------------------------------------- |
479 | 544 | ||
480 | export { | 545 | export { |
481 | videosAddValidator, | 546 | videosAddLegacyValidator, |
547 | videosAddResumableValidator, | ||
548 | videosAddResumableInitValidator, | ||
549 | |||
482 | videosUpdateValidator, | 550 | videosUpdateValidator, |
483 | videosGetValidator, | 551 | videosGetValidator, |
484 | videoFileMetadataGetValidator, | 552 | videoFileMetadataGetValidator, |
@@ -515,7 +583,51 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) | |||
515 | return false | 583 | return false |
516 | } | 584 | } |
517 | 585 | ||
518 | async function isVideoAccepted (req: express.Request, res: express.Response, videoFile: Express.Multer.File & { duration?: number }) { | 586 | async function commonVideoChecksPass (parameters: { |
587 | req: express.Request | ||
588 | res: express.Response | ||
589 | user: MUserAccountId | ||
590 | videoFileSize: number | ||
591 | files: express.UploadFilesForCheck | ||
592 | }): Promise<boolean> { | ||
593 | const { req, res, user, videoFileSize, files } = parameters | ||
594 | |||
595 | if (areErrorsInScheduleUpdate(req, res)) return false | ||
596 | |||
597 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false | ||
598 | |||
599 | if (!isVideoFileMimeTypeValid(files)) { | ||
600 | res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) | ||
601 | .json({ | ||
602 | error: 'This file is not supported. Please, make sure it is of the following type: ' + | ||
603 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | ||
604 | }) | ||
605 | |||
606 | return false | ||
607 | } | ||
608 | |||
609 | if (!isVideoFileSizeValid(videoFileSize.toString())) { | ||
610 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
611 | .json({ error: 'This file is too large. It exceeds the maximum file size authorized.' }) | ||
612 | |||
613 | return false | ||
614 | } | ||
615 | |||
616 | if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { | ||
617 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
618 | .json({ error: 'The user video quota is exceeded with this video.' }) | ||
619 | |||
620 | return false | ||
621 | } | ||
622 | |||
623 | return true | ||
624 | } | ||
625 | |||
626 | export async function isVideoAccepted ( | ||
627 | req: express.Request, | ||
628 | res: express.Response, | ||
629 | videoFile: express.VideoUploadFile | ||
630 | ) { | ||
519 | // Check we accept this video | 631 | // Check we accept this video |
520 | const acceptParameters = { | 632 | const acceptParameters = { |
521 | videoBody: req.body, | 633 | videoBody: req.body, |
@@ -538,3 +650,11 @@ async function isVideoAccepted (req: express.Request, res: express.Response, vid | |||
538 | 650 | ||
539 | return true | 651 | return true |
540 | } | 652 | } |
653 | |||
654 | async function addDurationToVideo (videoFile: { path: string, duration?: number }) { | ||
655 | const duration: number = await getDurationFromVideoFile(videoFile.path) | ||
656 | |||
657 | if (isNaN(duration)) throw new Error(`Couldn't get video duration`) | ||
658 | |||
659 | videoFile.duration = duration | ||
660 | } | ||
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index d0b0b9c21..143515838 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -13,6 +13,7 @@ import './plugins' | |||
13 | import './redundancy' | 13 | import './redundancy' |
14 | import './search' | 14 | import './search' |
15 | import './services' | 15 | import './services' |
16 | import './upload-quota' | ||
16 | import './user-notifications' | 17 | import './user-notifications' |
17 | import './user-subscriptions' | 18 | import './user-subscriptions' |
18 | import './users' | 19 | import './users' |
diff --git a/server/tests/api/check-params/upload-quota.ts b/server/tests/api/check-params/upload-quota.ts new file mode 100644 index 000000000..d0fbec415 --- /dev/null +++ b/server/tests/api/check-params/upload-quota.ts | |||
@@ -0,0 +1,152 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { expect } from 'chai' | ||
5 | import { HttpStatusCode, randomInt } from '@shared/core-utils' | ||
6 | import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '@shared/extra-utils/videos/video-imports' | ||
7 | import { MyUser, VideoImport, VideoImportState, VideoPrivacy } from '@shared/models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | flushAndRunServer, | ||
11 | getMyUserInformation, | ||
12 | immutableAssign, | ||
13 | registerUser, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, | ||
16 | setDefaultVideoChannel, | ||
17 | updateUser, | ||
18 | uploadVideo, | ||
19 | userLogin, | ||
20 | waitJobs | ||
21 | } from '../../../../shared/extra-utils' | ||
22 | |||
23 | describe('Test upload quota', function () { | ||
24 | let server: ServerInfo | ||
25 | let rootId: number | ||
26 | |||
27 | // --------------------------------------------------------------- | ||
28 | |||
29 | before(async function () { | ||
30 | this.timeout(30000) | ||
31 | |||
32 | server = await flushAndRunServer(1) | ||
33 | await setAccessTokensToServers([ server ]) | ||
34 | await setDefaultVideoChannel([ server ]) | ||
35 | |||
36 | const res = await getMyUserInformation(server.url, server.accessToken) | ||
37 | rootId = (res.body as MyUser).id | ||
38 | |||
39 | await updateUser({ | ||
40 | url: server.url, | ||
41 | userId: rootId, | ||
42 | accessToken: server.accessToken, | ||
43 | videoQuota: 42 | ||
44 | }) | ||
45 | }) | ||
46 | |||
47 | describe('When having a video quota', function () { | ||
48 | |||
49 | it('Should fail with a registered user having too many videos with legacy upload', async function () { | ||
50 | this.timeout(30000) | ||
51 | |||
52 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } | ||
53 | await registerUser(server.url, user.username, user.password) | ||
54 | const userAccessToken = await userLogin(server, user) | ||
55 | |||
56 | const videoAttributes = { fixture: 'video_short2.webm' } | ||
57 | for (let i = 0; i < 5; i++) { | ||
58 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
59 | } | ||
60 | |||
61 | await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy') | ||
62 | }) | ||
63 | |||
64 | it('Should fail with a registered user having too many videos with resumable upload', async function () { | ||
65 | this.timeout(30000) | ||
66 | |||
67 | const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } | ||
68 | await registerUser(server.url, user.username, user.password) | ||
69 | const userAccessToken = await userLogin(server, user) | ||
70 | |||
71 | const videoAttributes = { fixture: 'video_short2.webm' } | ||
72 | for (let i = 0; i < 5; i++) { | ||
73 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
74 | } | ||
75 | |||
76 | await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable') | ||
77 | }) | ||
78 | |||
79 | it('Should fail to import with HTTP/Torrent/magnet', async function () { | ||
80 | this.timeout(120000) | ||
81 | |||
82 | const baseAttributes = { | ||
83 | channelId: server.videoChannel.id, | ||
84 | privacy: VideoPrivacy.PUBLIC | ||
85 | } | ||
86 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { targetUrl: getGoodVideoUrl() })) | ||
87 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() })) | ||
88 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' as any })) | ||
89 | |||
90 | await waitJobs([ server ]) | ||
91 | |||
92 | const res = await getMyVideoImports(server.url, server.accessToken) | ||
93 | |||
94 | expect(res.body.total).to.equal(3) | ||
95 | const videoImports: VideoImport[] = res.body.data | ||
96 | expect(videoImports).to.have.lengthOf(3) | ||
97 | |||
98 | for (const videoImport of videoImports) { | ||
99 | expect(videoImport.state.id).to.equal(VideoImportState.FAILED) | ||
100 | expect(videoImport.error).not.to.be.undefined | ||
101 | expect(videoImport.error).to.contain('user video quota is exceeded') | ||
102 | } | ||
103 | }) | ||
104 | }) | ||
105 | |||
106 | describe('When having a daily video quota', function () { | ||
107 | |||
108 | it('Should fail with a user having too many videos daily', async function () { | ||
109 | await updateUser({ | ||
110 | url: server.url, | ||
111 | userId: rootId, | ||
112 | accessToken: server.accessToken, | ||
113 | videoQuotaDaily: 42 | ||
114 | }) | ||
115 | |||
116 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy') | ||
117 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable') | ||
118 | }) | ||
119 | }) | ||
120 | |||
121 | describe('When having an absolute and daily video quota', function () { | ||
122 | it('Should fail if exceeding total quota', async function () { | ||
123 | await updateUser({ | ||
124 | url: server.url, | ||
125 | userId: rootId, | ||
126 | accessToken: server.accessToken, | ||
127 | videoQuota: 42, | ||
128 | videoQuotaDaily: 1024 * 1024 * 1024 | ||
129 | }) | ||
130 | |||
131 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy') | ||
132 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable') | ||
133 | }) | ||
134 | |||
135 | it('Should fail if exceeding daily quota', async function () { | ||
136 | await updateUser({ | ||
137 | url: server.url, | ||
138 | userId: rootId, | ||
139 | accessToken: server.accessToken, | ||
140 | videoQuota: 1024 * 1024 * 1024, | ||
141 | videoQuotaDaily: 42 | ||
142 | }) | ||
143 | |||
144 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'legacy') | ||
145 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413, 'resumable') | ||
146 | }) | ||
147 | }) | ||
148 | |||
149 | after(async function () { | ||
150 | await cleanupTests([ server ]) | ||
151 | }) | ||
152 | }) | ||
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 2b03fde2d..dcff0d52b 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { expect } from 'chai' | ||
5 | import { omit } from 'lodash' | 4 | import { omit } from 'lodash' |
6 | import { join } from 'path' | 5 | import { join } from 'path' |
7 | import { User, UserRole, VideoImport, VideoImportState } from '../../../../shared' | 6 | import { User, UserRole } from '../../../../shared' |
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
8 | import { | 8 | import { |
9 | addVideoChannel, | 9 | addVideoChannel, |
10 | blockUser, | 10 | blockUser, |
@@ -29,7 +29,6 @@ import { | |||
29 | ServerInfo, | 29 | ServerInfo, |
30 | setAccessTokensToServers, | 30 | setAccessTokensToServers, |
31 | unblockUser, | 31 | unblockUser, |
32 | updateUser, | ||
33 | uploadVideo, | 32 | uploadVideo, |
34 | userLogin | 33 | userLogin |
35 | } from '../../../../shared/extra-utils' | 34 | } from '../../../../shared/extra-utils' |
@@ -39,11 +38,7 @@ import { | |||
39 | checkBadSortPagination, | 38 | checkBadSortPagination, |
40 | checkBadStartPagination | 39 | checkBadStartPagination |
41 | } from '../../../../shared/extra-utils/requests/check-api-params' | 40 | } from '../../../../shared/extra-utils/requests/check-api-params' |
42 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
43 | import { getGoodVideoUrl, getMagnetURI, getMyVideoImports, importVideo } from '../../../../shared/extra-utils/videos/video-imports' | ||
44 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' | 41 | import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' |
45 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
46 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
47 | 42 | ||
48 | describe('Test users API validators', function () { | 43 | describe('Test users API validators', function () { |
49 | const path = '/api/v1/users/' | 44 | const path = '/api/v1/users/' |
@@ -1093,102 +1088,6 @@ describe('Test users API validators', function () { | |||
1093 | }) | 1088 | }) |
1094 | }) | 1089 | }) |
1095 | 1090 | ||
1096 | describe('When having a video quota', function () { | ||
1097 | it('Should fail with a user having too many videos', async function () { | ||
1098 | await updateUser({ | ||
1099 | url: server.url, | ||
1100 | userId: rootId, | ||
1101 | accessToken: server.accessToken, | ||
1102 | videoQuota: 42 | ||
1103 | }) | ||
1104 | |||
1105 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1106 | }) | ||
1107 | |||
1108 | it('Should fail with a registered user having too many videos', async function () { | ||
1109 | this.timeout(30000) | ||
1110 | |||
1111 | const user = { | ||
1112 | username: 'user3', | ||
1113 | password: 'my super password' | ||
1114 | } | ||
1115 | userAccessToken = await userLogin(server, user) | ||
1116 | |||
1117 | const videoAttributes = { fixture: 'video_short2.webm' } | ||
1118 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1119 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1120 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1121 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1122 | await uploadVideo(server.url, userAccessToken, videoAttributes) | ||
1123 | await uploadVideo(server.url, userAccessToken, videoAttributes, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1124 | }) | ||
1125 | |||
1126 | it('Should fail to import with HTTP/Torrent/magnet', async function () { | ||
1127 | this.timeout(120000) | ||
1128 | |||
1129 | const baseAttributes = { | ||
1130 | channelId: 1, | ||
1131 | privacy: VideoPrivacy.PUBLIC | ||
1132 | } | ||
1133 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { targetUrl: getGoodVideoUrl() })) | ||
1134 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() })) | ||
1135 | await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' as any })) | ||
1136 | |||
1137 | await waitJobs([ server ]) | ||
1138 | |||
1139 | const res = await getMyVideoImports(server.url, server.accessToken) | ||
1140 | |||
1141 | expect(res.body.total).to.equal(3) | ||
1142 | const videoImports: VideoImport[] = res.body.data | ||
1143 | expect(videoImports).to.have.lengthOf(3) | ||
1144 | |||
1145 | for (const videoImport of videoImports) { | ||
1146 | expect(videoImport.state.id).to.equal(VideoImportState.FAILED) | ||
1147 | expect(videoImport.error).not.to.be.undefined | ||
1148 | expect(videoImport.error).to.contain('user video quota is exceeded') | ||
1149 | } | ||
1150 | }) | ||
1151 | }) | ||
1152 | |||
1153 | describe('When having a daily video quota', function () { | ||
1154 | it('Should fail with a user having too many videos daily', async function () { | ||
1155 | await updateUser({ | ||
1156 | url: server.url, | ||
1157 | userId: rootId, | ||
1158 | accessToken: server.accessToken, | ||
1159 | videoQuotaDaily: 42 | ||
1160 | }) | ||
1161 | |||
1162 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1163 | }) | ||
1164 | }) | ||
1165 | |||
1166 | describe('When having an absolute and daily video quota', function () { | ||
1167 | it('Should fail if exceeding total quota', async function () { | ||
1168 | await updateUser({ | ||
1169 | url: server.url, | ||
1170 | userId: rootId, | ||
1171 | accessToken: server.accessToken, | ||
1172 | videoQuota: 42, | ||
1173 | videoQuotaDaily: 1024 * 1024 * 1024 | ||
1174 | }) | ||
1175 | |||
1176 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1177 | }) | ||
1178 | |||
1179 | it('Should fail if exceeding daily quota', async function () { | ||
1180 | await updateUser({ | ||
1181 | url: server.url, | ||
1182 | userId: rootId, | ||
1183 | accessToken: server.accessToken, | ||
1184 | videoQuota: 1024 * 1024 * 1024, | ||
1185 | videoQuotaDaily: 42 | ||
1186 | }) | ||
1187 | |||
1188 | await uploadVideo(server.url, server.accessToken, {}, HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
1189 | }) | ||
1190 | }) | ||
1191 | |||
1192 | describe('When asking a password reset', function () { | 1091 | describe('When asking a password reset', function () { |
1193 | const path = '/api/v1/users/ask-reset-password' | 1092 | const path = '/api/v1/users/ask-reset-password' |
1194 | 1093 | ||
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 188d1835c..c970c4a15 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | ||
3 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
4 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
5 | import 'mocha' | ||
6 | import { join } from 'path' | 6 | import { join } from 'path' |
7 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' | 7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
8 | import { | 8 | import { |
9 | checkUploadVideoParam, | ||
9 | cleanupTests, | 10 | cleanupTests, |
10 | createUser, | 11 | createUser, |
11 | flushAndRunServer, | 12 | flushAndRunServer, |
@@ -18,17 +19,18 @@ import { | |||
18 | makePutBodyRequest, | 19 | makePutBodyRequest, |
19 | makeUploadRequest, | 20 | makeUploadRequest, |
20 | removeVideo, | 21 | removeVideo, |
22 | root, | ||
21 | ServerInfo, | 23 | ServerInfo, |
22 | setAccessTokensToServers, | 24 | setAccessTokensToServers, |
23 | userLogin, | 25 | userLogin |
24 | root | ||
25 | } from '../../../../shared/extra-utils' | 26 | } from '../../../../shared/extra-utils' |
26 | import { | 27 | import { |
27 | checkBadCountPagination, | 28 | checkBadCountPagination, |
28 | checkBadSortPagination, | 29 | checkBadSortPagination, |
29 | checkBadStartPagination | 30 | checkBadStartPagination |
30 | } from '../../../../shared/extra-utils/requests/check-api-params' | 31 | } from '../../../../shared/extra-utils/requests/check-api-params' |
31 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 32 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' |
33 | import { randomInt } from '@shared/core-utils' | ||
32 | 34 | ||
33 | const expect = chai.expect | 35 | const expect = chai.expect |
34 | 36 | ||
@@ -183,7 +185,7 @@ describe('Test videos API validator', function () { | |||
183 | describe('When adding a video', function () { | 185 | describe('When adding a video', function () { |
184 | let baseCorrectParams | 186 | let baseCorrectParams |
185 | const baseCorrectAttaches = { | 187 | const baseCorrectAttaches = { |
186 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm') | 188 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm') |
187 | } | 189 | } |
188 | 190 | ||
189 | before(function () { | 191 | before(function () { |
@@ -206,256 +208,243 @@ describe('Test videos API validator', function () { | |||
206 | } | 208 | } |
207 | }) | 209 | }) |
208 | 210 | ||
209 | it('Should fail with nothing', async function () { | 211 | function runSuite (mode: 'legacy' | 'resumable') { |
210 | const fields = {} | ||
211 | const attaches = {} | ||
212 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
213 | }) | ||
214 | 212 | ||
215 | it('Should fail without name', async function () { | 213 | it('Should fail with nothing', async function () { |
216 | const fields = omit(baseCorrectParams, 'name') | 214 | const fields = {} |
217 | const attaches = baseCorrectAttaches | 215 | const attaches = {} |
216 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) | ||
217 | }) | ||
218 | 218 | ||
219 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 219 | it('Should fail without name', async function () { |
220 | }) | 220 | const fields = omit(baseCorrectParams, 'name') |
221 | const attaches = baseCorrectAttaches | ||
221 | 222 | ||
222 | it('Should fail with a long name', async function () { | 223 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
223 | const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) }) | 224 | }) |
224 | const attaches = baseCorrectAttaches | ||
225 | 225 | ||
226 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 226 | it('Should fail with a long name', async function () { |
227 | }) | 227 | const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) }) |
228 | const attaches = baseCorrectAttaches | ||
228 | 229 | ||
229 | it('Should fail with a bad category', async function () { | 230 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
230 | const fields = immutableAssign(baseCorrectParams, { category: 125 }) | 231 | }) |
231 | const attaches = baseCorrectAttaches | ||
232 | 232 | ||
233 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 233 | it('Should fail with a bad category', async function () { |
234 | }) | 234 | const fields = immutableAssign(baseCorrectParams, { category: 125 }) |
235 | const attaches = baseCorrectAttaches | ||
235 | 236 | ||
236 | it('Should fail with a bad licence', async function () { | 237 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
237 | const fields = immutableAssign(baseCorrectParams, { licence: 125 }) | 238 | }) |
238 | const attaches = baseCorrectAttaches | ||
239 | 239 | ||
240 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 240 | it('Should fail with a bad licence', async function () { |
241 | }) | 241 | const fields = immutableAssign(baseCorrectParams, { licence: 125 }) |
242 | const attaches = baseCorrectAttaches | ||
242 | 243 | ||
243 | it('Should fail with a bad language', async function () { | 244 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
244 | const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) }) | 245 | }) |
245 | const attaches = baseCorrectAttaches | ||
246 | 246 | ||
247 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 247 | it('Should fail with a bad language', async function () { |
248 | }) | 248 | const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) }) |
249 | const attaches = baseCorrectAttaches | ||
249 | 250 | ||
250 | it('Should fail with a long description', async function () { | 251 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
251 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) | 252 | }) |
252 | const attaches = baseCorrectAttaches | ||
253 | 253 | ||
254 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 254 | it('Should fail with a long description', async function () { |
255 | }) | 255 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) |
256 | const attaches = baseCorrectAttaches | ||
256 | 257 | ||
257 | it('Should fail with a long support text', async function () { | 258 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
258 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) | 259 | }) |
259 | const attaches = baseCorrectAttaches | ||
260 | 260 | ||
261 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 261 | it('Should fail with a long support text', async function () { |
262 | }) | 262 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
263 | const attaches = baseCorrectAttaches | ||
263 | 264 | ||
264 | it('Should fail without a channel', async function () { | 265 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
265 | const fields = omit(baseCorrectParams, 'channelId') | 266 | }) |
266 | const attaches = baseCorrectAttaches | ||
267 | 267 | ||
268 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 268 | it('Should fail without a channel', async function () { |
269 | }) | 269 | const fields = omit(baseCorrectParams, 'channelId') |
270 | const attaches = baseCorrectAttaches | ||
270 | 271 | ||
271 | it('Should fail with a bad channel', async function () { | 272 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
272 | const fields = immutableAssign(baseCorrectParams, { channelId: 545454 }) | 273 | }) |
273 | const attaches = baseCorrectAttaches | ||
274 | 274 | ||
275 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 275 | it('Should fail with a bad channel', async function () { |
276 | }) | 276 | const fields = immutableAssign(baseCorrectParams, { channelId: 545454 }) |
277 | const attaches = baseCorrectAttaches | ||
277 | 278 | ||
278 | it('Should fail with another user channel', async function () { | 279 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
279 | const user = { | 280 | }) |
280 | username: 'fake', | ||
281 | password: 'fake_password' | ||
282 | } | ||
283 | await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password }) | ||
284 | 281 | ||
285 | const accessTokenUser = await userLogin(server, user) | 282 | it('Should fail with another user channel', async function () { |
286 | const res = await getMyUserInformation(server.url, accessTokenUser) | 283 | const user = { |
287 | const customChannelId = res.body.videoChannels[0].id | 284 | username: 'fake' + randomInt(0, 1500), |
285 | password: 'fake_password' | ||
286 | } | ||
287 | await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password }) | ||
288 | 288 | ||
289 | const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId }) | 289 | const accessTokenUser = await userLogin(server, user) |
290 | const attaches = baseCorrectAttaches | 290 | const res = await getMyUserInformation(server.url, accessTokenUser) |
291 | const customChannelId = res.body.videoChannels[0].id | ||
291 | 292 | ||
292 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: userAccessToken, fields, attaches }) | 293 | const fields = immutableAssign(baseCorrectParams, { channelId: customChannelId }) |
293 | }) | 294 | const attaches = baseCorrectAttaches |
294 | 295 | ||
295 | it('Should fail with too many tags', async function () { | 296 | await checkUploadVideoParam(server.url, userAccessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
296 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }) | 297 | }) |
297 | const attaches = baseCorrectAttaches | ||
298 | 298 | ||
299 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 299 | it('Should fail with too many tags', async function () { |
300 | }) | 300 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }) |
301 | const attaches = baseCorrectAttaches | ||
301 | 302 | ||
302 | it('Should fail with a tag length too low', async function () { | 303 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
303 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] }) | 304 | }) |
304 | const attaches = baseCorrectAttaches | ||
305 | 305 | ||
306 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 306 | it('Should fail with a tag length too low', async function () { |
307 | }) | 307 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] }) |
308 | const attaches = baseCorrectAttaches | ||
308 | 309 | ||
309 | it('Should fail with a tag length too big', async function () { | 310 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
310 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }) | 311 | }) |
311 | const attaches = baseCorrectAttaches | ||
312 | 312 | ||
313 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 313 | it('Should fail with a tag length too big', async function () { |
314 | }) | 314 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }) |
315 | const attaches = baseCorrectAttaches | ||
315 | 316 | ||
316 | it('Should fail with a bad schedule update (miss updateAt)', async function () { | 317 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
317 | const fields = immutableAssign(baseCorrectParams, { 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC }) | 318 | }) |
318 | const attaches = baseCorrectAttaches | ||
319 | 319 | ||
320 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 320 | it('Should fail with a bad schedule update (miss updateAt)', async function () { |
321 | }) | 321 | const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }) |
322 | const attaches = baseCorrectAttaches | ||
322 | 323 | ||
323 | it('Should fail with a bad schedule update (wrong updateAt)', async function () { | 324 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
324 | const fields = immutableAssign(baseCorrectParams, { | ||
325 | 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC, | ||
326 | 'scheduleUpdate[updateAt]': 'toto' | ||
327 | }) | 325 | }) |
328 | const attaches = baseCorrectAttaches | ||
329 | 326 | ||
330 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 327 | it('Should fail with a bad schedule update (wrong updateAt)', async function () { |
331 | }) | 328 | const fields = immutableAssign(baseCorrectParams, { |
329 | scheduleUpdate: { | ||
330 | privacy: VideoPrivacy.PUBLIC, | ||
331 | updateAt: 'toto' | ||
332 | } | ||
333 | }) | ||
334 | const attaches = baseCorrectAttaches | ||
332 | 335 | ||
333 | it('Should fail with a bad originally published at attribute', async function () { | 336 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
334 | const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' }) | 337 | }) |
335 | const attaches = baseCorrectAttaches | ||
336 | 338 | ||
337 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 339 | it('Should fail with a bad originally published at attribute', async function () { |
338 | }) | 340 | const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' }) |
341 | const attaches = baseCorrectAttaches | ||
339 | 342 | ||
340 | it('Should fail without an input file', async function () { | 343 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
341 | const fields = baseCorrectParams | 344 | }) |
342 | const attaches = {} | ||
343 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | ||
344 | }) | ||
345 | 345 | ||
346 | it('Should fail with an incorrect input file', async function () { | 346 | it('Should fail without an input file', async function () { |
347 | const fields = baseCorrectParams | 347 | const fields = baseCorrectParams |
348 | let attaches = { | 348 | const attaches = {} |
349 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm') | 349 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
350 | } | ||
351 | await makeUploadRequest({ | ||
352 | url: server.url, | ||
353 | path: path + '/upload', | ||
354 | token: server.accessToken, | ||
355 | fields, | ||
356 | attaches, | ||
357 | statusCodeExpected: HttpStatusCode.UNPROCESSABLE_ENTITY_422 | ||
358 | }) | 350 | }) |
359 | 351 | ||
360 | attaches = { | 352 | it('Should fail with an incorrect input file', async function () { |
361 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv') | 353 | const fields = baseCorrectParams |
362 | } | 354 | let attaches = { fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm') } |
363 | await makeUploadRequest({ | 355 | |
364 | url: server.url, | 356 | await checkUploadVideoParam( |
365 | path: path + '/upload', | 357 | server.url, |
366 | token: server.accessToken, | 358 | server.accessToken, |
367 | fields, | 359 | { ...fields, ...attaches }, |
368 | attaches, | 360 | HttpStatusCode.UNPROCESSABLE_ENTITY_422, |
369 | statusCodeExpected: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 | 361 | mode |
362 | ) | ||
363 | |||
364 | attaches = { fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv') } | ||
365 | await checkUploadVideoParam( | ||
366 | server.url, | ||
367 | server.accessToken, | ||
368 | { ...fields, ...attaches }, | ||
369 | HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415, | ||
370 | mode | ||
371 | ) | ||
370 | }) | 372 | }) |
371 | }) | ||
372 | 373 | ||
373 | it('Should fail with an incorrect thumbnail file', async function () { | 374 | it('Should fail with an incorrect thumbnail file', async function () { |
374 | const fields = baseCorrectParams | 375 | const fields = baseCorrectParams |
375 | const attaches = { | 376 | const attaches = { |
376 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'), | 377 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'), |
377 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 378 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
378 | } | 379 | } |
379 | 380 | ||
380 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 381 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
381 | }) | 382 | }) |
382 | 383 | ||
383 | it('Should fail with a big thumbnail file', async function () { | 384 | it('Should fail with a big thumbnail file', async function () { |
384 | const fields = baseCorrectParams | 385 | const fields = baseCorrectParams |
385 | const attaches = { | 386 | const attaches = { |
386 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), | 387 | thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), |
387 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 388 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
388 | } | 389 | } |
389 | 390 | ||
390 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 391 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
391 | }) | 392 | }) |
392 | 393 | ||
393 | it('Should fail with an incorrect preview file', async function () { | 394 | it('Should fail with an incorrect preview file', async function () { |
394 | const fields = baseCorrectParams | 395 | const fields = baseCorrectParams |
395 | const attaches = { | 396 | const attaches = { |
396 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'), | 397 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4'), |
397 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 398 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
398 | } | 399 | } |
399 | 400 | ||
400 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 401 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
401 | }) | 402 | }) |
402 | 403 | ||
403 | it('Should fail with a big preview file', async function () { | 404 | it('Should fail with a big preview file', async function () { |
404 | const fields = baseCorrectParams | 405 | const fields = baseCorrectParams |
405 | const attaches = { | 406 | const attaches = { |
406 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), | 407 | previewfile: join(root(), 'server', 'tests', 'fixtures', 'preview-big.png'), |
407 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 408 | fixture: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
408 | } | 409 | } |
409 | 410 | ||
410 | await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) | 411 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
411 | }) | 412 | }) |
412 | 413 | ||
413 | it('Should succeed with the correct parameters', async function () { | 414 | it('Should succeed with the correct parameters', async function () { |
414 | this.timeout(10000) | 415 | this.timeout(10000) |
415 | 416 | ||
416 | const fields = baseCorrectParams | 417 | const fields = baseCorrectParams |
417 | 418 | ||
418 | { | 419 | { |
419 | const attaches = baseCorrectAttaches | 420 | const attaches = baseCorrectAttaches |
420 | await makeUploadRequest({ | 421 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode) |
421 | url: server.url, | 422 | } |
422 | path: path + '/upload', | ||
423 | token: server.accessToken, | ||
424 | fields, | ||
425 | attaches, | ||
426 | statusCodeExpected: HttpStatusCode.OK_200 | ||
427 | }) | ||
428 | } | ||
429 | 423 | ||
430 | { | 424 | { |
431 | const attaches = immutableAssign(baseCorrectAttaches, { | 425 | const attaches = immutableAssign(baseCorrectAttaches, { |
432 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') | 426 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') |
433 | }) | 427 | }) |
434 | 428 | ||
435 | await makeUploadRequest({ | 429 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode) |
436 | url: server.url, | 430 | } |
437 | path: path + '/upload', | ||
438 | token: server.accessToken, | ||
439 | fields, | ||
440 | attaches, | ||
441 | statusCodeExpected: HttpStatusCode.OK_200 | ||
442 | }) | ||
443 | } | ||
444 | 431 | ||
445 | { | 432 | { |
446 | const attaches = immutableAssign(baseCorrectAttaches, { | 433 | const attaches = immutableAssign(baseCorrectAttaches, { |
447 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.ogv') | 434 | videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.ogv') |
448 | }) | 435 | }) |
449 | 436 | ||
450 | await makeUploadRequest({ | 437 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.OK_200, mode) |
451 | url: server.url, | 438 | } |
452 | path: path + '/upload', | 439 | }) |
453 | token: server.accessToken, | 440 | } |
454 | fields, | 441 | |
455 | attaches, | 442 | describe('Resumable upload', function () { |
456 | statusCodeExpected: HttpStatusCode.OK_200 | 443 | runSuite('resumable') |
457 | }) | 444 | }) |
458 | } | 445 | |
446 | describe('Legacy upload', function () { | ||
447 | runSuite('legacy') | ||
459 | }) | 448 | }) |
460 | }) | 449 | }) |
461 | 450 | ||
@@ -678,7 +667,7 @@ describe('Test videos API validator', function () { | |||
678 | }) | 667 | }) |
679 | 668 | ||
680 | expect(res.body.data).to.be.an('array') | 669 | expect(res.body.data).to.be.an('array') |
681 | expect(res.body.data.length).to.equal(3) | 670 | expect(res.body.data.length).to.equal(6) |
682 | }) | 671 | }) |
683 | 672 | ||
684 | it('Should fail without a correct uuid', async function () { | 673 | it('Should fail without a correct uuid', async function () { |
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts index fc8b447b7..5c07f8926 100644 --- a/server/tests/api/videos/index.ts +++ b/server/tests/api/videos/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import './audio-only' | 1 | import './audio-only' |
2 | import './multiple-servers' | 2 | import './multiple-servers' |
3 | import './resumable-upload' | ||
3 | import './single-server' | 4 | import './single-server' |
4 | import './video-captions' | 5 | import './video-captions' |
5 | import './video-change-ownership' | 6 | import './video-change-ownership' |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 55e280e9f..41cd814e0 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -181,7 +181,7 @@ describe('Test multiple servers', function () { | |||
181 | thumbnailfile: 'thumbnail.jpg', | 181 | thumbnailfile: 'thumbnail.jpg', |
182 | previewfile: 'preview.jpg' | 182 | previewfile: 'preview.jpg' |
183 | } | 183 | } |
184 | await uploadVideo(servers[1].url, userAccessToken, videoAttributes) | 184 | await uploadVideo(servers[1].url, userAccessToken, videoAttributes, HttpStatusCode.OK_200, 'resumable') |
185 | 185 | ||
186 | // Transcoding | 186 | // Transcoding |
187 | await waitJobs(servers) | 187 | await waitJobs(servers) |
diff --git a/server/tests/api/videos/resumable-upload.ts b/server/tests/api/videos/resumable-upload.ts new file mode 100644 index 000000000..af9221c43 --- /dev/null +++ b/server/tests/api/videos/resumable-upload.ts | |||
@@ -0,0 +1,187 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { pathExists, readdir, stat } from 'fs-extra' | ||
6 | import { join } from 'path' | ||
7 | import { HttpStatusCode } from '@shared/core-utils' | ||
8 | import { | ||
9 | buildAbsoluteFixturePath, | ||
10 | buildServerDirectory, | ||
11 | flushAndRunServer, | ||
12 | getMyUserInformation, | ||
13 | prepareResumableUpload, | ||
14 | sendDebugCommand, | ||
15 | sendResumableChunks, | ||
16 | ServerInfo, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultVideoChannel, | ||
19 | updateUser | ||
20 | } from '@shared/extra-utils' | ||
21 | import { MyUser, VideoPrivacy } from '@shared/models' | ||
22 | |||
23 | const expect = chai.expect | ||
24 | |||
25 | // Most classic resumable upload tests are done in other test suites | ||
26 | |||
27 | describe('Test resumable upload', function () { | ||
28 | const defaultFixture = 'video_short.mp4' | ||
29 | let server: ServerInfo | ||
30 | let rootId: number | ||
31 | |||
32 | async function buildSize (fixture: string, size?: number) { | ||
33 | if (size !== undefined) return size | ||
34 | |||
35 | const baseFixture = buildAbsoluteFixturePath(fixture) | ||
36 | return (await stat(baseFixture)).size | ||
37 | } | ||
38 | |||
39 | async function prepareUpload (sizeArg?: number) { | ||
40 | const size = await buildSize(defaultFixture, sizeArg) | ||
41 | |||
42 | const attributes = { | ||
43 | name: 'video', | ||
44 | channelId: server.videoChannel.id, | ||
45 | privacy: VideoPrivacy.PUBLIC, | ||
46 | fixture: defaultFixture | ||
47 | } | ||
48 | |||
49 | const mimetype = 'video/mp4' | ||
50 | |||
51 | const res = await prepareResumableUpload({ url: server.url, token: server.accessToken, attributes, size, mimetype }) | ||
52 | |||
53 | return res.header['location'].split('?')[1] | ||
54 | } | ||
55 | |||
56 | async function sendChunks (options: { | ||
57 | pathUploadId: string | ||
58 | size?: number | ||
59 | expectedStatus?: HttpStatusCode | ||
60 | contentLength?: number | ||
61 | contentRange?: string | ||
62 | contentRangeBuilder?: (start: number, chunk: any) => string | ||
63 | }) { | ||
64 | const { pathUploadId, expectedStatus, contentLength, contentRangeBuilder } = options | ||
65 | |||
66 | const size = await buildSize(defaultFixture, options.size) | ||
67 | const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture) | ||
68 | |||
69 | return sendResumableChunks({ | ||
70 | url: server.url, | ||
71 | token: server.accessToken, | ||
72 | pathUploadId, | ||
73 | videoFilePath: absoluteFilePath, | ||
74 | size, | ||
75 | contentLength, | ||
76 | contentRangeBuilder, | ||
77 | specialStatus: expectedStatus | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | async function checkFileSize (uploadIdArg: string, expectedSize: number | null) { | ||
82 | const uploadId = uploadIdArg.replace(/^upload_id=/, '') | ||
83 | |||
84 | const subPath = join('tmp', 'resumable-uploads', uploadId) | ||
85 | const filePath = buildServerDirectory(server, subPath) | ||
86 | const exists = await pathExists(filePath) | ||
87 | |||
88 | if (expectedSize === null) { | ||
89 | expect(exists).to.be.false | ||
90 | return | ||
91 | } | ||
92 | |||
93 | expect(exists).to.be.true | ||
94 | |||
95 | expect((await stat(filePath)).size).to.equal(expectedSize) | ||
96 | } | ||
97 | |||
98 | async function countResumableUploads () { | ||
99 | const subPath = join('tmp', 'resumable-uploads') | ||
100 | const filePath = buildServerDirectory(server, subPath) | ||
101 | |||
102 | const files = await readdir(filePath) | ||
103 | return files.length | ||
104 | } | ||
105 | |||
106 | before(async function () { | ||
107 | this.timeout(30000) | ||
108 | |||
109 | server = await flushAndRunServer(1) | ||
110 | await setAccessTokensToServers([ server ]) | ||
111 | await setDefaultVideoChannel([ server ]) | ||
112 | |||
113 | const res = await getMyUserInformation(server.url, server.accessToken) | ||
114 | rootId = (res.body as MyUser).id | ||
115 | |||
116 | await updateUser({ | ||
117 | url: server.url, | ||
118 | userId: rootId, | ||
119 | accessToken: server.accessToken, | ||
120 | videoQuota: 10_000_000 | ||
121 | }) | ||
122 | }) | ||
123 | |||
124 | describe('Directory cleaning', function () { | ||
125 | |||
126 | it('Should correctly delete files after an upload', async function () { | ||
127 | const uploadId = await prepareUpload() | ||
128 | await sendChunks({ pathUploadId: uploadId }) | ||
129 | |||
130 | expect(await countResumableUploads()).to.equal(0) | ||
131 | }) | ||
132 | |||
133 | it('Should not delete files after an unfinished upload', async function () { | ||
134 | await prepareUpload() | ||
135 | |||
136 | expect(await countResumableUploads()).to.equal(2) | ||
137 | }) | ||
138 | |||
139 | it('Should not delete recent uploads', async function () { | ||
140 | await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' }) | ||
141 | |||
142 | expect(await countResumableUploads()).to.equal(2) | ||
143 | }) | ||
144 | |||
145 | it('Should delete old uploads', async function () { | ||
146 | await sendDebugCommand(server.url, server.accessToken, { command: 'remove-dandling-resumable-uploads' }) | ||
147 | |||
148 | expect(await countResumableUploads()).to.equal(0) | ||
149 | }) | ||
150 | }) | ||
151 | |||
152 | describe('Resumable upload and chunks', function () { | ||
153 | |||
154 | it('Should accept the same amount of chunks', async function () { | ||
155 | const uploadId = await prepareUpload() | ||
156 | await sendChunks({ pathUploadId: uploadId }) | ||
157 | |||
158 | await checkFileSize(uploadId, null) | ||
159 | }) | ||
160 | |||
161 | it('Should not accept more chunks than expected', async function () { | ||
162 | const size = 100 | ||
163 | const uploadId = await prepareUpload(size) | ||
164 | |||
165 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 }) | ||
166 | await checkFileSize(uploadId, 0) | ||
167 | }) | ||
168 | |||
169 | it('Should not accept more chunks than expected with an invalid content length/content range', async function () { | ||
170 | const uploadId = await prepareUpload(1500) | ||
171 | |||
172 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 }) | ||
173 | await checkFileSize(uploadId, 0) | ||
174 | }) | ||
175 | |||
176 | it('Should not accept more chunks than expected with an invalid content length', async function () { | ||
177 | const uploadId = await prepareUpload(500) | ||
178 | |||
179 | const size = 1000 | ||
180 | |||
181 | const contentRangeBuilder = start => `bytes ${start}-${start + size - 1}/${size}` | ||
182 | await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentRangeBuilder, contentLength: size }) | ||
183 | await checkFileSize(uploadId, 0) | ||
184 | }) | ||
185 | }) | ||
186 | |||
187 | }) | ||
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index a79648bf7..1058a1e9c 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | ||
3 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
4 | import { keyBy } from 'lodash' | 5 | import { keyBy } from 'lodash' |
5 | import 'mocha' | 6 | |
6 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
7 | import { | 7 | import { |
8 | checkVideoFilesWereRemoved, | 8 | checkVideoFilesWereRemoved, |
9 | cleanupTests, | 9 | cleanupTests, |
@@ -28,430 +28,432 @@ import { | |||
28 | viewVideo, | 28 | viewVideo, |
29 | wait | 29 | wait |
30 | } from '../../../../shared/extra-utils' | 30 | } from '../../../../shared/extra-utils' |
31 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
32 | import { HttpStatusCode } from '@shared/core-utils' | ||
31 | 33 | ||
32 | const expect = chai.expect | 34 | const expect = chai.expect |
33 | 35 | ||
34 | describe('Test a single server', function () { | 36 | describe('Test a single server', function () { |
35 | let server: ServerInfo = null | ||
36 | let videoId = -1 | ||
37 | let videoId2 = -1 | ||
38 | let videoUUID = '' | ||
39 | let videosListBase: any[] = null | ||
40 | |||
41 | const getCheckAttributes = () => ({ | ||
42 | name: 'my super name', | ||
43 | category: 2, | ||
44 | licence: 6, | ||
45 | language: 'zh', | ||
46 | nsfw: true, | ||
47 | description: 'my super description', | ||
48 | support: 'my super support text', | ||
49 | account: { | ||
50 | name: 'root', | ||
51 | host: 'localhost:' + server.port | ||
52 | }, | ||
53 | isLocal: true, | ||
54 | duration: 5, | ||
55 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
56 | privacy: VideoPrivacy.PUBLIC, | ||
57 | commentsEnabled: true, | ||
58 | downloadEnabled: true, | ||
59 | channel: { | ||
60 | displayName: 'Main root channel', | ||
61 | name: 'root_channel', | ||
62 | description: '', | ||
63 | isLocal: true | ||
64 | }, | ||
65 | fixture: 'video_short.webm', | ||
66 | files: [ | ||
67 | { | ||
68 | resolution: 720, | ||
69 | size: 218910 | ||
70 | } | ||
71 | ] | ||
72 | }) | ||
73 | |||
74 | const updateCheckAttributes = () => ({ | ||
75 | name: 'my super video updated', | ||
76 | category: 4, | ||
77 | licence: 2, | ||
78 | language: 'ar', | ||
79 | nsfw: false, | ||
80 | description: 'my super description updated', | ||
81 | support: 'my super support text updated', | ||
82 | account: { | ||
83 | name: 'root', | ||
84 | host: 'localhost:' + server.port | ||
85 | }, | ||
86 | isLocal: true, | ||
87 | tags: [ 'tagup1', 'tagup2' ], | ||
88 | privacy: VideoPrivacy.PUBLIC, | ||
89 | duration: 5, | ||
90 | commentsEnabled: false, | ||
91 | downloadEnabled: false, | ||
92 | channel: { | ||
93 | name: 'root_channel', | ||
94 | displayName: 'Main root channel', | ||
95 | description: '', | ||
96 | isLocal: true | ||
97 | }, | ||
98 | fixture: 'video_short3.webm', | ||
99 | files: [ | ||
100 | { | ||
101 | resolution: 720, | ||
102 | size: 292677 | ||
103 | } | ||
104 | ] | ||
105 | }) | ||
106 | |||
107 | before(async function () { | ||
108 | this.timeout(30000) | ||
109 | |||
110 | server = await flushAndRunServer(1) | ||
111 | |||
112 | await setAccessTokensToServers([ server ]) | ||
113 | }) | ||
114 | |||
115 | it('Should list video categories', async function () { | ||
116 | const res = await getVideoCategories(server.url) | ||
117 | |||
118 | const categories = res.body | ||
119 | expect(Object.keys(categories)).to.have.length.above(10) | ||
120 | |||
121 | expect(categories[11]).to.equal('News & Politics') | ||
122 | }) | ||
123 | |||
124 | it('Should list video licences', async function () { | ||
125 | const res = await getVideoLicences(server.url) | ||
126 | |||
127 | const licences = res.body | ||
128 | expect(Object.keys(licences)).to.have.length.above(5) | ||
129 | |||
130 | expect(licences[3]).to.equal('Attribution - No Derivatives') | ||
131 | }) | ||
132 | |||
133 | it('Should list video languages', async function () { | ||
134 | const res = await getVideoLanguages(server.url) | ||
135 | |||
136 | const languages = res.body | ||
137 | expect(Object.keys(languages)).to.have.length.above(5) | ||
138 | |||
139 | expect(languages['ru']).to.equal('Russian') | ||
140 | }) | ||
141 | |||
142 | it('Should list video privacies', async function () { | ||
143 | const res = await getVideoPrivacies(server.url) | ||
144 | |||
145 | const privacies = res.body | ||
146 | expect(Object.keys(privacies)).to.have.length.at.least(3) | ||
147 | |||
148 | expect(privacies[3]).to.equal('Private') | ||
149 | }) | ||
150 | |||
151 | it('Should not have videos', async function () { | ||
152 | const res = await getVideosList(server.url) | ||
153 | |||
154 | expect(res.body.total).to.equal(0) | ||
155 | expect(res.body.data).to.be.an('array') | ||
156 | expect(res.body.data.length).to.equal(0) | ||
157 | }) | ||
158 | 37 | ||
159 | it('Should upload the video', async function () { | 38 | function runSuite (mode: 'legacy' | 'resumable') { |
160 | this.timeout(10000) | 39 | let server: ServerInfo = null |
40 | let videoId = -1 | ||
41 | let videoId2 = -1 | ||
42 | let videoUUID = '' | ||
43 | let videosListBase: any[] = null | ||
161 | 44 | ||
162 | const videoAttributes = { | 45 | const getCheckAttributes = () => ({ |
163 | name: 'my super name', | 46 | name: 'my super name', |
164 | category: 2, | 47 | category: 2, |
165 | nsfw: true, | ||
166 | licence: 6, | 48 | licence: 6, |
167 | tags: [ 'tag1', 'tag2', 'tag3' ] | 49 | language: 'zh', |
168 | } | 50 | nsfw: true, |
169 | const res = await uploadVideo(server.url, server.accessToken, videoAttributes) | 51 | description: 'my super description', |
170 | expect(res.body.video).to.not.be.undefined | 52 | support: 'my super support text', |
171 | expect(res.body.video.id).to.equal(1) | 53 | account: { |
172 | expect(res.body.video.uuid).to.have.length.above(5) | 54 | name: 'root', |
173 | 55 | host: 'localhost:' + server.port | |
174 | videoId = res.body.video.id | 56 | }, |
175 | videoUUID = res.body.video.uuid | 57 | isLocal: true, |
176 | }) | 58 | duration: 5, |
177 | 59 | tags: [ 'tag1', 'tag2', 'tag3' ], | |
178 | it('Should get and seed the uploaded video', async function () { | 60 | privacy: VideoPrivacy.PUBLIC, |
179 | this.timeout(5000) | 61 | commentsEnabled: true, |
180 | 62 | downloadEnabled: true, | |
181 | const res = await getVideosList(server.url) | 63 | channel: { |
182 | 64 | displayName: 'Main root channel', | |
183 | expect(res.body.total).to.equal(1) | 65 | name: 'root_channel', |
184 | expect(res.body.data).to.be.an('array') | 66 | description: '', |
185 | expect(res.body.data.length).to.equal(1) | 67 | isLocal: true |
186 | 68 | }, | |
187 | const video = res.body.data[0] | 69 | fixture: 'video_short.webm', |
188 | await completeVideoCheck(server.url, video, getCheckAttributes()) | 70 | files: [ |
189 | }) | 71 | { |
72 | resolution: 720, | ||
73 | size: 218910 | ||
74 | } | ||
75 | ] | ||
76 | }) | ||
77 | |||
78 | const updateCheckAttributes = () => ({ | ||
79 | name: 'my super video updated', | ||
80 | category: 4, | ||
81 | licence: 2, | ||
82 | language: 'ar', | ||
83 | nsfw: false, | ||
84 | description: 'my super description updated', | ||
85 | support: 'my super support text updated', | ||
86 | account: { | ||
87 | name: 'root', | ||
88 | host: 'localhost:' + server.port | ||
89 | }, | ||
90 | isLocal: true, | ||
91 | tags: [ 'tagup1', 'tagup2' ], | ||
92 | privacy: VideoPrivacy.PUBLIC, | ||
93 | duration: 5, | ||
94 | commentsEnabled: false, | ||
95 | downloadEnabled: false, | ||
96 | channel: { | ||
97 | name: 'root_channel', | ||
98 | displayName: 'Main root channel', | ||
99 | description: '', | ||
100 | isLocal: true | ||
101 | }, | ||
102 | fixture: 'video_short3.webm', | ||
103 | files: [ | ||
104 | { | ||
105 | resolution: 720, | ||
106 | size: 292677 | ||
107 | } | ||
108 | ] | ||
109 | }) | ||
190 | 110 | ||
191 | it('Should get the video by UUID', async function () { | 111 | before(async function () { |
192 | this.timeout(5000) | 112 | this.timeout(30000) |
193 | 113 | ||
194 | const res = await getVideo(server.url, videoUUID) | 114 | server = await flushAndRunServer(1) |
195 | 115 | ||
196 | const video = res.body | 116 | await setAccessTokensToServers([ server ]) |
197 | await completeVideoCheck(server.url, video, getCheckAttributes()) | 117 | }) |
198 | }) | ||
199 | 118 | ||
200 | it('Should have the views updated', async function () { | 119 | it('Should list video categories', async function () { |
201 | this.timeout(20000) | 120 | const res = await getVideoCategories(server.url) |
202 | 121 | ||
203 | await viewVideo(server.url, videoId) | 122 | const categories = res.body |
204 | await viewVideo(server.url, videoId) | 123 | expect(Object.keys(categories)).to.have.length.above(10) |
205 | await viewVideo(server.url, videoId) | ||
206 | 124 | ||
207 | await wait(1500) | 125 | expect(categories[11]).to.equal('News & Politics') |
126 | }) | ||
208 | 127 | ||
209 | await viewVideo(server.url, videoId) | 128 | it('Should list video licences', async function () { |
210 | await viewVideo(server.url, videoId) | 129 | const res = await getVideoLicences(server.url) |
211 | 130 | ||
212 | await wait(1500) | 131 | const licences = res.body |
132 | expect(Object.keys(licences)).to.have.length.above(5) | ||
213 | 133 | ||
214 | await viewVideo(server.url, videoId) | 134 | expect(licences[3]).to.equal('Attribution - No Derivatives') |
215 | await viewVideo(server.url, videoId) | 135 | }) |
216 | 136 | ||
217 | // Wait the repeatable job | 137 | it('Should list video languages', async function () { |
218 | await wait(8000) | 138 | const res = await getVideoLanguages(server.url) |
219 | 139 | ||
220 | const res = await getVideo(server.url, videoId) | 140 | const languages = res.body |
141 | expect(Object.keys(languages)).to.have.length.above(5) | ||
221 | 142 | ||
222 | const video = res.body | 143 | expect(languages['ru']).to.equal('Russian') |
223 | expect(video.views).to.equal(3) | 144 | }) |
224 | }) | ||
225 | 145 | ||
226 | it('Should remove the video', async function () { | 146 | it('Should list video privacies', async function () { |
227 | await removeVideo(server.url, server.accessToken, videoId) | 147 | const res = await getVideoPrivacies(server.url) |
228 | 148 | ||
229 | await checkVideoFilesWereRemoved(videoUUID, 1) | 149 | const privacies = res.body |
230 | }) | 150 | expect(Object.keys(privacies)).to.have.length.at.least(3) |
231 | 151 | ||
232 | it('Should not have videos', async function () { | 152 | expect(privacies[3]).to.equal('Private') |
233 | const res = await getVideosList(server.url) | 153 | }) |
234 | 154 | ||
235 | expect(res.body.total).to.equal(0) | 155 | it('Should not have videos', async function () { |
236 | expect(res.body.data).to.be.an('array') | 156 | const res = await getVideosList(server.url) |
237 | expect(res.body.data).to.have.lengthOf(0) | ||
238 | }) | ||
239 | 157 | ||
240 | it('Should upload 6 videos', async function () { | 158 | expect(res.body.total).to.equal(0) |
241 | this.timeout(25000) | 159 | expect(res.body.data).to.be.an('array') |
160 | expect(res.body.data.length).to.equal(0) | ||
161 | }) | ||
242 | 162 | ||
243 | const videos = new Set([ | 163 | it('Should upload the video', async function () { |
244 | 'video_short.mp4', 'video_short.ogv', 'video_short.webm', | 164 | this.timeout(10000) |
245 | 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' | ||
246 | ]) | ||
247 | 165 | ||
248 | for (const video of videos) { | ||
249 | const videoAttributes = { | 166 | const videoAttributes = { |
250 | name: video + ' name', | 167 | name: 'my super name', |
251 | description: video + ' description', | ||
252 | category: 2, | 168 | category: 2, |
253 | licence: 1, | ||
254 | language: 'en', | ||
255 | nsfw: true, | 169 | nsfw: true, |
256 | tags: [ 'tag1', 'tag2', 'tag3' ], | 170 | licence: 6, |
257 | fixture: video | 171 | tags: [ 'tag1', 'tag2', 'tag3' ] |
258 | } | 172 | } |
173 | const res = await uploadVideo(server.url, server.accessToken, videoAttributes, HttpStatusCode.OK_200, mode) | ||
174 | expect(res.body.video).to.not.be.undefined | ||
175 | expect(res.body.video.id).to.equal(1) | ||
176 | expect(res.body.video.uuid).to.have.length.above(5) | ||
259 | 177 | ||
260 | await uploadVideo(server.url, server.accessToken, videoAttributes) | 178 | videoId = res.body.video.id |
261 | } | 179 | videoUUID = res.body.video.uuid |
262 | }) | 180 | }) |
263 | 181 | ||
264 | it('Should have the correct durations', async function () { | 182 | it('Should get and seed the uploaded video', async function () { |
265 | const res = await getVideosList(server.url) | 183 | this.timeout(5000) |
266 | |||
267 | expect(res.body.total).to.equal(6) | ||
268 | const videos = res.body.data | ||
269 | expect(videos).to.be.an('array') | ||
270 | expect(videos).to.have.lengthOf(6) | ||
271 | |||
272 | const videosByName = keyBy<{ duration: number }>(videos, 'name') | ||
273 | expect(videosByName['video_short.mp4 name'].duration).to.equal(5) | ||
274 | expect(videosByName['video_short.ogv name'].duration).to.equal(5) | ||
275 | expect(videosByName['video_short.webm name'].duration).to.equal(5) | ||
276 | expect(videosByName['video_short1.webm name'].duration).to.equal(10) | ||
277 | expect(videosByName['video_short2.webm name'].duration).to.equal(5) | ||
278 | expect(videosByName['video_short3.webm name'].duration).to.equal(5) | ||
279 | }) | ||
280 | 184 | ||
281 | it('Should have the correct thumbnails', async function () { | 185 | const res = await getVideosList(server.url) |
282 | const res = await getVideosList(server.url) | ||
283 | 186 | ||
284 | const videos = res.body.data | 187 | expect(res.body.total).to.equal(1) |
285 | // For the next test | 188 | expect(res.body.data).to.be.an('array') |
286 | videosListBase = videos | 189 | expect(res.body.data.length).to.equal(1) |
287 | 190 | ||
288 | for (const video of videos) { | 191 | const video = res.body.data[0] |
289 | const videoName = video.name.replace(' name', '') | 192 | await completeVideoCheck(server.url, video, getCheckAttributes()) |
290 | await testImage(server.url, videoName, video.thumbnailPath) | 193 | }) |
291 | } | ||
292 | }) | ||
293 | 194 | ||
294 | it('Should list only the two first videos', async function () { | 195 | it('Should get the video by UUID', async function () { |
295 | const res = await getVideosListPagination(server.url, 0, 2, 'name') | 196 | this.timeout(5000) |
296 | 197 | ||
297 | const videos = res.body.data | 198 | const res = await getVideo(server.url, videoUUID) |
298 | expect(res.body.total).to.equal(6) | ||
299 | expect(videos.length).to.equal(2) | ||
300 | expect(videos[0].name).to.equal(videosListBase[0].name) | ||
301 | expect(videos[1].name).to.equal(videosListBase[1].name) | ||
302 | }) | ||
303 | 199 | ||
304 | it('Should list only the next three videos', async function () { | 200 | const video = res.body |
305 | const res = await getVideosListPagination(server.url, 2, 3, 'name') | 201 | await completeVideoCheck(server.url, video, getCheckAttributes()) |
202 | }) | ||
306 | 203 | ||
307 | const videos = res.body.data | 204 | it('Should have the views updated', async function () { |
308 | expect(res.body.total).to.equal(6) | 205 | this.timeout(20000) |
309 | expect(videos.length).to.equal(3) | ||
310 | expect(videos[0].name).to.equal(videosListBase[2].name) | ||
311 | expect(videos[1].name).to.equal(videosListBase[3].name) | ||
312 | expect(videos[2].name).to.equal(videosListBase[4].name) | ||
313 | }) | ||
314 | 206 | ||
315 | it('Should list the last video', async function () { | 207 | await viewVideo(server.url, videoId) |
316 | const res = await getVideosListPagination(server.url, 5, 6, 'name') | 208 | await viewVideo(server.url, videoId) |
209 | await viewVideo(server.url, videoId) | ||
317 | 210 | ||
318 | const videos = res.body.data | 211 | await wait(1500) |
319 | expect(res.body.total).to.equal(6) | ||
320 | expect(videos.length).to.equal(1) | ||
321 | expect(videos[0].name).to.equal(videosListBase[5].name) | ||
322 | }) | ||
323 | 212 | ||
324 | it('Should not have the total field', async function () { | 213 | await viewVideo(server.url, videoId) |
325 | const res = await getVideosListPagination(server.url, 5, 6, 'name', true) | 214 | await viewVideo(server.url, videoId) |
326 | 215 | ||
327 | const videos = res.body.data | 216 | await wait(1500) |
328 | expect(res.body.total).to.not.exist | ||
329 | expect(videos.length).to.equal(1) | ||
330 | expect(videos[0].name).to.equal(videosListBase[5].name) | ||
331 | }) | ||
332 | 217 | ||
333 | it('Should list and sort by name in descending order', async function () { | 218 | await viewVideo(server.url, videoId) |
334 | const res = await getVideosListSort(server.url, '-name') | 219 | await viewVideo(server.url, videoId) |
335 | |||
336 | const videos = res.body.data | ||
337 | expect(res.body.total).to.equal(6) | ||
338 | expect(videos.length).to.equal(6) | ||
339 | expect(videos[0].name).to.equal('video_short.webm name') | ||
340 | expect(videos[1].name).to.equal('video_short.ogv name') | ||
341 | expect(videos[2].name).to.equal('video_short.mp4 name') | ||
342 | expect(videos[3].name).to.equal('video_short3.webm name') | ||
343 | expect(videos[4].name).to.equal('video_short2.webm name') | ||
344 | expect(videos[5].name).to.equal('video_short1.webm name') | ||
345 | |||
346 | videoId = videos[3].uuid | ||
347 | videoId2 = videos[5].uuid | ||
348 | }) | ||
349 | 220 | ||
350 | it('Should list and sort by trending in descending order', async function () { | 221 | // Wait the repeatable job |
351 | const res = await getVideosListPagination(server.url, 0, 2, '-trending') | 222 | await wait(8000) |
352 | 223 | ||
353 | const videos = res.body.data | 224 | const res = await getVideo(server.url, videoId) |
354 | expect(res.body.total).to.equal(6) | ||
355 | expect(videos.length).to.equal(2) | ||
356 | }) | ||
357 | 225 | ||
358 | it('Should list and sort by hotness in descending order', async function () { | 226 | const video = res.body |
359 | const res = await getVideosListPagination(server.url, 0, 2, '-hot') | 227 | expect(video.views).to.equal(3) |
228 | }) | ||
360 | 229 | ||
361 | const videos = res.body.data | 230 | it('Should remove the video', async function () { |
362 | expect(res.body.total).to.equal(6) | 231 | await removeVideo(server.url, server.accessToken, videoId) |
363 | expect(videos.length).to.equal(2) | ||
364 | }) | ||
365 | 232 | ||
366 | it('Should list and sort by best in descending order', async function () { | 233 | await checkVideoFilesWereRemoved(videoUUID, 1) |
367 | const res = await getVideosListPagination(server.url, 0, 2, '-best') | 234 | }) |
368 | 235 | ||
369 | const videos = res.body.data | 236 | it('Should not have videos', async function () { |
370 | expect(res.body.total).to.equal(6) | 237 | const res = await getVideosList(server.url) |
371 | expect(videos.length).to.equal(2) | ||
372 | }) | ||
373 | 238 | ||
374 | it('Should update a video', async function () { | 239 | expect(res.body.total).to.equal(0) |
375 | const attributes = { | 240 | expect(res.body.data).to.be.an('array') |
376 | name: 'my super video updated', | 241 | expect(res.body.data).to.have.lengthOf(0) |
377 | category: 4, | 242 | }) |
378 | licence: 2, | ||
379 | language: 'ar', | ||
380 | nsfw: false, | ||
381 | description: 'my super description updated', | ||
382 | commentsEnabled: false, | ||
383 | downloadEnabled: false, | ||
384 | tags: [ 'tagup1', 'tagup2' ] | ||
385 | } | ||
386 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
387 | }) | ||
388 | 243 | ||
389 | it('Should filter by tags and category', async function () { | 244 | it('Should upload 6 videos', async function () { |
390 | const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] }) | 245 | this.timeout(25000) |
391 | expect(res1.body.total).to.equal(1) | ||
392 | expect(res1.body.data[0].name).to.equal('my super video updated') | ||
393 | 246 | ||
394 | const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] }) | 247 | const videos = new Set([ |
395 | expect(res2.body.total).to.equal(0) | 248 | 'video_short.mp4', 'video_short.ogv', 'video_short.webm', |
396 | }) | 249 | 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' |
250 | ]) | ||
397 | 251 | ||
398 | it('Should have the video updated', async function () { | 252 | for (const video of videos) { |
399 | this.timeout(60000) | 253 | const videoAttributes = { |
254 | name: video + ' name', | ||
255 | description: video + ' description', | ||
256 | category: 2, | ||
257 | licence: 1, | ||
258 | language: 'en', | ||
259 | nsfw: true, | ||
260 | tags: [ 'tag1', 'tag2', 'tag3' ], | ||
261 | fixture: video | ||
262 | } | ||
400 | 263 | ||
401 | const res = await getVideo(server.url, videoId) | 264 | await uploadVideo(server.url, server.accessToken, videoAttributes, HttpStatusCode.OK_200, mode) |
402 | const video = res.body | 265 | } |
266 | }) | ||
267 | |||
268 | it('Should have the correct durations', async function () { | ||
269 | const res = await getVideosList(server.url) | ||
270 | |||
271 | expect(res.body.total).to.equal(6) | ||
272 | const videos = res.body.data | ||
273 | expect(videos).to.be.an('array') | ||
274 | expect(videos).to.have.lengthOf(6) | ||
275 | |||
276 | const videosByName = keyBy<{ duration: number }>(videos, 'name') | ||
277 | expect(videosByName['video_short.mp4 name'].duration).to.equal(5) | ||
278 | expect(videosByName['video_short.ogv name'].duration).to.equal(5) | ||
279 | expect(videosByName['video_short.webm name'].duration).to.equal(5) | ||
280 | expect(videosByName['video_short1.webm name'].duration).to.equal(10) | ||
281 | expect(videosByName['video_short2.webm name'].duration).to.equal(5) | ||
282 | expect(videosByName['video_short3.webm name'].duration).to.equal(5) | ||
283 | }) | ||
284 | |||
285 | it('Should have the correct thumbnails', async function () { | ||
286 | const res = await getVideosList(server.url) | ||
287 | |||
288 | const videos = res.body.data | ||
289 | // For the next test | ||
290 | videosListBase = videos | ||
291 | |||
292 | for (const video of videos) { | ||
293 | const videoName = video.name.replace(' name', '') | ||
294 | await testImage(server.url, videoName, video.thumbnailPath) | ||
295 | } | ||
296 | }) | ||
297 | |||
298 | it('Should list only the two first videos', async function () { | ||
299 | const res = await getVideosListPagination(server.url, 0, 2, 'name') | ||
300 | |||
301 | const videos = res.body.data | ||
302 | expect(res.body.total).to.equal(6) | ||
303 | expect(videos.length).to.equal(2) | ||
304 | expect(videos[0].name).to.equal(videosListBase[0].name) | ||
305 | expect(videos[1].name).to.equal(videosListBase[1].name) | ||
306 | }) | ||
307 | |||
308 | it('Should list only the next three videos', async function () { | ||
309 | const res = await getVideosListPagination(server.url, 2, 3, 'name') | ||
310 | |||
311 | const videos = res.body.data | ||
312 | expect(res.body.total).to.equal(6) | ||
313 | expect(videos.length).to.equal(3) | ||
314 | expect(videos[0].name).to.equal(videosListBase[2].name) | ||
315 | expect(videos[1].name).to.equal(videosListBase[3].name) | ||
316 | expect(videos[2].name).to.equal(videosListBase[4].name) | ||
317 | }) | ||
318 | |||
319 | it('Should list the last video', async function () { | ||
320 | const res = await getVideosListPagination(server.url, 5, 6, 'name') | ||
321 | |||
322 | const videos = res.body.data | ||
323 | expect(res.body.total).to.equal(6) | ||
324 | expect(videos.length).to.equal(1) | ||
325 | expect(videos[0].name).to.equal(videosListBase[5].name) | ||
326 | }) | ||
327 | |||
328 | it('Should not have the total field', async function () { | ||
329 | const res = await getVideosListPagination(server.url, 5, 6, 'name', true) | ||
330 | |||
331 | const videos = res.body.data | ||
332 | expect(res.body.total).to.not.exist | ||
333 | expect(videos.length).to.equal(1) | ||
334 | expect(videos[0].name).to.equal(videosListBase[5].name) | ||
335 | }) | ||
336 | |||
337 | it('Should list and sort by name in descending order', async function () { | ||
338 | const res = await getVideosListSort(server.url, '-name') | ||
339 | |||
340 | const videos = res.body.data | ||
341 | expect(res.body.total).to.equal(6) | ||
342 | expect(videos.length).to.equal(6) | ||
343 | expect(videos[0].name).to.equal('video_short.webm name') | ||
344 | expect(videos[1].name).to.equal('video_short.ogv name') | ||
345 | expect(videos[2].name).to.equal('video_short.mp4 name') | ||
346 | expect(videos[3].name).to.equal('video_short3.webm name') | ||
347 | expect(videos[4].name).to.equal('video_short2.webm name') | ||
348 | expect(videos[5].name).to.equal('video_short1.webm name') | ||
349 | |||
350 | videoId = videos[3].uuid | ||
351 | videoId2 = videos[5].uuid | ||
352 | }) | ||
353 | |||
354 | it('Should list and sort by trending in descending order', async function () { | ||
355 | const res = await getVideosListPagination(server.url, 0, 2, '-trending') | ||
356 | |||
357 | const videos = res.body.data | ||
358 | expect(res.body.total).to.equal(6) | ||
359 | expect(videos.length).to.equal(2) | ||
360 | }) | ||
361 | |||
362 | it('Should list and sort by hotness in descending order', async function () { | ||
363 | const res = await getVideosListPagination(server.url, 0, 2, '-hot') | ||
364 | |||
365 | const videos = res.body.data | ||
366 | expect(res.body.total).to.equal(6) | ||
367 | expect(videos.length).to.equal(2) | ||
368 | }) | ||
369 | |||
370 | it('Should list and sort by best in descending order', async function () { | ||
371 | const res = await getVideosListPagination(server.url, 0, 2, '-best') | ||
372 | |||
373 | const videos = res.body.data | ||
374 | expect(res.body.total).to.equal(6) | ||
375 | expect(videos.length).to.equal(2) | ||
376 | }) | ||
377 | |||
378 | it('Should update a video', async function () { | ||
379 | const attributes = { | ||
380 | name: 'my super video updated', | ||
381 | category: 4, | ||
382 | licence: 2, | ||
383 | language: 'ar', | ||
384 | nsfw: false, | ||
385 | description: 'my super description updated', | ||
386 | commentsEnabled: false, | ||
387 | downloadEnabled: false, | ||
388 | tags: [ 'tagup1', 'tagup2' ] | ||
389 | } | ||
390 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
391 | }) | ||
403 | 392 | ||
404 | await completeVideoCheck(server.url, video, updateCheckAttributes()) | 393 | it('Should filter by tags and category', async function () { |
405 | }) | 394 | const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 4 ] }) |
395 | expect(res1.body.total).to.equal(1) | ||
396 | expect(res1.body.data[0].name).to.equal('my super video updated') | ||
406 | 397 | ||
407 | it('Should update only the tags of a video', async function () { | 398 | const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: [ 3 ] }) |
408 | const attributes = { | 399 | expect(res2.body.total).to.equal(0) |
409 | tags: [ 'supertag', 'tag1', 'tag2' ] | 400 | }) |
410 | } | ||
411 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
412 | 401 | ||
413 | const res = await getVideo(server.url, videoId) | 402 | it('Should have the video updated', async function () { |
414 | const video = res.body | 403 | this.timeout(60000) |
415 | 404 | ||
416 | await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes(), attributes)) | 405 | const res = await getVideo(server.url, videoId) |
417 | }) | 406 | const video = res.body |
418 | 407 | ||
419 | it('Should update only the description of a video', async function () { | 408 | await completeVideoCheck(server.url, video, updateCheckAttributes()) |
420 | const attributes = { | 409 | }) |
421 | description: 'hello everybody' | ||
422 | } | ||
423 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
424 | 410 | ||
425 | const res = await getVideo(server.url, videoId) | 411 | it('Should update only the tags of a video', async function () { |
426 | const video = res.body | 412 | const attributes = { |
413 | tags: [ 'supertag', 'tag1', 'tag2' ] | ||
414 | } | ||
415 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
427 | 416 | ||
428 | const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes) | 417 | const res = await getVideo(server.url, videoId) |
429 | await completeVideoCheck(server.url, video, expectedAttributes) | 418 | const video = res.body |
430 | }) | ||
431 | 419 | ||
432 | it('Should like a video', async function () { | 420 | await completeVideoCheck(server.url, video, Object.assign(updateCheckAttributes(), attributes)) |
433 | await rateVideo(server.url, server.accessToken, videoId, 'like') | 421 | }) |
434 | 422 | ||
435 | const res = await getVideo(server.url, videoId) | 423 | it('Should update only the description of a video', async function () { |
436 | const video = res.body | 424 | const attributes = { |
425 | description: 'hello everybody' | ||
426 | } | ||
427 | await updateVideo(server.url, server.accessToken, videoId, attributes) | ||
437 | 428 | ||
438 | expect(video.likes).to.equal(1) | 429 | const res = await getVideo(server.url, videoId) |
439 | expect(video.dislikes).to.equal(0) | 430 | const video = res.body |
440 | }) | ||
441 | 431 | ||
442 | it('Should dislike the same video', async function () { | 432 | const expectedAttributes = Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes) |
443 | await rateVideo(server.url, server.accessToken, videoId, 'dislike') | 433 | await completeVideoCheck(server.url, video, expectedAttributes) |
434 | }) | ||
444 | 435 | ||
445 | const res = await getVideo(server.url, videoId) | 436 | it('Should like a video', async function () { |
446 | const video = res.body | 437 | await rateVideo(server.url, server.accessToken, videoId, 'like') |
447 | 438 | ||
448 | expect(video.likes).to.equal(0) | 439 | const res = await getVideo(server.url, videoId) |
449 | expect(video.dislikes).to.equal(1) | 440 | const video = res.body |
450 | }) | ||
451 | 441 | ||
452 | it('Should sort by originallyPublishedAt', async function () { | 442 | expect(video.likes).to.equal(1) |
453 | { | 443 | expect(video.dislikes).to.equal(0) |
444 | }) | ||
454 | 445 | ||
446 | it('Should dislike the same video', async function () { | ||
447 | await rateVideo(server.url, server.accessToken, videoId, 'dislike') | ||
448 | |||
449 | const res = await getVideo(server.url, videoId) | ||
450 | const video = res.body | ||
451 | |||
452 | expect(video.likes).to.equal(0) | ||
453 | expect(video.dislikes).to.equal(1) | ||
454 | }) | ||
455 | |||
456 | it('Should sort by originallyPublishedAt', async function () { | ||
455 | { | 457 | { |
456 | const now = new Date() | 458 | const now = new Date() |
457 | const attributes = { originallyPublishedAt: now.toISOString() } | 459 | const attributes = { originallyPublishedAt: now.toISOString() } |
@@ -483,10 +485,18 @@ describe('Test a single server', function () { | |||
483 | expect(names[4]).to.equal('video_short.ogv name') | 485 | expect(names[4]).to.equal('video_short.ogv name') |
484 | expect(names[5]).to.equal('video_short.mp4 name') | 486 | expect(names[5]).to.equal('video_short.mp4 name') |
485 | } | 487 | } |
486 | } | 488 | }) |
489 | |||
490 | after(async function () { | ||
491 | await cleanupTests([ server ]) | ||
492 | }) | ||
493 | } | ||
494 | |||
495 | describe('Legacy upload', function () { | ||
496 | runSuite('legacy') | ||
487 | }) | 497 | }) |
488 | 498 | ||
489 | after(async function () { | 499 | describe('Resumable upload', function () { |
490 | await cleanupTests([ server ]) | 500 | runSuite('resumable') |
491 | }) | 501 | }) |
492 | }) | 502 | }) |
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 1c99f26df..ea5ffd239 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -361,106 +361,117 @@ describe('Test video transcoding', function () { | |||
361 | 361 | ||
362 | describe('Audio upload', function () { | 362 | describe('Audio upload', function () { |
363 | 363 | ||
364 | before(async function () { | 364 | function runSuite (mode: 'legacy' | 'resumable') { |
365 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, { | 365 | |
366 | transcoding: { | 366 | before(async function () { |
367 | hls: { enabled: true }, | 367 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, { |
368 | webtorrent: { enabled: true }, | 368 | transcoding: { |
369 | resolutions: { | 369 | hls: { enabled: true }, |
370 | '0p': false, | 370 | webtorrent: { enabled: true }, |
371 | '240p': false, | 371 | resolutions: { |
372 | '360p': false, | 372 | '0p': false, |
373 | '480p': false, | 373 | '240p': false, |
374 | '720p': false, | 374 | '360p': false, |
375 | '1080p': false, | 375 | '480p': false, |
376 | '1440p': false, | 376 | '720p': false, |
377 | '2160p': false | 377 | '1080p': false, |
378 | '1440p': false, | ||
379 | '2160p': false | ||
380 | } | ||
378 | } | 381 | } |
379 | } | 382 | }) |
380 | }) | 383 | }) |
381 | }) | ||
382 | |||
383 | it('Should merge an audio file with the preview file', async function () { | ||
384 | this.timeout(60_000) | ||
385 | |||
386 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | ||
387 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) | ||
388 | 384 | ||
389 | await waitJobs(servers) | 385 | it('Should merge an audio file with the preview file', async function () { |
386 | this.timeout(60_000) | ||
390 | 387 | ||
391 | for (const server of servers) { | 388 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } |
392 | const res = await getVideosList(server.url) | 389 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode) |
393 | 390 | ||
394 | const video = res.body.data.find(v => v.name === 'audio_with_preview') | 391 | await waitJobs(servers) |
395 | const res2 = await getVideo(server.url, video.id) | ||
396 | const videoDetails: VideoDetails = res2.body | ||
397 | 392 | ||
398 | expect(videoDetails.files).to.have.lengthOf(1) | 393 | for (const server of servers) { |
394 | const res = await getVideosList(server.url) | ||
399 | 395 | ||
400 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) | 396 | const video = res.body.data.find(v => v.name === 'audio_with_preview') |
401 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) | 397 | const res2 = await getVideo(server.url, video.id) |
398 | const videoDetails: VideoDetails = res2.body | ||
402 | 399 | ||
403 | const magnetUri = videoDetails.files[0].magnetUri | 400 | expect(videoDetails.files).to.have.lengthOf(1) |
404 | expect(magnetUri).to.contain('.mp4') | ||
405 | } | ||
406 | }) | ||
407 | 401 | ||
408 | it('Should upload an audio file and choose a default background image', async function () { | 402 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) |
409 | this.timeout(60_000) | 403 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) |
410 | 404 | ||
411 | const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } | 405 | const magnetUri = videoDetails.files[0].magnetUri |
412 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) | 406 | expect(magnetUri).to.contain('.mp4') |
407 | } | ||
408 | }) | ||
413 | 409 | ||
414 | await waitJobs(servers) | 410 | it('Should upload an audio file and choose a default background image', async function () { |
411 | this.timeout(60_000) | ||
415 | 412 | ||
416 | for (const server of servers) { | 413 | const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } |
417 | const res = await getVideosList(server.url) | 414 | await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode) |
418 | 415 | ||
419 | const video = res.body.data.find(v => v.name === 'audio_without_preview') | 416 | await waitJobs(servers) |
420 | const res2 = await getVideo(server.url, video.id) | ||
421 | const videoDetails = res2.body | ||
422 | 417 | ||
423 | expect(videoDetails.files).to.have.lengthOf(1) | 418 | for (const server of servers) { |
419 | const res = await getVideosList(server.url) | ||
424 | 420 | ||
425 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) | 421 | const video = res.body.data.find(v => v.name === 'audio_without_preview') |
426 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) | 422 | const res2 = await getVideo(server.url, video.id) |
423 | const videoDetails = res2.body | ||
427 | 424 | ||
428 | const magnetUri = videoDetails.files[0].magnetUri | 425 | expect(videoDetails.files).to.have.lengthOf(1) |
429 | expect(magnetUri).to.contain('.mp4') | ||
430 | } | ||
431 | }) | ||
432 | 426 | ||
433 | it('Should upload an audio file and create an audio version only', async function () { | 427 | await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: HttpStatusCode.OK_200 }) |
434 | this.timeout(60_000) | 428 | await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: HttpStatusCode.OK_200 }) |
435 | 429 | ||
436 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, { | 430 | const magnetUri = videoDetails.files[0].magnetUri |
437 | transcoding: { | 431 | expect(magnetUri).to.contain('.mp4') |
438 | hls: { enabled: true }, | ||
439 | webtorrent: { enabled: true }, | ||
440 | resolutions: { | ||
441 | '0p': true, | ||
442 | '240p': false, | ||
443 | '360p': false | ||
444 | } | ||
445 | } | 432 | } |
446 | }) | 433 | }) |
447 | 434 | ||
448 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } | 435 | it('Should upload an audio file and create an audio version only', async function () { |
449 | const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg) | 436 | this.timeout(60_000) |
437 | |||
438 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, { | ||
439 | transcoding: { | ||
440 | hls: { enabled: true }, | ||
441 | webtorrent: { enabled: true }, | ||
442 | resolutions: { | ||
443 | '0p': true, | ||
444 | '240p': false, | ||
445 | '360p': false | ||
446 | } | ||
447 | } | ||
448 | }) | ||
450 | 449 | ||
451 | await waitJobs(servers) | 450 | const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } |
451 | const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg, HttpStatusCode.OK_200, mode) | ||
452 | 452 | ||
453 | for (const server of servers) { | 453 | await waitJobs(servers) |
454 | const res2 = await getVideo(server.url, resVideo.body.video.id) | 454 | |
455 | const videoDetails: VideoDetails = res2.body | 455 | for (const server of servers) { |
456 | const res2 = await getVideo(server.url, resVideo.body.video.id) | ||
457 | const videoDetails: VideoDetails = res2.body | ||
456 | 458 | ||
457 | for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) { | 459 | for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) { |
458 | expect(files).to.have.lengthOf(2) | 460 | expect(files).to.have.lengthOf(2) |
459 | expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined | 461 | expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined |
462 | } | ||
460 | } | 463 | } |
461 | } | ||
462 | 464 | ||
463 | await updateConfigForTranscoding(servers[1]) | 465 | await updateConfigForTranscoding(servers[1]) |
466 | }) | ||
467 | } | ||
468 | |||
469 | describe('Legacy upload', function () { | ||
470 | runSuite('legacy') | ||
471 | }) | ||
472 | |||
473 | describe('Resumable upload', function () { | ||
474 | runSuite('resumable') | ||
464 | }) | 475 | }) |
465 | }) | 476 | }) |
466 | 477 | ||
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index cf3e7ae34..55b6e0039 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts | |||
@@ -19,6 +19,9 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server' | |||
19 | import { MVideoImportDefault } from '@server/types/models/video/video-import' | 19 | import { MVideoImportDefault } from '@server/types/models/video/video-import' |
20 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' | 20 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' |
21 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' | 21 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' |
22 | import { HttpMethod } from '@shared/core-utils/miscs/http-methods' | ||
23 | import { VideoCreate } from '@shared/models' | ||
24 | import { File as UploadXFile, Metadata } from '@uploadx/core' | ||
22 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' | 25 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' |
23 | import { | 26 | import { |
24 | MAccountDefault, | 27 | MAccountDefault, |
@@ -37,86 +40,125 @@ import { | |||
37 | MVideoThumbnail, | 40 | MVideoThumbnail, |
38 | MVideoWithRights | 41 | MVideoWithRights |
39 | } from '../../types/models' | 42 | } from '../../types/models' |
40 | |||
41 | declare module 'express' { | 43 | declare module 'express' { |
42 | export interface Request { | 44 | export interface Request { |
43 | query: any | 45 | query: any |
46 | method: HttpMethod | ||
44 | } | 47 | } |
45 | interface Response { | 48 | |
46 | locals: PeerTubeLocals | 49 | // Upload using multer or uploadx middleware |
50 | export type MulterOrUploadXFile = UploadXFile | Express.Multer.File | ||
51 | |||
52 | export type UploadFiles = { | ||
53 | [fieldname: string]: MulterOrUploadXFile[] | ||
54 | } | MulterOrUploadXFile[] | ||
55 | |||
56 | // Partial object used by some functions to check the file mimetype/extension | ||
57 | export type UploadFileForCheck = { | ||
58 | originalname: string | ||
59 | mimetype: string | ||
47 | } | 60 | } |
48 | } | ||
49 | 61 | ||
50 | interface PeerTubeLocals { | 62 | export type UploadFilesForCheck = { |
51 | videoAll?: MVideoFullLight | 63 | [fieldname: string]: UploadFileForCheck[] |
52 | onlyImmutableVideo?: MVideoImmutable | 64 | } | UploadFileForCheck[] |
53 | onlyVideo?: MVideoThumbnail | ||
54 | onlyVideoWithRights?: MVideoWithRights | ||
55 | videoId?: MVideoIdThumbnail | ||
56 | 65 | ||
57 | videoLive?: MVideoLive | 66 | // Upload file with a duration added by our middleware |
67 | export type VideoUploadFile = Pick<Express.Multer.File, 'path' | 'filename' | 'size'> & { | ||
68 | duration: number | ||
69 | } | ||
58 | 70 | ||
59 | videoShare?: MVideoShareActor | 71 | // Extends Metadata property of UploadX object |
72 | export type UploadXFileMetadata = Metadata & VideoCreate & { | ||
73 | previewfile: Express.Multer.File[] | ||
74 | thumbnailfile: Express.Multer.File[] | ||
75 | } | ||
60 | 76 | ||
61 | videoFile?: MVideoFile | 77 | // Our custom UploadXFile object using our custom metadata |
78 | export type CustomUploadXFile <T extends Metadata> = UploadXFile & { metadata: T } | ||
62 | 79 | ||
63 | videoImport?: MVideoImportDefault | 80 | export type EnhancedUploadXFile = CustomUploadXFile<UploadXFileMetadata> & { |
81 | duration: number | ||
82 | path: string | ||
83 | filename: string | ||
84 | } | ||
64 | 85 | ||
65 | videoBlacklist?: MVideoBlacklist | 86 | // Extends locals property from Response |
87 | interface Response { | ||
88 | locals: { | ||
89 | videoAll?: MVideoFullLight | ||
90 | onlyImmutableVideo?: MVideoImmutable | ||
91 | onlyVideo?: MVideoThumbnail | ||
92 | onlyVideoWithRights?: MVideoWithRights | ||
93 | videoId?: MVideoIdThumbnail | ||
66 | 94 | ||
67 | videoCaption?: MVideoCaptionVideo | 95 | videoLive?: MVideoLive |
68 | 96 | ||
69 | abuse?: MAbuseReporter | 97 | videoShare?: MVideoShareActor |
70 | abuseMessage?: MAbuseMessage | ||
71 | 98 | ||
72 | videoStreamingPlaylist?: MStreamingPlaylist | 99 | videoFile?: MVideoFile |
73 | 100 | ||
74 | videoChannel?: MChannelBannerAccountDefault | 101 | videoFileResumable?: EnhancedUploadXFile |
75 | 102 | ||
76 | videoPlaylistFull?: MVideoPlaylistFull | 103 | videoImport?: MVideoImportDefault |
77 | videoPlaylistSummary?: MVideoPlaylistFullSummary | ||
78 | 104 | ||
79 | videoPlaylistElement?: MVideoPlaylistElement | 105 | videoBlacklist?: MVideoBlacklist |
80 | videoPlaylistElementAP?: MVideoPlaylistElementVideoUrlPlaylistPrivacy | ||
81 | 106 | ||
82 | accountVideoRate?: MAccountVideoRateAccountVideo | 107 | videoCaption?: MVideoCaptionVideo |
83 | 108 | ||
84 | videoCommentFull?: MCommentOwnerVideoReply | 109 | abuse?: MAbuseReporter |
85 | videoCommentThread?: MComment | 110 | abuseMessage?: MAbuseMessage |
86 | 111 | ||
87 | follow?: MActorFollowActorsDefault | 112 | videoStreamingPlaylist?: MStreamingPlaylist |
88 | subscription?: MActorFollowActorsDefaultSubscription | ||
89 | 113 | ||
90 | nextOwner?: MAccountDefault | 114 | videoChannel?: MChannelBannerAccountDefault |
91 | videoChangeOwnership?: MVideoChangeOwnershipFull | ||
92 | 115 | ||
93 | account?: MAccountDefault | 116 | videoPlaylistFull?: MVideoPlaylistFull |
117 | videoPlaylistSummary?: MVideoPlaylistFullSummary | ||
94 | 118 | ||
95 | actorUrl?: MActorUrl | 119 | videoPlaylistElement?: MVideoPlaylistElement |
96 | actorFull?: MActorFull | 120 | videoPlaylistElementAP?: MVideoPlaylistElementVideoUrlPlaylistPrivacy |
97 | 121 | ||
98 | user?: MUserDefault | 122 | accountVideoRate?: MAccountVideoRateAccountVideo |
99 | 123 | ||
100 | server?: MServer | 124 | videoCommentFull?: MCommentOwnerVideoReply |
125 | videoCommentThread?: MComment | ||
101 | 126 | ||
102 | videoRedundancy?: MVideoRedundancyVideo | 127 | follow?: MActorFollowActorsDefault |
128 | subscription?: MActorFollowActorsDefaultSubscription | ||
103 | 129 | ||
104 | accountBlock?: MAccountBlocklist | 130 | nextOwner?: MAccountDefault |
105 | serverBlock?: MServerBlocklist | 131 | videoChangeOwnership?: MVideoChangeOwnershipFull |
106 | 132 | ||
107 | oauth?: { | 133 | account?: MAccountDefault |
108 | token: MOAuthTokenUser | ||
109 | } | ||
110 | 134 | ||
111 | signature?: { | 135 | actorUrl?: MActorUrl |
112 | actor: MActorAccountChannelId | 136 | actorFull?: MActorFull |
113 | } | 137 | |
138 | user?: MUserDefault | ||
139 | |||
140 | server?: MServer | ||
141 | |||
142 | videoRedundancy?: MVideoRedundancyVideo | ||
114 | 143 | ||
115 | authenticated?: boolean | 144 | accountBlock?: MAccountBlocklist |
145 | serverBlock?: MServerBlocklist | ||
116 | 146 | ||
117 | registeredPlugin?: RegisteredPlugin | 147 | oauth?: { |
148 | token: MOAuthTokenUser | ||
149 | } | ||
118 | 150 | ||
119 | externalAuth?: RegisterServerAuthExternalOptions | 151 | signature?: { |
152 | actor: MActorAccountChannelId | ||
153 | } | ||
120 | 154 | ||
121 | plugin?: MPlugin | 155 | authenticated?: boolean |
156 | |||
157 | registeredPlugin?: RegisteredPlugin | ||
158 | |||
159 | externalAuth?: RegisterServerAuthExternalOptions | ||
160 | |||
161 | plugin?: MPlugin | ||
162 | } | ||
163 | } | ||
122 | } | 164 | } |