TRANSCODING_THREADS
} from '@app/shared/forms/form-validators/custom-config'
import { NotificationsService } from 'angular2-notifications'
-import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model'
+import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
@Component({
selector: 'my-edit-custom-config',
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
-import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model'
+import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
import { environment } from '../../../../environments/environment'
import { RestExtractor, RestService } from '../../../shared'
import { Observable } from 'rxjs/Observable'
import { ResultList } from '../../../../../../shared'
import { JobState } from '../../../../../../shared/models'
-import { Job } from '../../../../../../shared/models/job.model'
+import { Job } from '../../../../../../shared/models/server/job.model'
import { environment } from '../../../../environments/environment'
import { RestExtractor, RestPagination, RestService } from '../../../shared'
import 'rxjs/add/operator/do'
import { ReplaySubject } from 'rxjs/ReplaySubject'
import { ServerConfig } from '../../../../../shared'
-import { About } from '../../../../../shared/models/config/about.model'
+import { About } from '../../../../../shared/models/server/about.model'
import { environment } from '../../../environments/environment'
@Injectable()
specificActor = res.locals.videoChannel
}
+ logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor)
+
await processActivities(activities, res.locals.signature.actor, specificActor)
res.status(204).end()
import * as express from 'express'
import { omit } from 'lodash'
import { ServerConfig, UserRight } from '../../../shared'
-import { About } from '../../../shared/models/config/about.model'
-import { CustomConfig } from '../../../shared/models/config/custom-config.model'
+import { About } from '../../../shared/models/server/about.model'
+import { CustomConfig } from '../../../shared/models/server/custom-config.model'
import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils'
import { isSignupAllowed } from '../../helpers/utils'
import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers'
import * as express from 'express'
import { serverFollowsRouter } from './follows'
+import { statsRouter } from './stats'
const serverRouter = express.Router()
serverRouter.use('/', serverFollowsRouter)
+serverRouter.use('/', statsRouter)
// ---------------------------------------------------------------------------
--- /dev/null
+import * as express from 'express'
+import { ServerStats } from '../../../../shared/models/server/server-stats.model'
+import { asyncMiddleware } from '../../../middlewares'
+import { UserModel } from '../../../models/account/user'
+import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { VideoModel } from '../../../models/video/video'
+import { VideoCommentModel } from '../../../models/video/video-comment'
+
+const statsRouter = express.Router()
+
+statsRouter.get('/stats',
+ asyncMiddleware(getStats)
+)
+
+async function getStats (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats()
+ const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats()
+ const { totalUsers } = await UserModel.getStats()
+ const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats()
+
+ const data: ServerStats = {
+ totalLocalVideos,
+ totalLocalVideoViews,
+ totalVideos,
+ totalLocalVideoComments,
+ totalVideoComments,
+ totalUsers,
+ totalInstanceFollowers,
+ totalInstanceFollowing
+ }
+
+ return res.json(data).end()
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ statsRouter
+}
import { OAuthTokenModel } from '../oauth/oauth-token'
import { getSort, throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel'
+import { VideoCommentModel } from '../video/video-comment'
import { AccountModel } from './account'
@DefaultScope({
})
}
+ static async getStats () {
+ const totalUsers = await UserModel.count()
+
+ return {
+ totalUsers
+ }
+ }
+
hasRight (right: UserRight) {
return hasUserRight(this.role, right)
}
import { FollowState } from '../../../shared/models/actors'
import { AccountFollow } from '../../../shared/models/actors/follow.model'
import { logger } from '../../helpers/logger'
+import { getServerActor } from '../../helpers/utils'
import { ACTOR_FOLLOW_SCORE } from '../../initializers'
import { FOLLOW_STATES } from '../../initializers/constants'
import { ServerModel } from '../server/server'
return ActorFollowModel.findOne(query)
}
- static loadByFollowerInbox (url: string, t?: Sequelize.Transaction) {
- const query = {
- where: {
- state: 'accepted'
- },
- include: [
- {
- model: ActorModel,
- required: true,
- as: 'ActorFollower',
- where: {
- [Sequelize.Op.or]: [
- {
- inboxUrl: url
- },
- {
- sharedInboxUrl: url
- }
- ]
- }
- }
- ],
- transaction: t
- } as any // FIXME: typings does not work
-
- return ActorFollowModel.findOne(query)
- }
-
static listFollowingForApi (id: number, start: number, count: number, sort: string) {
const query = {
distinct: true,
return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
}
+ static async getStats () {
+ const serverActor = await getServerActor()
+
+ const totalInstanceFollowing = await ActorFollowModel.count({
+ where: {
+ actorId: serverActor.id
+ }
+ })
+
+ const totalInstanceFollowers = await ActorFollowModel.count({
+ where: {
+ targetActorId: serverActor.id
+ }
+ })
+
+ return {
+ totalInstanceFollowing,
+ totalInstanceFollowers
+ }
+ }
+
private static async createListAcceptedFollowForApiQuery (
type: 'followers' | 'following',
actorIds: number[],
.findAll(query)
}
+ static async getStats () {
+ const totalLocalVideoComments = await VideoCommentModel.count({
+ include: [
+ {
+ model: AccountModel,
+ required: true,
+ include: [
+ {
+ model: ActorModel,
+ required: true,
+ where: {
+ serverId: null
+ }
+ }
+ ]
+ }
+ ]
+ })
+ const totalVideoComments = await VideoCommentModel.count()
+
+ return {
+ totalLocalVideoComments,
+ totalVideoComments
+ }
+ }
+
getThreadId (): number {
return this.originCommentId || this.id
}
.findOne(options)
}
+ static async getStats () {
+ const totalLocalVideos = await VideoModel.count({
+ where: {
+ remote: false
+ }
+ })
+ const totalVideos = await VideoModel.count()
+
+ let totalLocalVideoViews = await VideoModel.sum('views', {
+ where: {
+ remote: false
+ }
+ })
+ // Sequelize could return null...
+ if (!totalLocalVideoViews) totalLocalVideoViews = 0
+
+ return {
+ totalLocalVideos,
+ totalLocalVideoViews,
+ totalVideos
+ }
+ }
+
getOriginalFile () {
if (Array.isArray(this.VideoFiles) === false) return undefined
import { omit } from 'lodash'
import 'mocha'
-import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
+import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
import {
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,
// Order of the tests we want to execute
-import './server/config'
+import './server/stats'
import './check-params'
import './users/users'
import './videos/single-server'
import './videos/video-privacy'
import './videos/services'
import './server/email'
+import './server/config'
// Order of the tests we want to execute
-// import './multiple-servers'
import './videos/video-transcoder'
import './videos/multiple-servers'
import './server/follows'
import 'mocha'
import * as chai from 'chai'
-import { About } from '../../../../shared/models/config/about.model'
-import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
+import { About } from '../../../../shared/models/server/about.model'
+import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils'
const expect = chai.expect
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { ServerStats } from '../../../../shared/models/server/server-stats.model'
+import {
+ createUser,
+ doubleFollow,
+ flushAndRunMultipleServers,
+ follow,
+ killallServers,
+ ServerInfo,
+ uploadVideo,
+ viewVideo,
+ wait
+} from '../../utils'
+import { flushTests, setAccessTokensToServers } from '../../utils/index'
+import { getStats } from '../../utils/server/stats'
+import { addVideoCommentThread } from '../../utils/videos/video-comments'
+
+const expect = chai.expect
+
+describe('Test stats', function () {
+ let servers: ServerInfo[] = []
+
+ before(async function () {
+ this.timeout(60000)
+
+ await flushTests()
+ servers = await flushAndRunMultipleServers(3)
+ await setAccessTokensToServers(servers)
+
+ await doubleFollow(servers[0], servers[1])
+
+ const user = {
+ username: 'user1',
+ password: 'super_password'
+ }
+ await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
+
+ const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, {})
+ const videoUUID = resVideo.body.video.uuid
+
+ await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment')
+
+ await viewVideo(servers[0].url, videoUUID)
+
+ await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
+ await wait(5000)
+ })
+
+ it('Should have the correct stats on instance 1', async function () {
+ const res = await getStats(servers[0].url)
+ const data: ServerStats = res.body
+
+ expect(data.totalLocalVideoComments).to.equal(1)
+ expect(data.totalLocalVideos).to.equal(1)
+ expect(data.totalLocalVideoViews).to.equal(1)
+ expect(data.totalUsers).to.equal(2)
+ expect(data.totalVideoComments).to.equal(1)
+ expect(data.totalVideos).to.equal(1)
+ expect(data.totalInstanceFollowers).to.equal(2)
+ expect(data.totalInstanceFollowing).to.equal(1)
+ })
+
+ it('Should have the correct stats on instance 2', async function () {
+ const res = await getStats(servers[1].url)
+ const data: ServerStats = res.body
+
+ expect(data.totalLocalVideoComments).to.equal(0)
+ expect(data.totalLocalVideos).to.equal(0)
+ expect(data.totalLocalVideoViews).to.equal(0)
+ expect(data.totalUsers).to.equal(1)
+ expect(data.totalVideoComments).to.equal(1)
+ expect(data.totalVideos).to.equal(1)
+ expect(data.totalInstanceFollowers).to.equal(1)
+ expect(data.totalInstanceFollowing).to.equal(1)
+ })
+
+ it('Should have the correct stats on instance 3', async function () {
+ const res = await getStats(servers[2].url)
+ const data: ServerStats = res.body
+
+ expect(data.totalLocalVideoComments).to.equal(0)
+ expect(data.totalLocalVideos).to.equal(0)
+ expect(data.totalLocalVideoViews).to.equal(0)
+ expect(data.totalUsers).to.equal(1)
+ expect(data.totalVideoComments).to.equal(1)
+ expect(data.totalVideos).to.equal(1)
+ expect(data.totalInstanceFollowing).to.equal(1)
+ expect(data.totalInstanceFollowers).to.equal(0)
+ })
+
+ after(async function () {
+ killallServers(servers)
+
+ // Keep the logs if the test failed
+ if (this['ok']) {
+ await flushTests()
+ }
+ })
+})
import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../'
-import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
+import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
function getConfig (url: string) {
const path = '/api/v1/config'
--- /dev/null
+import { makeGetRequest } from '../'
+
+function getStats (url: string) {
+ const path = '/api/v1/server/stats'
+
+ return makeGetRequest({
+ url,
+ path,
+ statusCodeExpected: 200
+ })
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ getStats
+}
+++ /dev/null
-export interface About {
- instance: {
- name: string
- description: string
- terms: string
- }
-}
+++ /dev/null
-export interface CustomConfig {
- instance: {
- name: string
- description: string
- terms: string
- customizations: {
- javascript?: string
- css?: string
- }
- }
-
- cache: {
- previews: {
- size: number
- }
- }
-
- signup: {
- enabled: boolean
- limit: number
- }
-
- admin: {
- email: string
- }
-
- user: {
- videoQuota: number
- }
-
- transcoding: {
- enabled: boolean
- threads: number
- resolutions: {
- '240p': boolean
- '360p': boolean
- '480p': boolean
- '720p': boolean
- '1080p': boolean
- }
- }
-}
+++ /dev/null
-export interface Customization {
- instance: {
- customization: {
- javascript: string
- css: string
- }
- }
-}
+++ /dev/null
-export interface ServerConfig {
- serverVersion: string
-
- instance: {
- name: string;
- customizations: {
- javascript: string
- css: string
- }
- }
-
- signup: {
- allowed: boolean
- }
-
- transcoding: {
- enabledResolutions: number[]
- }
-
- avatar: {
- file: {
- size: {
- max: number
- },
- extensions: string[]
- }
- }
-
- video: {
- image: {
- size: {
- max: number
- }
- extensions: string[]
- },
- file: {
- extensions: string[]
- }
- }
-}
export * from './activitypub'
export * from './users'
export * from './videos'
-export * from './job.model'
+export * from './server/job.model'
export * from './oauth-client-local.model'
export * from './result-list.model'
-export * from './config/server-config.model'
+export * from './server/server-config.model'
+++ /dev/null
-export type JobState = 'active' | 'complete' | 'failed' | 'inactive' | 'delayed'
-
-export type JobType = 'activitypub-http-unicast' |
- 'activitypub-http-broadcast' |
- 'activitypub-http-fetcher' |
- 'video-file' |
- 'email'
-
-export interface Job {
- id: number
- state: JobState
- type: JobType
- data: any,
- error: any,
- createdAt: Date
- updatedAt: Date
-}
--- /dev/null
+export interface About {
+ instance: {
+ name: string
+ description: string
+ terms: string
+ }
+}
--- /dev/null
+export interface CustomConfig {
+ instance: {
+ name: string
+ description: string
+ terms: string
+ customizations: {
+ javascript?: string
+ css?: string
+ }
+ }
+
+ cache: {
+ previews: {
+ size: number
+ }
+ }
+
+ signup: {
+ enabled: boolean
+ limit: number
+ }
+
+ admin: {
+ email: string
+ }
+
+ user: {
+ videoQuota: number
+ }
+
+ transcoding: {
+ enabled: boolean
+ threads: number
+ resolutions: {
+ '240p': boolean
+ '360p': boolean
+ '480p': boolean
+ '720p': boolean
+ '1080p': boolean
+ }
+ }
+}
--- /dev/null
+export interface Customization {
+ instance: {
+ customization: {
+ javascript: string
+ css: string
+ }
+ }
+}
--- /dev/null
+export type JobState = 'active' | 'complete' | 'failed' | 'inactive' | 'delayed'
+
+export type JobType = 'activitypub-http-unicast' |
+ 'activitypub-http-broadcast' |
+ 'activitypub-http-fetcher' |
+ 'video-file' |
+ 'email'
+
+export interface Job {
+ id: number
+ state: JobState
+ type: JobType
+ data: any,
+ error: any,
+ createdAt: Date
+ updatedAt: Date
+}
--- /dev/null
+export interface ServerConfig {
+ serverVersion: string
+
+ instance: {
+ name: string;
+ customizations: {
+ javascript: string
+ css: string
+ }
+ }
+
+ signup: {
+ allowed: boolean
+ }
+
+ transcoding: {
+ enabledResolutions: number[]
+ }
+
+ avatar: {
+ file: {
+ size: {
+ max: number
+ },
+ extensions: string[]
+ }
+ }
+
+ video: {
+ image: {
+ size: {
+ max: number
+ }
+ extensions: string[]
+ },
+ file: {
+ extensions: string[]
+ }
+ }
+}
--- /dev/null
+export interface ServerStats {
+ totalUsers: number
+ totalLocalVideos: number
+ totalLocalVideoViews: number
+ totalLocalVideoComments: number
+
+ totalVideos: number
+ totalVideoComments: number
+
+ totalInstanceFollowers: number
+ totalInstanceFollowing: number
+}