>(JobService.BASE_JOB_URL, { params })
+ .map(res => this.restExtractor.convertResultListDateToHuman(res))
+ .catch(err => this.restExtractor.handleError(err))
+ }
+}
diff --git a/client/src/app/core/menu/menu-admin.component.html b/client/src/app/core/menu/menu-admin.component.html
index eb2d0d69c..9857b2e3e 100644
--- a/client/src/app/core/menu/menu-admin.component.html
+++ b/client/src/app/core/menu/menu-admin.component.html
@@ -19,6 +19,11 @@
Video blacklist
+
+
+
+ Jobs
+
diff --git a/client/src/app/core/menu/menu-admin.component.ts b/client/src/app/core/menu/menu-admin.component.ts
index 466da1aee..ea8d5f57c 100644
--- a/client/src/app/core/menu/menu-admin.component.ts
+++ b/client/src/app/core/menu/menu-admin.component.ts
@@ -26,4 +26,8 @@ export class MenuAdminComponent {
hasVideoBlacklistRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
}
+
+ hasJobsRight () {
+ return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
+ }
}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index b00fb7467..737ea4602 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -7,6 +7,7 @@ import { configRouter } from './config'
import { serverRouter } from './server'
import { usersRouter } from './users'
import { videosRouter } from './videos'
+import { jobsRouter } from './jobs'
const apiRouter = express.Router()
@@ -15,6 +16,7 @@ apiRouter.use('/oauth-clients', oauthClientsRouter)
apiRouter.use('/config', configRouter)
apiRouter.use('/users', usersRouter)
apiRouter.use('/videos', videosRouter)
+apiRouter.use('/jobs', jobsRouter)
apiRouter.use('/ping', pong)
apiRouter.use('/*', badRequest)
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
new file mode 100644
index 000000000..f6fbff369
--- /dev/null
+++ b/server/controllers/api/jobs.ts
@@ -0,0 +1,34 @@
+import * as express from 'express'
+import { asyncMiddleware, jobsSortValidator, setJobsSort, setPagination } from '../../middlewares'
+import { paginationValidator } from '../../middlewares/validators/pagination'
+import { database as db } from '../../initializers'
+import { getFormattedObjects } from '../../helpers/utils'
+import { authenticate } from '../../middlewares/oauth'
+import { ensureUserHasRight } from '../../middlewares/user-right'
+import { UserRight } from '../../../shared/models/users/user-right.enum'
+
+const jobsRouter = express.Router()
+
+jobsRouter.get('/',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_JOBS),
+ paginationValidator,
+ jobsSortValidator,
+ setJobsSort,
+ setPagination,
+ asyncMiddleware(listJobs)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ jobsRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function listJobs (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const resultList = await db.Job.listForApi(req.query.start, req.query.count, req.query.sort)
+
+ return res.json(getFormattedObjects(resultList.data, resultList.total))
+}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 6d8aa7332..8f278fb0b 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -32,6 +32,7 @@ const SEARCHABLE_COLUMNS = {
// Sortable columns per schema
const SORTABLE_COLUMNS = {
USERS: [ 'id', 'username', 'createdAt' ],
+ JOBS: [ 'id', 'createdAt' ],
VIDEO_ABUSES: [ 'id', 'createdAt' ],
VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts
index 279b29e65..7b60920de 100644
--- a/server/middlewares/sort.ts
+++ b/server/middlewares/sort.ts
@@ -10,6 +10,12 @@ function setUsersSort (req: express.Request, res: express.Response, next: expres
return next()
}
+function setJobsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
+ if (!req.query.sort) req.query.sort = '-createdAt'
+
+ return next()
+}
+
function setVideoAbusesSort (req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.query.sort) req.query.sort = '-createdAt'
@@ -70,5 +76,6 @@ export {
setVideosSort,
setBlacklistSort,
setFollowersSort,
- setFollowingSort
+ setFollowingSort,
+ setJobsSort
}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 636f68885..d5822ac81 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -7,6 +7,7 @@ import { areValidationErrors } from './utils'
// Initialize constants here for better performances
const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
+const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
@@ -15,6 +16,7 @@ const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOW
const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
+const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
@@ -31,7 +33,8 @@ export {
videosSortValidator,
blacklistSortValidator,
followersSortValidator,
- followingSortValidator
+ followingSortValidator,
+ jobsSortValidator
}
// ---------------------------------------------------------------------------
diff --git a/server/models/account/user-interface.ts b/server/models/account/user-interface.ts
index 1a04fb750..0f0b72063 100644
--- a/server/models/account/user-interface.ts
+++ b/server/models/account/user-interface.ts
@@ -1,12 +1,10 @@
-import * as Sequelize from 'sequelize'
import * as Bluebird from 'bluebird'
-
-// Don't use barrel, import just what we need
-import { AccountInstance } from './account-interface'
-import { User as FormattedUser } from '../../../shared/models/users/user.model'
+import * as Sequelize from 'sequelize'
import { ResultList } from '../../../shared/models/result-list.model'
import { UserRight } from '../../../shared/models/users/user-right.enum'
import { UserRole } from '../../../shared/models/users/user-role'
+import { User as FormattedUser } from '../../../shared/models/users/user.model'
+import { AccountInstance } from './account-interface'
export namespace UserMethods {
export type HasRight = (this: UserInstance, right: UserRight) => boolean
diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts
index 411a05029..3cfc0fbed 100644
--- a/server/models/job/job-interface.ts
+++ b/server/models/job/job-interface.ts
@@ -1,18 +1,23 @@
+import * as Bluebird from 'bluebird'
import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
-
-import { JobCategory, JobState } from '../../../shared/models/job.model'
+import { Job as FormattedJob, JobCategory, JobState } from '../../../shared/models/job.model'
+import { ResultList } from '../../../shared/models/result-list.model'
export namespace JobMethods {
- export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise
+ export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Bluebird
+ export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList >
+
+ export type ToFormattedJSON = (this: JobInstance) => FormattedJob
}
export interface JobClass {
listWithLimitByCategory: JobMethods.ListWithLimitByCategory
+ listForApi: JobMethods.ListForApi,
}
export interface JobAttributes {
state: JobState
+ category: JobCategory
handlerName: string
handlerInputData: any
}
@@ -21,6 +26,8 @@ export interface JobInstance extends JobClass, JobAttributes, Sequelize.Instance
id: number
createdAt: Date
updatedAt: Date
+
+ toFormattedJSON: JobMethods.ToFormattedJSON
}
export interface JobModel extends JobClass, Sequelize.Model {}
diff --git a/server/models/job/job.ts b/server/models/job/job.ts
index c2d088090..f428e26db 100644
--- a/server/models/job/job.ts
+++ b/server/models/job/job.ts
@@ -1,19 +1,14 @@
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
-
-import { JOB_STATES, JOB_CATEGORIES } from '../../initializers'
-
-import { addMethodsToModel } from '../utils'
-import {
- JobInstance,
- JobAttributes,
-
- JobMethods
-} from './job-interface'
import { JobCategory, JobState } from '../../../shared/models/job.model'
+import { JOB_CATEGORIES, JOB_STATES } from '../../initializers'
+import { addMethodsToModel, getSort } from '../utils'
+import { JobAttributes, JobInstance, JobMethods } from './job-interface'
let Job: Sequelize.Model
let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
+let listForApi: JobMethods.ListForApi
+let toFormattedJSON: JobMethods.ToFormattedJSON
export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Job = sequelize.define('Job',
@@ -44,12 +39,30 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
}
)
- const classMethods = [ listWithLimitByCategory ]
- addMethodsToModel(Job, classMethods)
+ const classMethods = [
+ listWithLimitByCategory,
+ listForApi
+ ]
+ const instanceMethods = [
+ toFormattedJSON
+ ]
+ addMethodsToModel(Job, classMethods, instanceMethods)
return Job
}
+toFormattedJSON = function (this: JobInstance) {
+ return {
+ id: this.id,
+ state: this.state,
+ category: this.category,
+ handlerName: this.handlerName,
+ handlerInputData: this.handlerInputData,
+ createdAt: this.createdAt,
+ updatedAt: this.updatedAt
+ }
+}
+
// ---------------------------------------------------------------------------
listWithLimitByCategory = function (limit: number, state: JobState, jobCategory: JobCategory) {
@@ -66,3 +79,18 @@ listWithLimitByCategory = function (limit: number, state: JobState, jobCategory:
return Job.findAll(query)
}
+
+listForApi = function (start: number, count: number, sort: string) {
+ const query = {
+ offset: start,
+ limit: count,
+ order: [ getSort(sort) ]
+ }
+
+ return Job.findAndCountAll(query).then(({ rows, count }) => {
+ return {
+ data: rows,
+ total: count
+ }
+ })
+}
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 287480808..b22bf054a 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -1,5 +1,6 @@
// Order of the tests we want to execute
import './follows'
+import './jobs'
import './users'
import './services'
import './videos'
diff --git a/server/tests/api/check-params/jobs.ts b/server/tests/api/check-params/jobs.ts
new file mode 100644
index 000000000..7a0dd6e8c
--- /dev/null
+++ b/server/tests/api/check-params/jobs.ts
@@ -0,0 +1,84 @@
+/* tslint:disable:no-unused-expression */
+
+import 'mocha'
+import * as request from 'supertest'
+
+import { createUser, flushTests, getUserAccessToken, killallServers, runServer, ServerInfo, setAccessTokensToServers } from '../../utils'
+
+describe('Test jobs API validators', function () {
+ const path = '/api/v1/jobs/'
+ let server: ServerInfo
+ let userAccessToken = ''
+
+ // ---------------------------------------------------------------
+
+ before(async function () {
+ this.timeout(120000)
+
+ await flushTests()
+
+ server = await runServer(1)
+
+ await setAccessTokensToServers([ server ])
+
+ const user = {
+ username: 'user1',
+ password: 'my super password'
+ }
+ await createUser(server.url, server.accessToken, user.username, user.password)
+ userAccessToken = await getUserAccessToken(server, user)
+ })
+
+ describe('When listing jobs', function () {
+ it('Should fail with a bad start pagination', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ start: 'hello' })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(400)
+ })
+
+ it('Should fail with a bad count pagination', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ count: 'hello' })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(400)
+ })
+
+ it('Should fail with an incorrect sort', async function () {
+ await request(server.url)
+ .get(path)
+ .query({ sort: 'hello' })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + server.accessToken)
+ .expect(400)
+ })
+
+ it('Should fail with a non authenticated user', async function () {
+ await request(server.url)
+ .get(path)
+ .set('Accept', 'application/json')
+ .expect(401)
+ })
+
+ it('Should fail with a non admin user', async function () {
+ await request(server.url)
+ .get(path)
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + userAccessToken)
+ .expect(403)
+ })
+ })
+
+ after(async function () {
+ killallServers([ server ])
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
diff --git a/server/tests/api/index-slow.ts b/server/tests/api/index-slow.ts
index 2448147d8..4cd5b09a2 100644
--- a/server/tests/api/index-slow.ts
+++ b/server/tests/api/index-slow.ts
@@ -3,3 +3,4 @@
import './video-transcoder'
import './multiple-servers'
import './follows'
+import './jobs'
diff --git a/server/tests/api/jobs.ts b/server/tests/api/jobs.ts
new file mode 100644
index 000000000..4d9b61392
--- /dev/null
+++ b/server/tests/api/jobs.ts
@@ -0,0 +1,64 @@
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { flushTests, killallServers, ServerInfo, setAccessTokensToServers, wait } from '../utils'
+import { doubleFollow } from '../utils/follows'
+import { getJobsList, getJobsListPaginationAndSort } from '../utils/jobs'
+import { flushAndRunMultipleServers } from '../utils/servers'
+import { uploadVideo } from '../utils/videos'
+import { dateIsValid } from '../utils/miscs'
+
+const expect = chai.expect
+
+describe('Test jobs', function () {
+ let servers: ServerInfo[]
+
+ before(async function () {
+ this.timeout(30000)
+
+ servers = await flushAndRunMultipleServers(2)
+
+ await setAccessTokensToServers(servers)
+
+ // Server 1 and server 2 follow each other
+ await doubleFollow(servers[0], servers[1])
+ })
+
+ it('Should create some jobs', async function () {
+ this.timeout(30000)
+
+ await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' })
+ await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' })
+
+ await wait(15000)
+ })
+
+ it('Should list jobs', async function () {
+ const res = await getJobsList(servers[1].url, servers[1].accessToken)
+ expect(res.body.total).to.be.above(2)
+ expect(res.body.data).to.have.length.above(2)
+ })
+
+ it('Should list jobs with sort and pagination', async function () {
+ const res = await getJobsListPaginationAndSort(servers[1].url, servers[1].accessToken, 4, 1, 'createdAt')
+ expect(res.body.total).to.be.above(2)
+ expect(res.body.data).to.have.lengthOf(1)
+
+ const job = res.body.data[0]
+ expect(job.state).to.equal('success')
+ expect(job.category).to.equal('transcoding')
+ expect(job.handlerName).to.have.length.above(3)
+ expect(dateIsValid(job.createdAt)).to.be.true
+ expect(dateIsValid(job.updatedAt)).to.be.true
+ })
+
+ after(async function () {
+ killallServers(servers)
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
diff --git a/server/tests/utils/follows.ts b/server/tests/utils/follows.ts
index b88776011..033c6a719 100644
--- a/server/tests/utils/follows.ts
+++ b/server/tests/utils/follows.ts
@@ -61,7 +61,7 @@ async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
])
// Wait request propagation
- await wait(20000)
+ await wait(10000)
return true
}
diff --git a/server/tests/utils/jobs.ts b/server/tests/utils/jobs.ts
new file mode 100644
index 000000000..0a8c51575
--- /dev/null
+++ b/server/tests/utils/jobs.ts
@@ -0,0 +1,33 @@
+import * as request from 'supertest'
+
+function getJobsList (url: string, accessToken: string) {
+ const path = '/api/v1/jobs'
+
+ return request(url)
+ .get(path)
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .expect(200)
+ .expect('Content-Type', /json/)
+}
+
+function getJobsListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string) {
+ const path = '/api/v1/jobs'
+
+ return request(url)
+ .get(path)
+ .query({ start })
+ .query({ count })
+ .query({ sort })
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .expect(200)
+ .expect('Content-Type', /json/)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ getJobsList,
+ getJobsListPaginationAndSort
+}
diff --git a/shared/models/job.model.ts b/shared/models/job.model.ts
index 10696e3f8..1c46a7900 100644
--- a/shared/models/job.model.ts
+++ b/shared/models/job.model.ts
@@ -1,2 +1,12 @@
export type JobState = 'pending' | 'processing' | 'error' | 'success'
export type JobCategory = 'transcoding' | 'activitypub-http'
+
+export interface Job {
+ id: number
+ state: JobState
+ category: JobCategory
+ handlerName: string
+ handlerInputData: any
+ createdAt: Date
+ updatedAt: Date
+}
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index 9460b668e..238e38a36 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -4,6 +4,7 @@ export enum UserRight {
MANAGE_SERVER_FOLLOW,
MANAGE_VIDEO_ABUSES,
MANAGE_VIDEO_BLACKLIST,
+ MANAGE_JOBS,
REMOVE_ANY_VIDEO,
REMOVE_ANY_VIDEO_CHANNEL
}
diff --git a/shared/models/users/user-role.ts b/shared/models/users/user-role.ts
index cc32c768d..954fa426e 100644
--- a/shared/models/users/user-role.ts
+++ b/shared/models/users/user-role.ts
@@ -1,4 +1,5 @@
import { UserRight } from './user-right.enum'
+import user from '../../../server/models/account/user'
// Keep the order
export enum UserRole {