Add redundancy CLI

pull/2438/head
Chocobozzz 2020-01-28 11:07:23 +01:00 committed by Chocobozzz
parent b764380ac2
commit 26fcf2efeb
8 changed files with 308 additions and 41 deletions

View File

@ -6,15 +6,15 @@ import {
addVideoChannel, addVideoChannel,
buildAbsoluteFixturePath, buildAbsoluteFixturePath,
cleanupTests, cleanupTests,
createUser, createUser, doubleFollow,
execCLI, execCLI,
flushAndRunServer, flushAndRunServer,
getEnvCli, getEnvCli, getLocalIdByUUID,
getVideo, getVideo,
getVideosList, getVideosList,
getVideosListWithToken, removeVideo, getVideosListWithToken, removeVideo,
ServerInfo, ServerInfo,
setAccessTokensToServers, setAccessTokensToServers, uploadVideo, uploadVideoAndGetId,
userLogin, userLogin,
waitJobs waitJobs
} from '../../../shared/extra-utils' } from '../../../shared/extra-utils'
@ -210,6 +210,75 @@ describe('Test CLI wrapper', function () {
}) })
}) })
describe('Manage video redundancies', function () {
let anotherServer: ServerInfo
let video1Server2: number
let servers: ServerInfo[]
before(async function () {
this.timeout(120000)
anotherServer = await flushAndRunServer(2)
await setAccessTokensToServers([ anotherServer ])
await doubleFollow(server, anotherServer)
servers = [ server, anotherServer ]
await waitJobs(servers)
const uuid = (await uploadVideoAndGetId({ server: anotherServer, videoName: 'super video' })).uuid
await waitJobs(servers)
video1Server2 = await getLocalIdByUUID(server.url, uuid)
})
it('Should add a redundancy', async function () {
this.timeout(60000)
const env = getEnvCli(server)
const params = `add --video ${video1Server2}`
await execCLI(`${env} ${cmd} redundancy ${params}`)
await waitJobs(servers)
})
it('Should list redundancies', async function () {
this.timeout(60000)
{
const env = getEnvCli(server)
const params = `list-my-redundancies`
const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`)
expect(stdout).to.contain('super video')
expect(stdout).to.contain(`localhost:${server.port}`)
}
})
it('Should remove a redundancy', async function () {
this.timeout(60000)
const env = getEnvCli(server)
const params = `remove --video ${video1Server2}`
await execCLI(`${env} ${cmd} redundancy ${params}`)
await waitJobs(servers)
{
const env = getEnvCli(server)
const params = `list-my-redundancies`
const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`)
expect(stdout).to.not.contain('super video')
}
})
})
after(async function () { after(async function () {
this.timeout(10000) this.timeout(10000)

View File

@ -6,6 +6,8 @@ import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels'
import { Command } from 'commander' import { Command } from 'commander'
import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
import { createLogger, format, transports } from 'winston' import { createLogger, format, transports } from 'winston'
import { getAccessToken, getMyUserInformation } from '@shared/extra-utils'
import { User, UserRole } from '@shared/models'
let configName = 'PeerTube/CLI' let configName = 'PeerTube/CLI'
if (isTestInstance()) configName += `-${getAppNumber()}` if (isTestInstance()) configName += `-${getAppNumber()}`
@ -14,6 +16,19 @@ const config = require('application-config')(configName)
const version = require('../../../package.json').version const version = require('../../../package.json').version
async function getAdminTokenOrDie (url: string, username: string, password: string) {
const accessToken = await getAccessToken(url, username, password)
const resMe = await getMyUserInformation(url, accessToken)
const me: User = resMe.body
if (me.role !== UserRole.ADMINISTRATOR) {
console.error('You must be an administrator.')
process.exit(-1)
}
return accessToken
}
interface Settings { interface Settings {
remotes: any[], remotes: any[],
default: number default: number
@ -222,5 +237,7 @@ export {
getServerCredentials, getServerCredentials,
buildCommonVideoOptions, buildCommonVideoOptions,
buildVideoAttributesFromCommander buildVideoAttributesFromCommander,
getAdminTokenOrDie
} }

View File

@ -4,11 +4,12 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"application-config": "^1.0.1", "application-config": "^1.0.1",
"cli-table": "^0.3.1", "cli-table3": "^0.5.1",
"netrc-parser": "^3.1.6", "netrc-parser": "^3.1.6",
"webtorrent-hybrid": "^4.0.1" "webtorrent-hybrid": "^4.0.1"
}, },
"summon": { "summon": {
"silent": true "silent": true
} },
"devDependencies": {}
} }

View File

@ -6,8 +6,7 @@ import * as prompt from 'prompt'
import { getNetrc, getSettings, writeSettings } from './cli' import { getNetrc, getSettings, writeSettings } from './cli'
import { isUserUsernameValid } from '../helpers/custom-validators/users' import { isUserUsernameValid } from '../helpers/custom-validators/users'
import { getAccessToken, login } from '../../shared/extra-utils' import { getAccessToken, login } from '../../shared/extra-utils'
import * as CliTable3 from 'cli-table3'
const Table = require('cli-table')
async function delInstance (url: string) { async function delInstance (url: string) {
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
@ -108,10 +107,10 @@ program
.action(async () => { .action(async () => {
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
const table = new Table({ const table = new CliTable3({
head: ['instance', 'login'], head: ['instance', 'login'],
colWidths: [30, 30] colWidths: [30, 30]
}) }) as CliTable3.HorizontalTable
settings.remotes.forEach(element => { settings.remotes.forEach(element => {
if (!netrc.machines[element]) return if (!netrc.machines[element]) return

View File

@ -3,15 +3,11 @@ registerTSPaths()
import * as program from 'commander' import * as program from 'commander'
import { PluginType } from '../../shared/models/plugins/plugin.type' import { PluginType } from '../../shared/models/plugins/plugin.type'
import { getAccessToken } from '../../shared/extra-utils/users/login'
import { getMyUserInformation } from '../../shared/extra-utils/users/users'
import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins' import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
import { getServerCredentials } from './cli' import { getAdminTokenOrDie, getServerCredentials } from './cli'
import { User, UserRole } from '../../shared/models/users'
import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model'
import { isAbsolute } from 'path' import { isAbsolute } from 'path'
import * as CliTable3 from 'cli-table3'
const Table = require('cli-table')
program program
.name('plugins') .name('plugins')
@ -82,10 +78,10 @@ async function pluginsListCLI () {
}) })
const plugins: PeerTubePlugin[] = res.body.data const plugins: PeerTubePlugin[] = res.body.data
const table = new Table({ const table = new CliTable3({
head: ['name', 'version', 'homepage'], head: ['name', 'version', 'homepage'],
colWidths: [ 50, 10, 50 ] colWidths: [ 50, 10, 50 ]
}) }) as CliTable3.HorizontalTable
for (const plugin of plugins) { for (const plugin of plugins) {
const npmName = plugin.type === PluginType.PLUGIN const npmName = plugin.type === PluginType.PLUGIN
@ -192,16 +188,3 @@ async function uninstallPluginCLI (options: any) {
console.log('Plugin uninstalled.') console.log('Plugin uninstalled.')
process.exit(0) process.exit(0)
} }
async function getAdminTokenOrDie (url: string, username: string, password: string) {
const accessToken = await getAccessToken(url, username, password)
const resMe = await getMyUserInformation(url, accessToken)
const me: User = resMe.body
if (me.role !== UserRole.ADMINISTRATOR) {
console.error('Cannot list plugins if you are not administrator.')
process.exit(-1)
}
return accessToken
}

View File

@ -0,0 +1,194 @@
import { registerTSPaths } from '../helpers/register-ts-paths'
registerTSPaths()
import * as program from 'commander'
import { getAdminTokenOrDie, getServerCredentials } from './cli'
import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy } from '@shared/extra-utils/server/redundancy'
import validator from 'validator'
import bytes = require('bytes')
import * as CliTable3 from 'cli-table3'
import { parse } from 'url'
import { uniq } from 'lodash'
program
.name('plugins')
.usage('[command] [options]')
program
.command('list-remote-redundancies')
.description('List remote redundancies on your videos')
.option('-u, --url <url>', 'Server url')
.option('-U, --username <username>', 'Username')
.option('-p, --password <token>', 'Password')
.action(() => listRedundanciesCLI('my-videos'))
program
.command('list-my-redundancies')
.description('List your redundancies of remote videos')
.option('-u, --url <url>', 'Server url')
.option('-U, --username <username>', 'Username')
.option('-p, --password <token>', 'Password')
.action(() => listRedundanciesCLI('remote-videos'))
program
.command('add')
.description('Duplicate a video in your redundancy system')
.option('-u, --url <url>', 'Server url')
.option('-U, --username <username>', 'Username')
.option('-p, --password <token>', 'Password')
.option('-v, --video <videoId>', 'Video id to duplicate')
.action((options) => addRedundancyCLI(options))
program
.command('remove')
.description('Remove a video from your redundancies')
.option('-u, --url <url>', 'Server url')
.option('-U, --username <username>', 'Username')
.option('-p, --password <token>', 'Password')
.option('-v, --video <videoId>', 'Video id to remove from redundancies')
.action((options) => removeRedundancyCLI(options))
if (!process.argv.slice(2).length) {
program.outputHelp()
}
program.parse(process.argv)
// ----------------------------------------------------------------------------
async function listRedundanciesCLI (target: VideoRedundanciesTarget) {
const { url, username, password } = await getServerCredentials(program)
const accessToken = await getAdminTokenOrDie(url, username, password)
const redundancies = await listVideoRedundanciesData(url, accessToken, target)
const table = new CliTable3({
head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ]
}) as CliTable3.HorizontalTable
for (const redundancy of redundancies) {
const webtorrentFiles = redundancy.redundancies.files
const streamingPlaylists = redundancy.redundancies.streamingPlaylists
let totalSize = ''
if (target === 'remote-videos') {
const tmp = webtorrentFiles.concat(streamingPlaylists)
.reduce((a, b) => a + b.size, 0)
totalSize = bytes(tmp)
}
const instances = uniq(
webtorrentFiles.concat(streamingPlaylists)
.map(r => r.fileUrl)
.map(u => parse(u).host)
)
table.push([
redundancy.id.toString(),
redundancy.name,
redundancy.url,
webtorrentFiles.length,
streamingPlaylists.length,
instances.join('\n'),
totalSize
])
}
console.log(table.toString())
process.exit(0)
}
async function addRedundancyCLI (options: { videoId: number }) {
const { url, username, password } = await getServerCredentials(program)
const accessToken = await getAdminTokenOrDie(url, username, password)
if (!options[ 'video' ] || validator.isInt('' + options[ 'video' ]) === false) {
console.error('You need to specify the video id to duplicate and it should be a number.\n')
program.outputHelp()
process.exit(-1)
}
try {
await addVideoRedundancy({
url,
accessToken,
videoId: options[ 'video' ]
})
console.log('Video will be duplicated by your instance!')
process.exit(0)
} catch (err) {
if (err.message.includes(409)) {
console.error('This video is already duplicated by your instance.')
} else if (err.message.includes(404)) {
console.error('This video id does not exist.')
} else {
console.error(err)
}
process.exit(-1)
}
}
async function removeRedundancyCLI (options: { videoId: number }) {
const { url, username, password } = await getServerCredentials(program)
const accessToken = await getAdminTokenOrDie(url, username, password)
if (!options[ 'video' ] || validator.isInt('' + options[ 'video' ]) === false) {
console.error('You need to specify the video id to remove from your redundancies.\n')
program.outputHelp()
process.exit(-1)
}
const videoId = parseInt(options[ 'video' ] + '', 10)
let redundancies = await listVideoRedundanciesData(url, accessToken, 'my-videos')
let videoRedundancy = redundancies.find(r => videoId === r.id)
if (!videoRedundancy) {
redundancies = await listVideoRedundanciesData(url, accessToken, 'remote-videos')
videoRedundancy = redundancies.find(r => videoId === r.id)
}
if (!videoRedundancy) {
console.error('Video redundancy not found.')
process.exit(-1)
}
try {
const ids = videoRedundancy.redundancies.files
.concat(videoRedundancy.redundancies.streamingPlaylists)
.map(r => r.id)
for (const id of ids) {
await removeVideoRedundancy({
url,
accessToken,
redundancyId: id
})
}
console.log('Video redundancy removed!')
process.exit(0)
} catch (err) {
console.error(err)
process.exit(-1)
}
}
async function listVideoRedundanciesData (url: string, accessToken: string, target: VideoRedundanciesTarget) {
const res = await listVideoRedundancies({
url,
accessToken,
start: 0,
count: 100,
sort: 'name',
target
})
return res.body.data as VideoRedundancy[]
}

View File

@ -22,6 +22,7 @@ program
.command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w')
.command('repl', 'initiate a REPL to access internals') .command('repl', 'initiate a REPL to access internals')
.command('plugins [action]', 'manage instance plugins/themes').alias('p') .command('plugins [action]', 'manage instance plugins/themes').alias('p')
.command('redundancy [action]', 'manage instance redundancies').alias('r')
/* Not Yet Implemented */ /* Not Yet Implemented */
program program

View File

@ -347,12 +347,15 @@ chunk-store-stream@^4.0.0:
block-stream2 "^2.0.0" block-stream2 "^2.0.0"
readable-stream "^3.4.0" readable-stream "^3.4.0"
cli-table@^0.3.1: cli-table3@^0.5.1:
version "0.3.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM= integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
dependencies: dependencies:
colors "1.0.3" object-assign "^4.1.0"
string-width "^2.1.1"
optionalDependencies:
colors "^1.1.2"
clivas@^0.2.0: clivas@^0.2.0:
version "0.2.0" version "0.2.0"
@ -364,10 +367,10 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
colors@1.0.3: colors@^1.1.2:
version "1.0.3" version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
common-tags@^1.8.0: common-tags@^1.8.0:
version "1.8.0" version "1.8.0"
@ -1609,7 +1612,7 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0" is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2": "string-width@^1.0.2 || 2", string-width@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==