mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-02-25 18:55:32 -06:00
shared/ typescript types dir server-commands
This commit is contained in:
353
shared/server-commands/server/config-command.ts
Normal file
353
shared/server-commands/server/config-command.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import { merge } from 'lodash'
|
||||
import { DeepPartial } from '@shared/typescript-utils'
|
||||
import { About, HttpStatusCode, ServerConfig } from '@shared/models'
|
||||
import { CustomConfig } from '../../models/server/custom-config.model'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
export class ConfigCommand extends AbstractCommand {
|
||||
|
||||
static getCustomConfigResolutions (enabled: boolean) {
|
||||
return {
|
||||
'144p': enabled,
|
||||
'240p': enabled,
|
||||
'360p': enabled,
|
||||
'480p': enabled,
|
||||
'720p': enabled,
|
||||
'1080p': enabled,
|
||||
'1440p': enabled,
|
||||
'2160p': enabled
|
||||
}
|
||||
}
|
||||
|
||||
enableImports () {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
import: {
|
||||
videos: {
|
||||
http: {
|
||||
enabled: true
|
||||
},
|
||||
|
||||
torrent: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enableLive (options: {
|
||||
allowReplay?: boolean
|
||||
transcoding?: boolean
|
||||
} = {}) {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: options.allowReplay ?? true,
|
||||
transcoding: {
|
||||
enabled: options.transcoding ?? true,
|
||||
resolutions: ConfigCommand.getCustomConfigResolutions(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
disableTranscoding () {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
transcoding: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enableTranscoding (webtorrent = true, hls = true) {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
resolutions: ConfigCommand.getCustomConfigResolutions(true),
|
||||
|
||||
webtorrent: {
|
||||
enabled: webtorrent
|
||||
},
|
||||
hls: {
|
||||
enabled: hls
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getConfig (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config'
|
||||
|
||||
return this.getRequestBody<ServerConfig>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getAbout (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config/about'
|
||||
|
||||
return this.getRequestBody<About>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getCustomConfig (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config/custom'
|
||||
|
||||
return this.getRequestBody<CustomConfig>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
updateCustomConfig (options: OverrideCommandOptions & {
|
||||
newCustomConfig: CustomConfig
|
||||
}) {
|
||||
const path = '/api/v1/config/custom'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.newCustomConfig,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
deleteCustomConfig (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config/custom'
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async updateExistingSubConfig (options: OverrideCommandOptions & {
|
||||
newConfig: DeepPartial<CustomConfig>
|
||||
}) {
|
||||
const existing = await this.getCustomConfig(options)
|
||||
|
||||
return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) })
|
||||
}
|
||||
|
||||
updateCustomSubConfig (options: OverrideCommandOptions & {
|
||||
newConfig: DeepPartial<CustomConfig>
|
||||
}) {
|
||||
const newCustomConfig: CustomConfig = {
|
||||
instance: {
|
||||
name: 'PeerTube updated',
|
||||
shortDescription: 'my short description',
|
||||
description: 'my super description',
|
||||
terms: 'my super terms',
|
||||
codeOfConduct: 'my super coc',
|
||||
|
||||
creationReason: 'my super creation reason',
|
||||
moderationInformation: 'my super moderation information',
|
||||
administrator: 'Kuja',
|
||||
maintenanceLifetime: 'forever',
|
||||
businessModel: 'my super business model',
|
||||
hardwareInformation: '2vCore 3GB RAM',
|
||||
|
||||
languages: [ 'en', 'es' ],
|
||||
categories: [ 1, 2 ],
|
||||
|
||||
isNSFW: true,
|
||||
defaultNSFWPolicy: 'blur',
|
||||
|
||||
defaultClientRoute: '/videos/recently-added',
|
||||
|
||||
customizations: {
|
||||
javascript: 'alert("coucou")',
|
||||
css: 'body { background-color: red; }'
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
default: 'default'
|
||||
},
|
||||
services: {
|
||||
twitter: {
|
||||
username: '@MySuperUsername',
|
||||
whitelisted: true
|
||||
}
|
||||
},
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: false
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: false
|
||||
}
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: 2
|
||||
},
|
||||
captions: {
|
||||
size: 3
|
||||
},
|
||||
torrents: {
|
||||
size: 4
|
||||
}
|
||||
},
|
||||
signup: {
|
||||
enabled: false,
|
||||
limit: 5,
|
||||
requiresEmailVerification: false,
|
||||
minimumAge: 16
|
||||
},
|
||||
admin: {
|
||||
email: 'superadmin1@example.com'
|
||||
},
|
||||
contactForm: {
|
||||
enabled: true
|
||||
},
|
||||
user: {
|
||||
videoQuota: 5242881,
|
||||
videoQuotaDaily: 318742
|
||||
},
|
||||
videoChannels: {
|
||||
maxPerUser: 20
|
||||
},
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
allowAdditionalExtensions: true,
|
||||
allowAudioFiles: true,
|
||||
threads: 1,
|
||||
concurrency: 3,
|
||||
profile: 'default',
|
||||
resolutions: {
|
||||
'0p': false,
|
||||
'144p': false,
|
||||
'240p': false,
|
||||
'360p': true,
|
||||
'480p': true,
|
||||
'720p': false,
|
||||
'1080p': false,
|
||||
'1440p': false,
|
||||
'2160p': false
|
||||
},
|
||||
webtorrent: {
|
||||
enabled: true
|
||||
},
|
||||
hls: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: false,
|
||||
maxDuration: -1,
|
||||
maxInstanceLives: -1,
|
||||
maxUserLives: 50,
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
threads: 4,
|
||||
profile: 'default',
|
||||
resolutions: {
|
||||
'144p': true,
|
||||
'240p': true,
|
||||
'360p': true,
|
||||
'480p': true,
|
||||
'720p': true,
|
||||
'1080p': true,
|
||||
'1440p': true,
|
||||
'2160p': true
|
||||
}
|
||||
}
|
||||
},
|
||||
import: {
|
||||
videos: {
|
||||
concurrency: 3,
|
||||
http: {
|
||||
enabled: false
|
||||
},
|
||||
torrent: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
videos: {
|
||||
algorithms: {
|
||||
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
|
||||
default: 'hot'
|
||||
}
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
followers: {
|
||||
instance: {
|
||||
enabled: true,
|
||||
manualApproval: false
|
||||
}
|
||||
},
|
||||
followings: {
|
||||
instance: {
|
||||
autoFollowBack: {
|
||||
enabled: false
|
||||
},
|
||||
autoFollowIndex: {
|
||||
indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: true,
|
||||
level: 'warning',
|
||||
message: 'hello',
|
||||
dismissable: true
|
||||
},
|
||||
search: {
|
||||
remoteUri: {
|
||||
users: true,
|
||||
anonymous: true
|
||||
},
|
||||
searchIndex: {
|
||||
enabled: true,
|
||||
url: 'https://search.joinpeertube.org',
|
||||
disableLocalSearch: true,
|
||||
isDefaultSearch: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
merge(newCustomConfig, options.newConfig)
|
||||
|
||||
return this.updateCustomConfig({ ...options, newCustomConfig })
|
||||
}
|
||||
}
|
||||
31
shared/server-commands/server/contact-form-command.ts
Normal file
31
shared/server-commands/server/contact-form-command.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { HttpStatusCode } from '@shared/models'
|
||||
import { ContactForm } from '../../models/server'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
export class ContactFormCommand extends AbstractCommand {
|
||||
|
||||
send (options: OverrideCommandOptions & {
|
||||
fromEmail: string
|
||||
fromName: string
|
||||
subject: string
|
||||
body: string
|
||||
}) {
|
||||
const path = '/api/v1/server/contact'
|
||||
|
||||
const body: ContactForm = {
|
||||
fromEmail: options.fromEmail,
|
||||
fromName: options.fromName,
|
||||
subject: options.subject,
|
||||
body: options.body
|
||||
}
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: body,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
||||
33
shared/server-commands/server/debug-command.ts
Normal file
33
shared/server-commands/server/debug-command.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
export class DebugCommand extends AbstractCommand {
|
||||
|
||||
getDebug (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/server/debug'
|
||||
|
||||
return this.getRequestBody<Debug>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
sendCommand (options: OverrideCommandOptions & {
|
||||
body: SendDebugCommand
|
||||
}) {
|
||||
const { body } = options
|
||||
const path = '/api/v1/server/debug/run-command'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: body,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
||||
34
shared/server-commands/server/directories.ts
Normal file
34
shared/server-commands/server/directories.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { pathExists, readdir } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { root } from '@shared/core-utils'
|
||||
import { PeerTubeServer } from './server'
|
||||
|
||||
async function checkTmpIsEmpty (server: PeerTubeServer) {
|
||||
await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
|
||||
|
||||
if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
|
||||
await checkDirectoryIsEmpty(server, 'tmp/hls')
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
|
||||
const testDirectory = 'test' + server.internalServerNumber
|
||||
|
||||
const directoryPath = join(root(), testDirectory, directory)
|
||||
|
||||
const directoryExists = await pathExists(directoryPath)
|
||||
expect(directoryExists).to.be.true
|
||||
|
||||
const files = await readdir(directoryPath)
|
||||
const filtered = files.filter(f => exceptions.includes(f) === false)
|
||||
|
||||
expect(filtered).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
export {
|
||||
checkTmpIsEmpty,
|
||||
checkDirectoryIsEmpty
|
||||
}
|
||||
139
shared/server-commands/server/follows-command.ts
Normal file
139
shared/server-commands/server/follows-command.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { pick } from '@shared/core-utils'
|
||||
import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
import { PeerTubeServer } from './server'
|
||||
|
||||
export class FollowsCommand extends AbstractCommand {
|
||||
|
||||
getFollowers (options: OverrideCommandOptions & {
|
||||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
search?: string
|
||||
actorType?: ActivityPubActorType
|
||||
state?: FollowState
|
||||
}) {
|
||||
const path = '/api/v1/server/followers'
|
||||
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<ActorFollow>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getFollowings (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
actorType?: ActivityPubActorType
|
||||
state?: FollowState
|
||||
} = {}) {
|
||||
const path = '/api/v1/server/following'
|
||||
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<ActorFollow>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
follow (options: OverrideCommandOptions & {
|
||||
hosts?: string[]
|
||||
handles?: string[]
|
||||
}) {
|
||||
const path = '/api/v1/server/following'
|
||||
|
||||
const fields: ServerFollowCreate = {}
|
||||
|
||||
if (options.hosts) {
|
||||
fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, ''))
|
||||
}
|
||||
|
||||
if (options.handles) {
|
||||
fields.handles = options.handles
|
||||
}
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async unfollow (options: OverrideCommandOptions & {
|
||||
target: PeerTubeServer | string
|
||||
}) {
|
||||
const { target } = options
|
||||
|
||||
const handle = typeof target === 'string'
|
||||
? target
|
||||
: target.host
|
||||
|
||||
const path = '/api/v1/server/following/' + handle
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
acceptFollower (options: OverrideCommandOptions & {
|
||||
follower: string
|
||||
}) {
|
||||
const path = '/api/v1/server/followers/' + options.follower + '/accept'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
rejectFollower (options: OverrideCommandOptions & {
|
||||
follower: string
|
||||
}) {
|
||||
const path = '/api/v1/server/followers/' + options.follower + '/reject'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeFollower (options: OverrideCommandOptions & {
|
||||
follower: PeerTubeServer
|
||||
}) {
|
||||
const path = '/api/v1/server/followers/peertube@' + options.follower.host
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
||||
20
shared/server-commands/server/follows.ts
Normal file
20
shared/server-commands/server/follows.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { waitJobs } from './jobs'
|
||||
import { PeerTubeServer } from './server'
|
||||
|
||||
async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
|
||||
await Promise.all([
|
||||
server1.follows.follow({ hosts: [ server2.url ] }),
|
||||
server2.follows.follow({ hosts: [ server1.url ] })
|
||||
])
|
||||
|
||||
// Wait request propagation
|
||||
await waitJobs([ server1, server2 ])
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doubleFollow
|
||||
}
|
||||
17
shared/server-commands/server/index.ts
Normal file
17
shared/server-commands/server/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export * from './config-command'
|
||||
export * from './contact-form-command'
|
||||
export * from './debug-command'
|
||||
export * from './directories'
|
||||
export * from './follows-command'
|
||||
export * from './follows'
|
||||
export * from './jobs'
|
||||
export * from './jobs-command'
|
||||
export * from './object-storage-command'
|
||||
export * from './plugins-command'
|
||||
export * from './plugins'
|
||||
export * from './redundancy-command'
|
||||
export * from './server'
|
||||
export * from './servers-command'
|
||||
export * from './servers'
|
||||
export * from './stats-command'
|
||||
export * from './tracker'
|
||||
61
shared/server-commands/server/jobs-command.ts
Normal file
61
shared/server-commands/server/jobs-command.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { pick } from '@shared/core-utils'
|
||||
import { HttpStatusCode } from '@shared/models'
|
||||
import { Job, JobState, JobType, ResultList } from '../../models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
export class JobsCommand extends AbstractCommand {
|
||||
|
||||
async getLatest (options: OverrideCommandOptions & {
|
||||
jobType: JobType
|
||||
}) {
|
||||
const { data } = await this.list({ ...options, start: 0, count: 1, sort: '-createdAt' })
|
||||
|
||||
if (data.length === 0) return undefined
|
||||
|
||||
return data[0]
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
state?: JobState
|
||||
jobType?: JobType
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
} = {}) {
|
||||
const path = this.buildJobsUrl(options.state)
|
||||
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<Job>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listFailed (options: OverrideCommandOptions & {
|
||||
jobType?: JobType
|
||||
}) {
|
||||
const path = this.buildJobsUrl('failed')
|
||||
|
||||
return this.getRequestBody<ResultList<Job>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { start: 0, count: 50 },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
private buildJobsUrl (state?: JobState) {
|
||||
let path = '/api/v1/jobs'
|
||||
|
||||
if (state) path += '/' + state
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
84
shared/server-commands/server/jobs.ts
Normal file
84
shared/server-commands/server/jobs.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { JobState, JobType } from '../../models'
|
||||
import { wait } from '../miscs'
|
||||
import { PeerTubeServer } from './server'
|
||||
|
||||
async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer, skipDelayed = false) {
|
||||
const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
|
||||
? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
|
||||
: 250
|
||||
|
||||
let servers: PeerTubeServer[]
|
||||
|
||||
if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ]
|
||||
else servers = serversArg as PeerTubeServer[]
|
||||
|
||||
const states: JobState[] = [ 'waiting', 'active' ]
|
||||
if (!skipDelayed) states.push('delayed')
|
||||
|
||||
const repeatableJobs: JobType[] = [ 'videos-views-stats', 'activitypub-cleaner' ]
|
||||
let pendingRequests: boolean
|
||||
|
||||
function tasksBuilder () {
|
||||
const tasks: Promise<any>[] = []
|
||||
|
||||
// Check if each server has pending request
|
||||
for (const server of servers) {
|
||||
for (const state of states) {
|
||||
const p = server.jobs.list({
|
||||
state,
|
||||
start: 0,
|
||||
count: 10,
|
||||
sort: '-createdAt'
|
||||
}).then(body => body.data)
|
||||
.then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type)))
|
||||
.then(jobs => {
|
||||
if (jobs.length !== 0) {
|
||||
pendingRequests = true
|
||||
}
|
||||
})
|
||||
|
||||
tasks.push(p)
|
||||
}
|
||||
|
||||
const p = server.debug.getDebug()
|
||||
.then(obj => {
|
||||
if (obj.activityPubMessagesWaiting !== 0) {
|
||||
pendingRequests = true
|
||||
}
|
||||
})
|
||||
|
||||
tasks.push(p)
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
do {
|
||||
pendingRequests = false
|
||||
await Promise.all(tasksBuilder())
|
||||
|
||||
// Retry, in case of new jobs were created
|
||||
if (pendingRequests === false) {
|
||||
await wait(pendingJobWait)
|
||||
await Promise.all(tasksBuilder())
|
||||
}
|
||||
|
||||
if (pendingRequests) {
|
||||
await wait(pendingJobWait)
|
||||
}
|
||||
} while (pendingRequests)
|
||||
}
|
||||
|
||||
async function expectNoFailedTranscodingJob (server: PeerTubeServer) {
|
||||
const { data } = await server.jobs.listFailed({ jobType: 'video-transcoding' })
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
waitJobs,
|
||||
expectNoFailedTranscodingJob
|
||||
}
|
||||
77
shared/server-commands/server/object-storage-command.ts
Normal file
77
shared/server-commands/server/object-storage-command.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
import { HttpStatusCode } from '@shared/models'
|
||||
import { makePostBodyRequest } from '../requests'
|
||||
import { AbstractCommand } from '../shared'
|
||||
|
||||
export class ObjectStorageCommand extends AbstractCommand {
|
||||
static readonly DEFAULT_PLAYLIST_BUCKET = 'streaming-playlists'
|
||||
static readonly DEFAULT_WEBTORRENT_BUCKET = 'videos'
|
||||
|
||||
static getDefaultConfig () {
|
||||
return {
|
||||
object_storage: {
|
||||
enabled: true,
|
||||
endpoint: 'http://' + this.getEndpointHost(),
|
||||
region: this.getRegion(),
|
||||
|
||||
credentials: this.getCredentialsConfig(),
|
||||
|
||||
streaming_playlists: {
|
||||
bucket_name: this.DEFAULT_PLAYLIST_BUCKET
|
||||
},
|
||||
|
||||
videos: {
|
||||
bucket_name: this.DEFAULT_WEBTORRENT_BUCKET
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getCredentialsConfig () {
|
||||
return {
|
||||
access_key_id: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
|
||||
}
|
||||
}
|
||||
|
||||
static getEndpointHost () {
|
||||
return 'localhost:9444'
|
||||
}
|
||||
|
||||
static getRegion () {
|
||||
return 'us-east-1'
|
||||
}
|
||||
|
||||
static getWebTorrentBaseUrl () {
|
||||
return `http://${this.DEFAULT_WEBTORRENT_BUCKET}.${this.getEndpointHost()}/`
|
||||
}
|
||||
|
||||
static getPlaylistBaseUrl () {
|
||||
return `http://${this.DEFAULT_PLAYLIST_BUCKET}.${this.getEndpointHost()}/`
|
||||
}
|
||||
|
||||
static async prepareDefaultBuckets () {
|
||||
await this.createBucket(this.DEFAULT_PLAYLIST_BUCKET)
|
||||
await this.createBucket(this.DEFAULT_WEBTORRENT_BUCKET)
|
||||
}
|
||||
|
||||
static async createBucket (name: string) {
|
||||
await makePostBodyRequest({
|
||||
url: this.getEndpointHost(),
|
||||
path: '/ui/' + name + '?delete',
|
||||
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: this.getEndpointHost(),
|
||||
path: '/ui/' + name + '?create',
|
||||
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: this.getEndpointHost(),
|
||||
path: '/ui/' + name + '?make-public',
|
||||
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
|
||||
})
|
||||
}
|
||||
}
|
||||
257
shared/server-commands/server/plugins-command.ts
Normal file
257
shared/server-commands/server/plugins-command.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { readJSON, writeJSON } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { root } from '@shared/core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
PeerTubePlugin,
|
||||
PeerTubePluginIndex,
|
||||
PeertubePluginIndexList,
|
||||
PluginPackageJson,
|
||||
PluginTranslation,
|
||||
PluginType,
|
||||
PublicServerSetting,
|
||||
RegisteredServerSettings,
|
||||
ResultList
|
||||
} from '@shared/models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
export class PluginsCommand extends AbstractCommand {
|
||||
|
||||
static getPluginTestPath (suffix = '') {
|
||||
return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
pluginType?: PluginType
|
||||
uninstalled?: boolean
|
||||
}) {
|
||||
const { start, count, sort, pluginType, uninstalled } = options
|
||||
const path = '/api/v1/plugins'
|
||||
|
||||
return this.getRequestBody<ResultList<PeerTubePlugin>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
start,
|
||||
count,
|
||||
sort,
|
||||
pluginType,
|
||||
uninstalled
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listAvailable (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
pluginType?: PluginType
|
||||
currentPeerTubeEngine?: string
|
||||
search?: string
|
||||
expectedStatus?: HttpStatusCode
|
||||
}) {
|
||||
const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options
|
||||
const path = '/api/v1/plugins/available'
|
||||
|
||||
const query: PeertubePluginIndexList = {
|
||||
start,
|
||||
count,
|
||||
sort,
|
||||
pluginType,
|
||||
currentPeerTubeEngine,
|
||||
search
|
||||
}
|
||||
|
||||
return this.getRequestBody<ResultList<PeerTubePluginIndex>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
}) {
|
||||
const path = '/api/v1/plugins/' + options.npmName
|
||||
|
||||
return this.getRequestBody<PeerTubePlugin>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
updateSettings (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
settings: any
|
||||
}) {
|
||||
const { npmName, settings } = options
|
||||
const path = '/api/v1/plugins/' + npmName + '/settings'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { settings },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
getRegisteredSettings (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
}) {
|
||||
const path = '/api/v1/plugins/' + options.npmName + '/registered-settings'
|
||||
|
||||
return this.getRequestBody<RegisteredServerSettings>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getPublicSettings (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
}) {
|
||||
const { npmName } = options
|
||||
const path = '/api/v1/plugins/' + npmName + '/public-settings'
|
||||
|
||||
return this.getRequestBody<PublicServerSetting>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getTranslations (options: OverrideCommandOptions & {
|
||||
locale: string
|
||||
}) {
|
||||
const { locale } = options
|
||||
const path = '/plugins/translations/' + locale + '.json'
|
||||
|
||||
return this.getRequestBody<PluginTranslation>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
install (options: OverrideCommandOptions & {
|
||||
path?: string
|
||||
npmName?: string
|
||||
pluginVersion?: string
|
||||
}) {
|
||||
const { npmName, path, pluginVersion } = options
|
||||
const apiPath = '/api/v1/plugins/install'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: apiPath,
|
||||
fields: { npmName, path, pluginVersion },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
path?: string
|
||||
npmName?: string
|
||||
}) {
|
||||
const { npmName, path } = options
|
||||
const apiPath = '/api/v1/plugins/update'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: apiPath,
|
||||
fields: { npmName, path },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
uninstall (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
}) {
|
||||
const { npmName } = options
|
||||
const apiPath = '/api/v1/plugins/uninstall'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: apiPath,
|
||||
fields: { npmName },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
getCSS (options: OverrideCommandOptions = {}) {
|
||||
const path = '/plugins/global.css'
|
||||
|
||||
return this.getRequestText({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getExternalAuth (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
npmVersion: string
|
||||
authName: string
|
||||
query?: any
|
||||
}) {
|
||||
const { npmName, npmVersion, authName, query } = options
|
||||
|
||||
const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
|
||||
|
||||
return this.getRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200,
|
||||
redirects: 0
|
||||
})
|
||||
}
|
||||
|
||||
updatePackageJSON (npmName: string, json: any) {
|
||||
const path = this.getPackageJSONPath(npmName)
|
||||
|
||||
return writeJSON(path, json)
|
||||
}
|
||||
|
||||
getPackageJSON (npmName: string): Promise<PluginPackageJson> {
|
||||
const path = this.getPackageJSONPath(npmName)
|
||||
|
||||
return readJSON(path)
|
||||
}
|
||||
|
||||
private getPackageJSONPath (npmName: string) {
|
||||
return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json'))
|
||||
}
|
||||
}
|
||||
18
shared/server-commands/server/plugins.ts
Normal file
18
shared/server-commands/server/plugins.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { PeerTubeServer } from './server'
|
||||
|
||||
async function testHelloWorldRegisteredSettings (server: PeerTubeServer) {
|
||||
const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' })
|
||||
|
||||
const registeredSettings = body.registeredSettings
|
||||
expect(registeredSettings).to.have.length.at.least(1)
|
||||
|
||||
const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name')
|
||||
expect(adminNameSettings).to.not.be.undefined
|
||||
}
|
||||
|
||||
export {
|
||||
testHelloWorldRegisteredSettings
|
||||
}
|
||||
80
shared/server-commands/server/redundancy-command.ts
Normal file
80
shared/server-commands/server/redundancy-command.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
export class RedundancyCommand extends AbstractCommand {
|
||||
|
||||
updateRedundancy (options: OverrideCommandOptions & {
|
||||
host: string
|
||||
redundancyAllowed: boolean
|
||||
}) {
|
||||
const { host, redundancyAllowed } = options
|
||||
const path = '/api/v1/server/redundancy/' + host
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { redundancyAllowed },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
listVideos (options: OverrideCommandOptions & {
|
||||
target: VideoRedundanciesTarget
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
}) {
|
||||
const path = '/api/v1/server/redundancy/videos'
|
||||
|
||||
const { target, start, count, sort } = options
|
||||
|
||||
return this.getRequestBody<ResultList<VideoRedundancy>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
|
||||
query: {
|
||||
start: start ?? 0,
|
||||
count: count ?? 5,
|
||||
sort: sort ?? 'name',
|
||||
target
|
||||
},
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
addVideo (options: OverrideCommandOptions & {
|
||||
videoId: number
|
||||
}) {
|
||||
const path = '/api/v1/server/redundancy/videos'
|
||||
const { videoId } = options
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { videoId },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeVideo (options: OverrideCommandOptions & {
|
||||
redundancyId: number
|
||||
}) {
|
||||
const { redundancyId } = options
|
||||
const path = '/api/v1/server/redundancy/videos/' + redundancyId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
||||
392
shared/server-commands/server/server.ts
Normal file
392
shared/server-commands/server/server.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
import { ChildProcess, fork } from 'child_process'
|
||||
import { copy } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { root, randomInt } from '@shared/core-utils'
|
||||
import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '../../models/videos'
|
||||
import { BulkCommand } from '../bulk'
|
||||
import { CLICommand } from '../cli'
|
||||
import { CustomPagesCommand } from '../custom-pages'
|
||||
import { FeedCommand } from '../feeds'
|
||||
import { LogsCommand } from '../logs'
|
||||
import { parallelTests, SQLCommand } from '../miscs'
|
||||
import { AbusesCommand } from '../moderation'
|
||||
import { OverviewsCommand } from '../overviews'
|
||||
import { SearchCommand } from '../search'
|
||||
import { SocketIOCommand } from '../socket'
|
||||
import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users'
|
||||
import {
|
||||
BlacklistCommand,
|
||||
CaptionsCommand,
|
||||
ChangeOwnershipCommand,
|
||||
ChannelsCommand,
|
||||
HistoryCommand,
|
||||
ImportsCommand,
|
||||
LiveCommand,
|
||||
PlaylistsCommand,
|
||||
ServicesCommand,
|
||||
StreamingPlaylistsCommand,
|
||||
VideosCommand
|
||||
} from '../videos'
|
||||
import { CommentsCommand } from '../videos/comments-command'
|
||||
import { ConfigCommand } from './config-command'
|
||||
import { ContactFormCommand } from './contact-form-command'
|
||||
import { DebugCommand } from './debug-command'
|
||||
import { FollowsCommand } from './follows-command'
|
||||
import { JobsCommand } from './jobs-command'
|
||||
import { PluginsCommand } from './plugins-command'
|
||||
import { RedundancyCommand } from './redundancy-command'
|
||||
import { ServersCommand } from './servers-command'
|
||||
import { StatsCommand } from './stats-command'
|
||||
import { ObjectStorageCommand } from './object-storage-command'
|
||||
|
||||
export type RunServerOptions = {
|
||||
hideLogs?: boolean
|
||||
nodeArgs?: string[]
|
||||
peertubeArgs?: string[]
|
||||
env?: { [ id: string ]: string }
|
||||
}
|
||||
|
||||
export class PeerTubeServer {
|
||||
app?: ChildProcess
|
||||
|
||||
url: string
|
||||
host?: string
|
||||
hostname?: string
|
||||
port?: number
|
||||
|
||||
rtmpPort?: number
|
||||
rtmpsPort?: number
|
||||
|
||||
parallel?: boolean
|
||||
internalServerNumber: number
|
||||
|
||||
serverNumber?: number
|
||||
customConfigFile?: string
|
||||
|
||||
store?: {
|
||||
client?: {
|
||||
id?: string
|
||||
secret?: string
|
||||
}
|
||||
|
||||
user?: {
|
||||
username: string
|
||||
password: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
channel?: VideoChannel
|
||||
|
||||
video?: Video
|
||||
videoCreated?: VideoCreateResult
|
||||
videoDetails?: VideoDetails
|
||||
|
||||
videos?: { id: number, uuid: string }[]
|
||||
}
|
||||
|
||||
accessToken?: string
|
||||
refreshToken?: string
|
||||
|
||||
bulk?: BulkCommand
|
||||
cli?: CLICommand
|
||||
customPage?: CustomPagesCommand
|
||||
feed?: FeedCommand
|
||||
logs?: LogsCommand
|
||||
abuses?: AbusesCommand
|
||||
overviews?: OverviewsCommand
|
||||
search?: SearchCommand
|
||||
contactForm?: ContactFormCommand
|
||||
debug?: DebugCommand
|
||||
follows?: FollowsCommand
|
||||
jobs?: JobsCommand
|
||||
plugins?: PluginsCommand
|
||||
redundancy?: RedundancyCommand
|
||||
stats?: StatsCommand
|
||||
config?: ConfigCommand
|
||||
socketIO?: SocketIOCommand
|
||||
accounts?: AccountsCommand
|
||||
blocklist?: BlocklistCommand
|
||||
subscriptions?: SubscriptionsCommand
|
||||
live?: LiveCommand
|
||||
services?: ServicesCommand
|
||||
blacklist?: BlacklistCommand
|
||||
captions?: CaptionsCommand
|
||||
changeOwnership?: ChangeOwnershipCommand
|
||||
playlists?: PlaylistsCommand
|
||||
history?: HistoryCommand
|
||||
imports?: ImportsCommand
|
||||
streamingPlaylists?: StreamingPlaylistsCommand
|
||||
channels?: ChannelsCommand
|
||||
comments?: CommentsCommand
|
||||
sql?: SQLCommand
|
||||
notifications?: NotificationsCommand
|
||||
servers?: ServersCommand
|
||||
login?: LoginCommand
|
||||
users?: UsersCommand
|
||||
objectStorage?: ObjectStorageCommand
|
||||
videos?: VideosCommand
|
||||
|
||||
constructor (options: { serverNumber: number } | { url: string }) {
|
||||
if ((options as any).url) {
|
||||
this.setUrl((options as any).url)
|
||||
} else {
|
||||
this.setServerNumber((options as any).serverNumber)
|
||||
}
|
||||
|
||||
this.store = {
|
||||
client: {
|
||||
id: null,
|
||||
secret: null
|
||||
},
|
||||
user: {
|
||||
username: null,
|
||||
password: null
|
||||
}
|
||||
}
|
||||
|
||||
this.assignCommands()
|
||||
}
|
||||
|
||||
setServerNumber (serverNumber: number) {
|
||||
this.serverNumber = serverNumber
|
||||
|
||||
this.parallel = parallelTests()
|
||||
|
||||
this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
|
||||
this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
|
||||
this.rtmpsPort = this.parallel ? this.randomRTMP() : 1937
|
||||
this.port = 9000 + this.internalServerNumber
|
||||
|
||||
this.url = `http://localhost:${this.port}`
|
||||
this.host = `localhost:${this.port}`
|
||||
this.hostname = 'localhost'
|
||||
}
|
||||
|
||||
setUrl (url: string) {
|
||||
const parsed = new URL(url)
|
||||
|
||||
this.url = url
|
||||
this.host = parsed.host
|
||||
this.hostname = parsed.hostname
|
||||
this.port = parseInt(parsed.port)
|
||||
}
|
||||
|
||||
async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) {
|
||||
await ServersCommand.flushTests(this.internalServerNumber)
|
||||
|
||||
return this.run(configOverride, options)
|
||||
}
|
||||
|
||||
async run (configOverrideArg?: any, options: RunServerOptions = {}) {
|
||||
// These actions are async so we need to be sure that they have both been done
|
||||
const serverRunString = {
|
||||
'HTTP server listening': false
|
||||
}
|
||||
const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
|
||||
serverRunString[key] = false
|
||||
|
||||
const regexps = {
|
||||
client_id: 'Client id: (.+)',
|
||||
client_secret: 'Client secret: (.+)',
|
||||
user_username: 'Username: (.+)',
|
||||
user_password: 'User password: (.+)'
|
||||
}
|
||||
|
||||
await this.assignCustomConfigFile()
|
||||
|
||||
const configOverride = this.buildConfigOverride()
|
||||
|
||||
if (configOverrideArg !== undefined) {
|
||||
Object.assign(configOverride, configOverrideArg)
|
||||
}
|
||||
|
||||
// Share the environment
|
||||
const env = Object.create(process.env)
|
||||
env['NODE_ENV'] = 'test'
|
||||
env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
|
||||
env['NODE_CONFIG'] = JSON.stringify(configOverride)
|
||||
|
||||
if (options.env) {
|
||||
Object.assign(env, options.env)
|
||||
}
|
||||
|
||||
const forkOptions = {
|
||||
silent: true,
|
||||
env,
|
||||
detached: true,
|
||||
execArgv: options.nodeArgs || []
|
||||
}
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
const self = this
|
||||
let aggregatedLogs = ''
|
||||
|
||||
this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
|
||||
|
||||
const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs))
|
||||
const onParentExit = () => {
|
||||
if (!this.app || !this.app.pid) return
|
||||
|
||||
try {
|
||||
process.kill(self.app.pid)
|
||||
} catch { /* empty */ }
|
||||
}
|
||||
|
||||
this.app.on('exit', onPeerTubeExit)
|
||||
process.on('exit', onParentExit)
|
||||
|
||||
this.app.stdout.on('data', function onStdout (data) {
|
||||
let dontContinue = false
|
||||
|
||||
const log: string = data.toString()
|
||||
aggregatedLogs += log
|
||||
|
||||
// Capture things if we want to
|
||||
for (const key of Object.keys(regexps)) {
|
||||
const regexp = regexps[key]
|
||||
const matches = log.match(regexp)
|
||||
if (matches !== null) {
|
||||
if (key === 'client_id') self.store.client.id = matches[1]
|
||||
else if (key === 'client_secret') self.store.client.secret = matches[1]
|
||||
else if (key === 'user_username') self.store.user.username = matches[1]
|
||||
else if (key === 'user_password') self.store.user.password = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all required sentences are here
|
||||
for (const key of Object.keys(serverRunString)) {
|
||||
if (log.includes(key)) serverRunString[key] = true
|
||||
if (serverRunString[key] === false) dontContinue = true
|
||||
}
|
||||
|
||||
// If no, there is maybe one thing not already initialized (client/user credentials generation...)
|
||||
if (dontContinue === true) return
|
||||
|
||||
if (options.hideLogs === false) {
|
||||
console.log(log)
|
||||
} else {
|
||||
process.removeListener('exit', onParentExit)
|
||||
self.app.stdout.removeListener('data', onStdout)
|
||||
self.app.removeListener('exit', onPeerTubeExit)
|
||||
}
|
||||
|
||||
res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async kill () {
|
||||
if (!this.app) return
|
||||
|
||||
await this.sql.cleanup()
|
||||
|
||||
process.kill(-this.app.pid)
|
||||
|
||||
this.app = null
|
||||
}
|
||||
|
||||
private randomServer () {
|
||||
const low = 10
|
||||
const high = 10000
|
||||
|
||||
return randomInt(low, high)
|
||||
}
|
||||
|
||||
private randomRTMP () {
|
||||
const low = 1900
|
||||
const high = 2100
|
||||
|
||||
return randomInt(low, high)
|
||||
}
|
||||
|
||||
private async assignCustomConfigFile () {
|
||||
if (this.internalServerNumber === this.serverNumber) return
|
||||
|
||||
const basePath = join(root(), 'config')
|
||||
|
||||
const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
|
||||
await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
|
||||
|
||||
this.customConfigFile = tmpConfigFile
|
||||
}
|
||||
|
||||
private buildConfigOverride () {
|
||||
if (!this.parallel) return {}
|
||||
|
||||
return {
|
||||
listen: {
|
||||
port: this.port
|
||||
},
|
||||
webserver: {
|
||||
port: this.port
|
||||
},
|
||||
database: {
|
||||
suffix: '_test' + this.internalServerNumber
|
||||
},
|
||||
storage: {
|
||||
tmp: `test${this.internalServerNumber}/tmp/`,
|
||||
bin: `test${this.internalServerNumber}/bin/`,
|
||||
avatars: `test${this.internalServerNumber}/avatars/`,
|
||||
videos: `test${this.internalServerNumber}/videos/`,
|
||||
streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`,
|
||||
redundancy: `test${this.internalServerNumber}/redundancy/`,
|
||||
logs: `test${this.internalServerNumber}/logs/`,
|
||||
previews: `test${this.internalServerNumber}/previews/`,
|
||||
thumbnails: `test${this.internalServerNumber}/thumbnails/`,
|
||||
torrents: `test${this.internalServerNumber}/torrents/`,
|
||||
captions: `test${this.internalServerNumber}/captions/`,
|
||||
cache: `test${this.internalServerNumber}/cache/`,
|
||||
plugins: `test${this.internalServerNumber}/plugins/`
|
||||
},
|
||||
admin: {
|
||||
email: `admin${this.internalServerNumber}@example.com`
|
||||
},
|
||||
live: {
|
||||
rtmp: {
|
||||
port: this.rtmpPort
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private assignCommands () {
|
||||
this.bulk = new BulkCommand(this)
|
||||
this.cli = new CLICommand(this)
|
||||
this.customPage = new CustomPagesCommand(this)
|
||||
this.feed = new FeedCommand(this)
|
||||
this.logs = new LogsCommand(this)
|
||||
this.abuses = new AbusesCommand(this)
|
||||
this.overviews = new OverviewsCommand(this)
|
||||
this.search = new SearchCommand(this)
|
||||
this.contactForm = new ContactFormCommand(this)
|
||||
this.debug = new DebugCommand(this)
|
||||
this.follows = new FollowsCommand(this)
|
||||
this.jobs = new JobsCommand(this)
|
||||
this.plugins = new PluginsCommand(this)
|
||||
this.redundancy = new RedundancyCommand(this)
|
||||
this.stats = new StatsCommand(this)
|
||||
this.config = new ConfigCommand(this)
|
||||
this.socketIO = new SocketIOCommand(this)
|
||||
this.accounts = new AccountsCommand(this)
|
||||
this.blocklist = new BlocklistCommand(this)
|
||||
this.subscriptions = new SubscriptionsCommand(this)
|
||||
this.live = new LiveCommand(this)
|
||||
this.services = new ServicesCommand(this)
|
||||
this.blacklist = new BlacklistCommand(this)
|
||||
this.captions = new CaptionsCommand(this)
|
||||
this.changeOwnership = new ChangeOwnershipCommand(this)
|
||||
this.playlists = new PlaylistsCommand(this)
|
||||
this.history = new HistoryCommand(this)
|
||||
this.imports = new ImportsCommand(this)
|
||||
this.streamingPlaylists = new StreamingPlaylistsCommand(this)
|
||||
this.channels = new ChannelsCommand(this)
|
||||
this.comments = new CommentsCommand(this)
|
||||
this.sql = new SQLCommand(this)
|
||||
this.notifications = new NotificationsCommand(this)
|
||||
this.servers = new ServersCommand(this)
|
||||
this.login = new LoginCommand(this)
|
||||
this.users = new UsersCommand(this)
|
||||
this.videos = new VideosCommand(this)
|
||||
this.objectStorage = new ObjectStorageCommand(this)
|
||||
}
|
||||
}
|
||||
92
shared/server-commands/server/servers-command.ts
Normal file
92
shared/server-commands/server/servers-command.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { exec } from 'child_process'
|
||||
import { copy, ensureDir, readFile, remove } from 'fs-extra'
|
||||
import { basename, join } from 'path'
|
||||
import { root } from '@shared/core-utils'
|
||||
import { HttpStatusCode } from '@shared/models'
|
||||
import { getFileSize, isGithubCI, wait } from '../miscs'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
export class ServersCommand extends AbstractCommand {
|
||||
|
||||
static flushTests (internalServerNumber: number) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
const suffix = ` -- ${internalServerNumber}`
|
||||
|
||||
return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
|
||||
if (err || stderr) return rej(err || new Error(stderr))
|
||||
|
||||
return res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ping (options: OverrideCommandOptions = {}) {
|
||||
return this.getRequestBody({
|
||||
...options,
|
||||
|
||||
path: '/api/v1/ping',
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupTests () {
|
||||
const p: Promise<any>[] = []
|
||||
|
||||
if (isGithubCI()) {
|
||||
await ensureDir('artifacts')
|
||||
|
||||
const origin = this.buildDirectory('logs/peertube.log')
|
||||
const destname = `peertube-${this.server.internalServerNumber}.log`
|
||||
console.log('Saving logs %s.', destname)
|
||||
|
||||
await copy(origin, join('artifacts', destname))
|
||||
}
|
||||
|
||||
if (this.server.parallel) {
|
||||
p.push(ServersCommand.flushTests(this.server.internalServerNumber))
|
||||
}
|
||||
|
||||
if (this.server.customConfigFile) {
|
||||
p.push(remove(this.server.customConfigFile))
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
async waitUntilLog (str: string, count = 1, strictCount = true) {
|
||||
const logfile = this.buildDirectory('logs/peertube.log')
|
||||
|
||||
while (true) {
|
||||
const buf = await readFile(logfile)
|
||||
|
||||
const matches = buf.toString().match(new RegExp(str, 'g'))
|
||||
if (matches && matches.length === count) return
|
||||
if (matches && strictCount === false && matches.length >= count) return
|
||||
|
||||
await wait(1000)
|
||||
}
|
||||
}
|
||||
|
||||
buildDirectory (directory: string) {
|
||||
return join(root(), 'test' + this.server.internalServerNumber, directory)
|
||||
}
|
||||
|
||||
buildWebTorrentFilePath (fileUrl: string) {
|
||||
return this.buildDirectory(join('videos', basename(fileUrl)))
|
||||
}
|
||||
|
||||
buildFragmentedFilePath (videoUUID: string, fileUrl: string) {
|
||||
return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
|
||||
}
|
||||
|
||||
getLogContent () {
|
||||
return readFile(this.buildDirectory('logs/peertube.log'))
|
||||
}
|
||||
|
||||
async getServerFileSize (subPath: string) {
|
||||
const path = this.server.servers.buildDirectory(subPath)
|
||||
|
||||
return getFileSize(path)
|
||||
}
|
||||
}
|
||||
49
shared/server-commands/server/servers.ts
Normal file
49
shared/server-commands/server/servers.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ensureDir } from 'fs-extra'
|
||||
import { isGithubCI } from '../miscs'
|
||||
import { PeerTubeServer, RunServerOptions } from './server'
|
||||
|
||||
async function createSingleServer (serverNumber: number, configOverride?: Object, options: RunServerOptions = {}) {
|
||||
const server = new PeerTubeServer({ serverNumber })
|
||||
|
||||
await server.flushAndRun(configOverride, options)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
function createMultipleServers (totalServers: number, configOverride?: Object, options: RunServerOptions = {}) {
|
||||
const serverPromises: Promise<PeerTubeServer>[] = []
|
||||
|
||||
for (let i = 1; i <= totalServers; i++) {
|
||||
serverPromises.push(createSingleServer(i, configOverride, options))
|
||||
}
|
||||
|
||||
return Promise.all(serverPromises)
|
||||
}
|
||||
|
||||
async function killallServers (servers: PeerTubeServer[]) {
|
||||
return Promise.all(servers.map(s => s.kill()))
|
||||
}
|
||||
|
||||
async function cleanupTests (servers: PeerTubeServer[]) {
|
||||
await killallServers(servers)
|
||||
|
||||
if (isGithubCI()) {
|
||||
await ensureDir('artifacts')
|
||||
}
|
||||
|
||||
let p: Promise<any>[] = []
|
||||
for (const server of servers) {
|
||||
p = p.concat(server.servers.cleanupTests())
|
||||
}
|
||||
|
||||
return Promise.all(p)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
createSingleServer,
|
||||
createMultipleServers,
|
||||
cleanupTests,
|
||||
killallServers
|
||||
}
|
||||
25
shared/server-commands/server/stats-command.ts
Normal file
25
shared/server-commands/server/stats-command.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { HttpStatusCode, ServerStats } from '@shared/models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
export class StatsCommand extends AbstractCommand {
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
useCache?: boolean // default false
|
||||
} = {}) {
|
||||
const { useCache = false } = options
|
||||
const path = '/api/v1/server/stats'
|
||||
|
||||
const query = {
|
||||
t: useCache ? undefined : new Date().getTime()
|
||||
}
|
||||
|
||||
return this.getRequestBody<ServerStats>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
||||
27
shared/server-commands/server/tracker.ts
Normal file
27
shared/server-commands/server/tracker.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect } from 'chai'
|
||||
import { sha1 } from '@shared/core-utils/crypto'
|
||||
import { makeGetRequest } from '../requests'
|
||||
|
||||
async function hlsInfohashExist (serverUrl: string, masterPlaylistUrl: string, fileNumber: number) {
|
||||
const path = '/tracker/announce'
|
||||
|
||||
const infohash = sha1(`2${masterPlaylistUrl}+V${fileNumber}`)
|
||||
|
||||
// From bittorrent-tracker
|
||||
const infohashBinary = escape(Buffer.from(infohash, 'hex').toString('binary')).replace(/[@*/+]/g, function (char) {
|
||||
return '%' + char.charCodeAt(0).toString(16).toUpperCase()
|
||||
})
|
||||
|
||||
const res = await makeGetRequest({
|
||||
url: serverUrl,
|
||||
path,
|
||||
rawQuery: `peer_id=-WW0105-NkvYO/egUAr4&info_hash=${infohashBinary}&port=42100`,
|
||||
expectedStatus: 200
|
||||
})
|
||||
|
||||
expect(res.text).to.not.contain('failure')
|
||||
}
|
||||
|
||||
export {
|
||||
hlsInfohashExist
|
||||
}
|
||||
Reference in New Issue
Block a user