addVideoChannel,
buildAbsoluteFixturePath,
cleanupTests,
- createUser,
+ createUser, doubleFollow,
execCLI,
flushAndRunServer,
- getEnvCli,
+ getEnvCli, getLocalIdByUUID,
getVideo,
getVideosList,
getVideosListWithToken, removeVideo,
ServerInfo,
- setAccessTokensToServers,
+ setAccessTokensToServers, uploadVideo, uploadVideoAndGetId,
userLogin,
waitJobs
} from '../../../shared/extra-utils'
})
})
+ 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 () {
this.timeout(10000)
import { Command } from 'commander'
import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
import { createLogger, format, transports } from 'winston'
+import { getAccessToken, getMyUserInformation } from '@shared/extra-utils'
+import { User, UserRole } from '@shared/models'
let configName = 'PeerTube/CLI'
if (isTestInstance()) configName += `-${getAppNumber()}`
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 {
remotes: any[],
default: number
getServerCredentials,
buildCommonVideoOptions,
- buildVideoAttributesFromCommander
+ buildVideoAttributesFromCommander,
+
+ getAdminTokenOrDie
}
"private": true,
"dependencies": {
"application-config": "^1.0.1",
- "cli-table": "^0.3.1",
+ "cli-table3": "^0.5.1",
"netrc-parser": "^3.1.6",
"webtorrent-hybrid": "^4.0.1"
},
"summon": {
"silent": true
- }
+ },
+ "devDependencies": {}
}
import { getNetrc, getSettings, writeSettings } from './cli'
import { isUserUsernameValid } from '../helpers/custom-validators/users'
import { getAccessToken, login } from '../../shared/extra-utils'
-
-const Table = require('cli-table')
+import * as CliTable3 from 'cli-table3'
async function delInstance (url: string) {
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
.action(async () => {
const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
- const table = new Table({
+ const table = new CliTable3({
head: ['instance', 'login'],
colWidths: [30, 30]
- })
+ }) as CliTable3.HorizontalTable
settings.remotes.forEach(element => {
if (!netrc.machines[element]) return
import * as program from 'commander'
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 { getServerCredentials } from './cli'
-import { User, UserRole } from '../../shared/models/users'
+import { getAdminTokenOrDie, getServerCredentials } from './cli'
import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model'
import { isAbsolute } from 'path'
-
-const Table = require('cli-table')
+import * as CliTable3 from 'cli-table3'
program
.name('plugins')
})
const plugins: PeerTubePlugin[] = res.body.data
- const table = new Table({
+ const table = new CliTable3({
head: ['name', 'version', 'homepage'],
colWidths: [ 50, 10, 50 ]
- })
+ }) as CliTable3.HorizontalTable
for (const plugin of plugins) {
const npmName = plugin.type === PluginType.PLUGIN
console.log('Plugin uninstalled.')
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
-}
--- /dev/null
+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[]
+}
.command('watch', 'watch a video in the terminal ✩°。⋆').alias('w')
.command('repl', 'initiate a REPL to access internals')
.command('plugins [action]', 'manage instance plugins/themes').alias('p')
+ .command('redundancy [action]', 'manage instance redundancies').alias('r')
/* Not Yet Implemented */
program
block-stream2 "^2.0.0"
readable-stream "^3.4.0"
-cli-table@^0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
- integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM=
+cli-table3@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
+ integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
dependencies:
- colors "1.0.3"
+ object-assign "^4.1.0"
+ string-width "^2.1.1"
+ optionalDependencies:
+ colors "^1.1.2"
clivas@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
-colors@1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
- integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
+colors@^1.1.2:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+ integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
common-tags@^1.8.0:
version "1.8.0"
is-fullwidth-code-point "^1.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"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==