aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-01-19 14:23:00 +0100
committerChocobozzz <me@florianbigard.com>2022-01-19 14:31:05 +0100
commit419b520ca4434d17f3505013174e195c3a316716 (patch)
tree24dbf663c4e11e970cb780f96e6eb3efe023b222 /server
parent52435e467a0b30175a10af1dd3ae10d7d564d8ae (diff)
downloadPeerTube-419b520ca4434d17f3505013174e195c3a316716.tar.gz
PeerTube-419b520ca4434d17f3505013174e195c3a316716.tar.zst
PeerTube-419b520ca4434d17f3505013174e195c3a316716.zip
Add ability to cancel & delete video imports
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/jobs.ts26
-rw-r--r--server/controllers/api/videos/import.ts48
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/lib/job-queue/handlers/video-import.ts29
-rw-r--r--server/lib/job-queue/job-queue.ts12
-rw-r--r--server/middlewares/validators/videos/video-imports.ts71
-rw-r--r--server/tests/api/check-params/jobs.ts43
-rw-r--r--server/tests/api/check-params/video-imports.ts66
-rw-r--r--server/tests/api/server/jobs.ts25
-rw-r--r--server/tests/api/videos/video-imports.ts81
10 files changed, 379 insertions, 26 deletions
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index eebd195b0..c61b7362f 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -1,5 +1,5 @@
1import express from 'express' 1import express from 'express'
2import { Job, JobState, JobType, ResultList, UserRight } from '@shared/models' 2import { HttpStatusCode, Job, JobState, JobType, ResultList, UserRight } from '@shared/models'
3import { isArray } from '../../helpers/custom-validators/misc' 3import { isArray } from '../../helpers/custom-validators/misc'
4import { JobQueue } from '../../lib/job-queue' 4import { JobQueue } from '../../lib/job-queue'
5import { 5import {
@@ -16,6 +16,18 @@ import { listJobsValidator } from '../../middlewares/validators/jobs'
16 16
17const jobsRouter = express.Router() 17const jobsRouter = express.Router()
18 18
19jobsRouter.post('/pause',
20 authenticate,
21 ensureUserHasRight(UserRight.MANAGE_JOBS),
22 asyncMiddleware(pauseJobQueue)
23)
24
25jobsRouter.post('/resume',
26 authenticate,
27 ensureUserHasRight(UserRight.MANAGE_JOBS),
28 asyncMiddleware(resumeJobQueue)
29)
30
19jobsRouter.get('/:state?', 31jobsRouter.get('/:state?',
20 openapiOperationDoc({ operationId: 'getJobs' }), 32 openapiOperationDoc({ operationId: 'getJobs' }),
21 authenticate, 33 authenticate,
@@ -36,6 +48,18 @@ export {
36 48
37// --------------------------------------------------------------------------- 49// ---------------------------------------------------------------------------
38 50
51async function pauseJobQueue (req: express.Request, res: express.Response) {
52 await JobQueue.Instance.pause()
53
54 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
55}
56
57async function resumeJobQueue (req: express.Request, res: express.Response) {
58 await JobQueue.Instance.resume()
59
60 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
61}
62
39async function listJobs (req: express.Request, res: express.Response) { 63async function listJobs (req: express.Request, res: express.Response) {
40 const state = req.params.state as JobState 64 const state = req.params.state as JobState
41 const asc = req.query.sort === 'createdAt' 65 const asc = req.query.sort === 'createdAt'
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 08d69827b..8cbfd3286 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -19,7 +19,15 @@ import {
19 MVideoWithBlacklistLight 19 MVideoWithBlacklistLight
20} from '@server/types/models' 20} from '@server/types/models'
21import { MVideoImportFormattable } from '@server/types/models/video/video-import' 21import { MVideoImportFormattable } from '@server/types/models/video/video-import'
22import { ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '@shared/models' 22import {
23 HttpStatusCode,
24 ServerErrorCode,
25 ThumbnailType,
26 VideoImportCreate,
27 VideoImportState,
28 VideoPrivacy,
29 VideoState
30} from '@shared/models'
23import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' 31import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
24import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' 32import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
25import { isArray } from '../../../helpers/custom-validators/misc' 33import { isArray } from '../../../helpers/custom-validators/misc'
@@ -34,7 +42,14 @@ import { getLocalVideoActivityPubUrl } from '../../../lib/activitypub/url'
34import { JobQueue } from '../../../lib/job-queue/job-queue' 42import { JobQueue } from '../../../lib/job-queue/job-queue'
35import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from '../../../lib/thumbnail' 43import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from '../../../lib/thumbnail'
36import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' 44import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
37import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' 45import {
46 asyncMiddleware,
47 asyncRetryTransactionMiddleware,
48 authenticate,
49 videoImportAddValidator,
50 videoImportCancelValidator,
51 videoImportDeleteValidator
52} from '../../../middlewares'
38import { VideoModel } from '../../../models/video/video' 53import { VideoModel } from '../../../models/video/video'
39import { VideoCaptionModel } from '../../../models/video/video-caption' 54import { VideoCaptionModel } from '../../../models/video/video-caption'
40import { VideoImportModel } from '../../../models/video/video-import' 55import { VideoImportModel } from '../../../models/video/video-import'
@@ -59,6 +74,18 @@ videoImportsRouter.post('/imports',
59 asyncRetryTransactionMiddleware(addVideoImport) 74 asyncRetryTransactionMiddleware(addVideoImport)
60) 75)
61 76
77videoImportsRouter.post('/imports/:id/cancel',
78 authenticate,
79 asyncMiddleware(videoImportCancelValidator),
80 asyncRetryTransactionMiddleware(cancelVideoImport)
81)
82
83videoImportsRouter.delete('/imports/:id',
84 authenticate,
85 asyncMiddleware(videoImportDeleteValidator),
86 asyncRetryTransactionMiddleware(deleteVideoImport)
87)
88
62// --------------------------------------------------------------------------- 89// ---------------------------------------------------------------------------
63 90
64export { 91export {
@@ -67,6 +94,23 @@ export {
67 94
68// --------------------------------------------------------------------------- 95// ---------------------------------------------------------------------------
69 96
97async function deleteVideoImport (req: express.Request, res: express.Response) {
98 const videoImport = res.locals.videoImport
99
100 await videoImport.destroy()
101
102 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
103}
104
105async function cancelVideoImport (req: express.Request, res: express.Response) {
106 const videoImport = res.locals.videoImport
107
108 videoImport.state = VideoImportState.CANCELLED
109 await videoImport.save()
110
111 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
112}
113
70function addVideoImport (req: express.Request, res: express.Response) { 114function addVideoImport (req: express.Request, res: express.Response) {
71 if (req.body.targetUrl) return addYoutubeDLImport(req, res) 115 if (req.body.targetUrl) return addYoutubeDLImport(req, res)
72 116
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index b2f511152..6a59bf805 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -441,7 +441,9 @@ const VIDEO_IMPORT_STATES: { [ id in VideoImportState ]: string } = {
441 [VideoImportState.FAILED]: 'Failed', 441 [VideoImportState.FAILED]: 'Failed',
442 [VideoImportState.PENDING]: 'Pending', 442 [VideoImportState.PENDING]: 'Pending',
443 [VideoImportState.SUCCESS]: 'Success', 443 [VideoImportState.SUCCESS]: 'Success',
444 [VideoImportState.REJECTED]: 'Rejected' 444 [VideoImportState.REJECTED]: 'Rejected',
445 [VideoImportState.CANCELLED]: 'Cancelled',
446 [VideoImportState.PROCESSING]: 'Processing'
445} 447}
446 448
447const ABUSE_STATES: { [ id in AbuseState ]: string } = { 449const ABUSE_STATES: { [ id in AbuseState ]: string } = {
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 2f74e9fbd..cb79725aa 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -42,8 +42,17 @@ import { generateVideoMiniature } from '../../thumbnail'
42async function processVideoImport (job: Job) { 42async function processVideoImport (job: Job) {
43 const payload = job.data as VideoImportPayload 43 const payload = job.data as VideoImportPayload
44 44
45 if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, payload) 45 const videoImport = await getVideoImportOrDie(payload.videoImportId)
46 if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, payload) 46 if (videoImport.state === VideoImportState.CANCELLED) {
47 logger.info('Do not process import since it has been cancelled', { payload })
48 return
49 }
50
51 videoImport.state = VideoImportState.PROCESSING
52 await videoImport.save()
53
54 if (payload.type === 'youtube-dl') return processYoutubeDLImport(job, videoImport, payload)
55 if (payload.type === 'magnet-uri' || payload.type === 'torrent-file') return processTorrentImport(job, videoImport, payload)
47} 56}
48 57
49// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
@@ -54,15 +63,11 @@ export {
54 63
55// --------------------------------------------------------------------------- 64// ---------------------------------------------------------------------------
56 65
57async function processTorrentImport (job: Job, payload: VideoImportTorrentPayload) { 66async function processTorrentImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportTorrentPayload) {
58 logger.info('Processing torrent video import in job %d.', job.id) 67 logger.info('Processing torrent video import in job %d.', job.id)
59 68
60 const videoImport = await getVideoImportOrDie(payload.videoImportId) 69 const options = { type: payload.type, videoImportId: payload.videoImportId }
61 70
62 const options = {
63 type: payload.type,
64 videoImportId: payload.videoImportId
65 }
66 const target = { 71 const target = {
67 torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined, 72 torrentName: videoImport.torrentName ? getSecureTorrentName(videoImport.torrentName) : undefined,
68 uri: videoImport.magnetUri 73 uri: videoImport.magnetUri
@@ -70,14 +75,10 @@ async function processTorrentImport (job: Job, payload: VideoImportTorrentPayloa
70 return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options) 75 return processFile(() => downloadWebTorrentVideo(target, VIDEO_IMPORT_TIMEOUT), videoImport, options)
71} 76}
72 77
73async function processYoutubeDLImport (job: Job, payload: VideoImportYoutubeDLPayload) { 78async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportYoutubeDLPayload) {
74 logger.info('Processing youtubeDL video import in job %d.', job.id) 79 logger.info('Processing youtubeDL video import in job %d.', job.id)
75 80
76 const videoImport = await getVideoImportOrDie(payload.videoImportId) 81 const options = { type: payload.type, videoImportId: videoImport.id }
77 const options = {
78 type: payload.type,
79 videoImportId: videoImport.id
80 }
81 82
82 const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) 83 const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod'))
83 84
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index fbc599f12..22bd1f5d2 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -162,6 +162,18 @@ class JobQueue {
162 } 162 }
163 } 163 }
164 164
165 async pause () {
166 for (const handler of Object.keys(this.queues)) {
167 await this.queues[handler].pause(true)
168 }
169 }
170
171 async resume () {
172 for (const handler of Object.keys(this.queues)) {
173 await this.queues[handler].resume(true)
174 }
175 }
176
165 createJob (obj: CreateJobArgument, options: CreateJobOptions = {}): void { 177 createJob (obj: CreateJobArgument, options: CreateJobOptions = {}): void {
166 this.createJobWithPromise(obj, options) 178 this.createJobWithPromise(obj, options)
167 .catch(err => logger.error('Cannot create job.', { err, obj })) 179 .catch(err => logger.error('Cannot create job.', { err, obj }))
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
index e4b54283f..a3a5cc531 100644
--- a/server/middlewares/validators/videos/video-imports.ts
+++ b/server/middlewares/validators/videos/video-imports.ts
@@ -1,8 +1,10 @@
1import express from 'express' 1import express from 'express'
2import { body } from 'express-validator' 2import { body, param } from 'express-validator'
3import { isValid as isIPValid, parse as parseIP } from 'ipaddr.js'
3import { isPreImportVideoAccepted } from '@server/lib/moderation' 4import { isPreImportVideoAccepted } from '@server/lib/moderation'
4import { Hooks } from '@server/lib/plugins/hooks' 5import { Hooks } from '@server/lib/plugins/hooks'
5import { HttpStatusCode } from '@shared/models' 6import { MUserAccountId, MVideoImport } from '@server/types/models'
7import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models'
6import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' 8import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model'
7import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' 9import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
8import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' 10import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
@@ -11,9 +13,8 @@ import { cleanUpReqFiles } from '../../../helpers/express-utils'
11import { logger } from '../../../helpers/logger' 13import { logger } from '../../../helpers/logger'
12import { CONFIG } from '../../../initializers/config' 14import { CONFIG } from '../../../initializers/config'
13import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 15import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
14import { areValidationErrors, doesVideoChannelOfAccountExist } from '../shared' 16import { areValidationErrors, doesVideoChannelOfAccountExist, doesVideoImportExist } from '../shared'
15import { getCommonVideoEditAttributes } from './videos' 17import { getCommonVideoEditAttributes } from './videos'
16import { isValid as isIPValid, parse as parseIP } from 'ipaddr.js'
17 18
18const videoImportAddValidator = getCommonVideoEditAttributes().concat([ 19const videoImportAddValidator = getCommonVideoEditAttributes().concat([
19 body('channelId') 20 body('channelId')
@@ -95,10 +96,58 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
95 } 96 }
96]) 97])
97 98
99const videoImportDeleteValidator = [
100 param('id')
101 .custom(isIdValid).withMessage('Should have correct import id'),
102
103 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
104 logger.debug('Checking videoImportDeleteValidator parameters', { parameters: req.params })
105
106 if (areValidationErrors(req, res)) return
107
108 if (!await doesVideoImportExist(parseInt(req.params.id), res)) return
109 if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
110
111 if (res.locals.videoImport.state === VideoImportState.PENDING) {
112 return res.fail({
113 status: HttpStatusCode.CONFLICT_409,
114 message: 'Cannot delete a pending video import. Cancel it or wait for the end of the import first.'
115 })
116 }
117
118 return next()
119 }
120]
121
122const videoImportCancelValidator = [
123 param('id')
124 .custom(isIdValid).withMessage('Should have correct import id'),
125
126 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
127 logger.debug('Checking videoImportCancelValidator parameters', { parameters: req.params })
128
129 if (areValidationErrors(req, res)) return
130
131 if (!await doesVideoImportExist(parseInt(req.params.id), res)) return
132 if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
133
134 if (res.locals.videoImport.state !== VideoImportState.PENDING) {
135 return res.fail({
136 status: HttpStatusCode.CONFLICT_409,
137 message: 'Cannot cancel a non pending video import.'
138 })
139 }
140
141 return next()
142 }
143]
144
98// --------------------------------------------------------------------------- 145// ---------------------------------------------------------------------------
99 146
100export { 147export {
101 videoImportAddValidator 148 videoImportAddValidator,
149 videoImportCancelValidator,
150 videoImportDeleteValidator
102} 151}
103 152
104// --------------------------------------------------------------------------- 153// ---------------------------------------------------------------------------
@@ -132,3 +181,15 @@ async function isImportAccepted (req: express.Request, res: express.Response) {
132 181
133 return true 182 return true
134} 183}
184
185function checkUserCanManageImport (user: MUserAccountId, videoImport: MVideoImport, res: express.Response) {
186 if (user.hasRight(UserRight.MANAGE_VIDEO_IMPORTS) === false && videoImport.userId !== user.id) {
187 res.fail({
188 status: HttpStatusCode.FORBIDDEN_403,
189 message: 'Cannot manage video import of another user'
190 })
191 return false
192 }
193
194 return true
195}
diff --git a/server/tests/api/check-params/jobs.ts b/server/tests/api/check-params/jobs.ts
index d85961d62..801b13d1e 100644
--- a/server/tests/api/check-params/jobs.ts
+++ b/server/tests/api/check-params/jobs.ts
@@ -3,7 +3,14 @@
3import 'mocha' 3import 'mocha'
4import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared' 4import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
5import { HttpStatusCode } from '@shared/models' 5import { HttpStatusCode } from '@shared/models'
6import { cleanupTests, createSingleServer, makeGetRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' 6import {
7 cleanupTests,
8 createSingleServer,
9 makeGetRequest,
10 makePostBodyRequest,
11 PeerTubeServer,
12 setAccessTokensToServers
13} from '@shared/server-commands'
7 14
8describe('Test jobs API validators', function () { 15describe('Test jobs API validators', function () {
9 const path = '/api/v1/jobs/failed' 16 const path = '/api/v1/jobs/failed'
@@ -76,7 +83,41 @@ describe('Test jobs API validators', function () {
76 expectedStatus: HttpStatusCode.FORBIDDEN_403 83 expectedStatus: HttpStatusCode.FORBIDDEN_403
77 }) 84 })
78 }) 85 })
86 })
87
88 describe('When pausing/resuming the job queue', async function () {
89 const commands = [ 'pause', 'resume' ]
90
91 it('Should fail with a non authenticated user', async function () {
92 for (const command of commands) {
93 await makePostBodyRequest({
94 url: server.url,
95 path: '/api/v1/jobs/' + command,
96 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
97 })
98 }
99 })
79 100
101 it('Should fail with a non admin user', async function () {
102 for (const command of commands) {
103 await makePostBodyRequest({
104 url: server.url,
105 path: '/api/v1/jobs/' + command,
106 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
107 })
108 }
109 })
110
111 it('Should succeed with the correct params', async function () {
112 for (const command of commands) {
113 await makePostBodyRequest({
114 url: server.url,
115 path: '/api/v1/jobs/' + command,
116 token: server.accessToken,
117 expectedStatus: HttpStatusCode.NO_CONTENT_204
118 })
119 }
120 })
80 }) 121 })
81 122
82 after(async function () { 123 after(async function () {
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts
index da05793a0..156a612ee 100644
--- a/server/tests/api/check-params/video-imports.ts
+++ b/server/tests/api/check-params/video-imports.ts
@@ -12,7 +12,9 @@ import {
12 makePostBodyRequest, 12 makePostBodyRequest,
13 makeUploadRequest, 13 makeUploadRequest,
14 PeerTubeServer, 14 PeerTubeServer,
15 setAccessTokensToServers 15 setAccessTokensToServers,
16 setDefaultVideoChannel,
17 waitJobs
16} from '@shared/server-commands' 18} from '@shared/server-commands'
17 19
18describe('Test video imports API validator', function () { 20describe('Test video imports API validator', function () {
@@ -29,6 +31,7 @@ describe('Test video imports API validator', function () {
29 server = await createSingleServer(1) 31 server = await createSingleServer(1)
30 32
31 await setAccessTokensToServers([ server ]) 33 await setAccessTokensToServers([ server ])
34 await setDefaultVideoChannel([ server ])
32 35
33 const username = 'user1' 36 const username = 'user1'
34 const password = 'my super password' 37 const password = 'my super password'
@@ -347,6 +350,67 @@ describe('Test video imports API validator', function () {
347 }) 350 })
348 }) 351 })
349 352
353 describe('Deleting/cancelling a video import', function () {
354 let importId: number
355
356 async function importVideo () {
357 const attributes = { channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo }
358 const res = await server.imports.importVideo({ attributes })
359
360 return res.id
361 }
362
363 before(async function () {
364 importId = await importVideo()
365 })
366
367 it('Should fail with an invalid import id', async function () {
368 await server.imports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
369 await server.imports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
370 })
371
372 it('Should fail with an unknown import id', async function () {
373 await server.imports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
374 await server.imports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
375 })
376
377 it('Should fail without token', async function () {
378 await server.imports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
379 await server.imports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
380 })
381
382 it('Should fail with another user token', async function () {
383 await server.imports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
384 await server.imports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
385 })
386
387 it('Should fail to cancel non pending import', async function () {
388 this.timeout(60000)
389
390 await waitJobs([ server ])
391
392 await server.imports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 })
393 })
394
395 it('Should succeed to delete an import', async function () {
396 await server.imports.delete({ importId })
397 })
398
399 it('Should fail to delete a pending import', async function () {
400 await server.jobs.pauseJobQueue()
401
402 importId = await importVideo()
403
404 await server.imports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 })
405 })
406
407 it('Should succeed to cancel an import', async function () {
408 importId = await importVideo()
409
410 await server.imports.cancel({ importId })
411 })
412 })
413
350 after(async function () { 414 after(async function () {
351 await cleanupTests([ server ]) 415 await cleanupTests([ server ])
352 }) 416 })
diff --git a/server/tests/api/server/jobs.ts b/server/tests/api/server/jobs.ts
index 4294e1fd5..bd8ffe188 100644
--- a/server/tests/api/server/jobs.ts
+++ b/server/tests/api/server/jobs.ts
@@ -11,6 +11,7 @@ import {
11 setAccessTokensToServers, 11 setAccessTokensToServers,
12 waitJobs 12 waitJobs
13} from '@shared/server-commands' 13} from '@shared/server-commands'
14import { wait } from '@shared/core-utils'
14 15
15const expect = chai.expect 16const expect = chai.expect
16 17
@@ -91,6 +92,30 @@ describe('Test jobs', function () {
91 expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined 92 expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined
92 }) 93 })
93 94
95 it('Should pause the job queue', async function () {
96 this.timeout(120000)
97
98 await servers[1].jobs.pauseJobQueue()
99
100 await servers[1].videos.upload({ attributes: { name: 'video2' } })
101
102 await wait(5000)
103
104 const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' })
105 expect(body.data).to.have.lengthOf(1)
106 })
107
108 it('Should resume the job queue', async function () {
109 this.timeout(120000)
110
111 await servers[1].jobs.resumeJobQueue()
112
113 await waitJobs(servers)
114
115 const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' })
116 expect(body.data).to.have.lengthOf(0)
117 })
118
94 after(async function () { 119 after(async function () {
95 await cleanupTests(servers) 120 await cleanupTests(servers)
96 }) 121 })
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index e8e0f01f1..ba21ab17a 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -6,7 +6,7 @@ import { pathExists, readdir, remove } from 'fs-extra'
6import { join } from 'path' 6import { join } from 'path'
7import { FIXTURE_URLS, testCaptionFile, testImage } from '@server/tests/shared' 7import { FIXTURE_URLS, testCaptionFile, testImage } from '@server/tests/shared'
8import { areHttpImportTestsDisabled } from '@shared/core-utils' 8import { areHttpImportTestsDisabled } from '@shared/core-utils'
9import { VideoPrivacy, VideoResolution } from '@shared/models' 9import { HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@shared/models'
10import { 10import {
11 cleanupTests, 11 cleanupTests,
12 createMultipleServers, 12 createMultipleServers,
@@ -382,6 +382,85 @@ describe('Test video imports', function () {
382 382
383 runSuite('yt-dlp') 383 runSuite('yt-dlp')
384 384
385 describe('Delete/cancel an import', function () {
386 let server: PeerTubeServer
387
388 let finishedImportId: number
389 let finishedVideo: Video
390 let pendingImportId: number
391
392 async function importVideo (name: string) {
393 const attributes = { name, channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo }
394 const res = await server.imports.importVideo({ attributes })
395
396 return res.id
397 }
398
399 before(async function () {
400 this.timeout(120_000)
401
402 server = await createSingleServer(1)
403
404 await setAccessTokensToServers([ server ])
405 await setDefaultVideoChannel([ server ])
406
407 finishedImportId = await importVideo('finished')
408 await waitJobs([ server ])
409
410 await server.jobs.pauseJobQueue()
411 pendingImportId = await importVideo('pending')
412
413 const { data } = await server.imports.getMyVideoImports()
414 expect(data).to.have.lengthOf(2)
415
416 finishedVideo = data.find(i => i.id === finishedImportId).video
417 })
418
419 it('Should delete a video import', async function () {
420 await server.imports.delete({ importId: finishedImportId })
421
422 const { data } = await server.imports.getMyVideoImports()
423 expect(data).to.have.lengthOf(1)
424 expect(data[0].id).to.equal(pendingImportId)
425 expect(data[0].state.id).to.equal(VideoImportState.PENDING)
426 })
427
428 it('Should not have deleted the associated video', async function () {
429 const video = await server.videos.get({ id: finishedVideo.id, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
430 expect(video.name).to.equal('finished')
431 expect(video.state.id).to.equal(VideoState.PUBLISHED)
432 })
433
434 it('Should cancel a video import', async function () {
435 await server.imports.cancel({ importId: pendingImportId })
436
437 const { data } = await server.imports.getMyVideoImports()
438 expect(data).to.have.lengthOf(1)
439 expect(data[0].id).to.equal(pendingImportId)
440 expect(data[0].state.id).to.equal(VideoImportState.CANCELLED)
441 })
442
443 it('Should not have processed the cancelled video import', async function () {
444 this.timeout(60_000)
445
446 await server.jobs.resumeJobQueue()
447
448 await waitJobs([ server ])
449
450 const { data } = await server.imports.getMyVideoImports()
451 expect(data).to.have.lengthOf(1)
452 expect(data[0].id).to.equal(pendingImportId)
453 expect(data[0].state.id).to.equal(VideoImportState.CANCELLED)
454 expect(data[0].video.state.id).to.equal(VideoState.TO_IMPORT)
455 })
456
457 it('Should delete the cancelled video import', async function () {
458 await server.imports.delete({ importId: pendingImportId })
459 const { data } = await server.imports.getMyVideoImports()
460 expect(data).to.have.lengthOf(0)
461 })
462 })
463
385 describe('Auto update', function () { 464 describe('Auto update', function () {
386 let server: PeerTubeServer 465 let server: PeerTubeServer
387 466