WIP plugins: update plugin
This commit is contained in:
parent
8d2be0ed7b
commit
b5f919ac8e
24 changed files with 389 additions and 90 deletions
|
@ -26,8 +26,11 @@
|
||||||
<span i18n class="button-label">Homepage</span>
|
<span i18n class="button-label">Homepage</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<my-edit-button *ngIf="pluginType !== PluginType.THEME" [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button>
|
||||||
|
|
||||||
<my-edit-button [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button>
|
<my-button class="update-button" *ngIf="!isUpdateAvailable(plugin)" (click)="update(plugin)" [loading]="isUpdating(plugin)"
|
||||||
|
[label]="getUpdateLabel(plugin)" icon="refresh" [attr.disabled]="isUpdating(plugin)"
|
||||||
|
></my-button>
|
||||||
|
|
||||||
<my-delete-button (click)="uninstall(plugin)" label="Uninstall" i18n-label></my-delete-button>
|
<my-delete-button (click)="uninstall(plugin)" label="Uninstall" i18n-label></my-delete-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -35,3 +35,7 @@
|
||||||
@include peertube-button-link;
|
@include peertube-button-link;
|
||||||
@include button-with-icon(21px, 0, -2px);
|
@include button-with-icon(21px, 0, -2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update-button[disabled="true"] /deep/ .action-button {
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pa
|
||||||
import { ConfirmService, Notifier } from '@app/core'
|
import { ConfirmService, Notifier } from '@app/core'
|
||||||
import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
|
import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { compareSemVer } from '@app/shared/misc/utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-plugin-list-installed',
|
selector: 'my-plugin-list-installed',
|
||||||
|
@ -26,6 +27,9 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
|
|
||||||
plugins: PeerTubePlugin[] = []
|
plugins: PeerTubePlugin[] = []
|
||||||
|
updating: { [name: string]: boolean } = {}
|
||||||
|
|
||||||
|
PluginType = PluginType
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private i18n: I18n,
|
private i18n: I18n,
|
||||||
|
@ -49,7 +53,7 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
this.pagination.currentPage = 1
|
this.pagination.currentPage = 1
|
||||||
this.plugins = []
|
this.plugins = []
|
||||||
|
|
||||||
this.router.navigate([], { queryParams: { pluginType: this.pluginType }})
|
this.router.navigate([], { queryParams: { pluginType: this.pluginType } })
|
||||||
|
|
||||||
this.loadMorePlugins()
|
this.loadMorePlugins()
|
||||||
}
|
}
|
||||||
|
@ -82,6 +86,18 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
return this.i18n('You don\'t have themes installed yet.')
|
return this.i18n('You don\'t have themes installed yet.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUpdateAvailable (plugin: PeerTubePlugin) {
|
||||||
|
return plugin.latestVersion && compareSemVer(plugin.latestVersion, plugin.version) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdateLabel (plugin: PeerTubePlugin) {
|
||||||
|
return this.i18n('Update to {{version}}', { version: plugin.latestVersion })
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdating (plugin: PeerTubePlugin) {
|
||||||
|
return !!this.updating[this.getUpdatingKey(plugin)]
|
||||||
|
}
|
||||||
|
|
||||||
async uninstall (plugin: PeerTubePlugin) {
|
async uninstall (plugin: PeerTubePlugin) {
|
||||||
const res = await this.confirmService.confirm(
|
const res = await this.confirmService.confirm(
|
||||||
this.i18n('Do you really want to uninstall {{pluginName}}?', { pluginName: plugin.name }),
|
this.i18n('Do you really want to uninstall {{pluginName}}?', { pluginName: plugin.name }),
|
||||||
|
@ -102,7 +118,32 @@ export class PluginListInstalledComponent implements OnInit {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update (plugin: PeerTubePlugin) {
|
||||||
|
const updatingKey = this.getUpdatingKey(plugin)
|
||||||
|
if (this.updating[updatingKey]) return
|
||||||
|
|
||||||
|
this.updating[updatingKey] = true
|
||||||
|
|
||||||
|
this.pluginService.update(plugin.name, plugin.type)
|
||||||
|
.pipe()
|
||||||
|
.subscribe(
|
||||||
|
res => {
|
||||||
|
this.updating[updatingKey] = false
|
||||||
|
|
||||||
|
this.notifier.success(this.i18n('{{pluginName}} updated.', { pluginName: plugin.name }))
|
||||||
|
|
||||||
|
Object.assign(plugin, res)
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
getShowRouterLink (plugin: PeerTubePlugin) {
|
getShowRouterLink (plugin: PeerTubePlugin) {
|
||||||
return [ '/admin', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, plugin.type) ]
|
return [ '/admin', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, plugin.type) ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUpdatingKey (plugin: PeerTubePlugin) {
|
||||||
|
return plugin.name + plugin.type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { ComponentPagination } from '@app/shared/rest/component-pagination.model
|
||||||
import { ResultList } from '@shared/models'
|
import { ResultList } from '@shared/models'
|
||||||
import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
|
import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
|
||||||
import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model'
|
import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model'
|
||||||
import { InstallPlugin } from '@shared/models/plugins/install-plugin.model'
|
import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model'
|
||||||
import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model'
|
import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -89,8 +89,17 @@ export class PluginApiService {
|
||||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update (pluginName: string, pluginType: PluginType) {
|
||||||
|
const body: ManagePlugin = {
|
||||||
|
npmName: this.nameToNpmName(pluginName, pluginType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.authHttp.post(PluginApiService.BASE_APPLICATION_URL + '/update', body)
|
||||||
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
|
}
|
||||||
|
|
||||||
install (npmName: string) {
|
install (npmName: string) {
|
||||||
const body: InstallPlugin = {
|
const body: InstallOrUpdatePlugin = {
|
||||||
npmName
|
npmName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,9 @@ export class PluginService {
|
||||||
.toPromise()
|
.toPromise()
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlugin (plugin: ServerConfigPlugin) {
|
addPlugin (plugin: ServerConfigPlugin, isTheme = false) {
|
||||||
|
const pathPrefix = isTheme ? '/themes' : '/plugins'
|
||||||
|
|
||||||
for (const key of Object.keys(plugin.clientScripts)) {
|
for (const key of Object.keys(plugin.clientScripts)) {
|
||||||
const clientScript = plugin.clientScripts[key]
|
const clientScript = plugin.clientScripts[key]
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@ export class PluginService {
|
||||||
this.scopes[scope].push({
|
this.scopes[scope].push({
|
||||||
plugin,
|
plugin,
|
||||||
clientScript: {
|
clientScript: {
|
||||||
script: environment.apiUrl + `/plugins/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
|
script: environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/client-scripts/${clientScript.script}`,
|
||||||
scopes: clientScript.scopes
|
scopes: clientScript.scopes
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<span class="action-button" [ngClass]="className" [title]="getTitle()">
|
<span class="action-button" [ngClass]="className" [title]="getTitle()">
|
||||||
<my-global-icon [iconName]="icon"></my-global-icon>
|
<my-global-icon *ngIf="!loading" [iconName]="icon"></my-global-icon>
|
||||||
|
<my-small-loader [loading]="loading"></my-small-loader>
|
||||||
|
|
||||||
<span class="button-label">{{ label }}</span>
|
<span class="button-label">{{ label }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
@import '_variables';
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
|
my-small-loader /deep/ .root {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 3px 0 0;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
@include peertube-button-link;
|
@include peertube-button-link;
|
||||||
@include button-with-icon(21px, 0, -2px);
|
@include button-with-icon(21px, 0, -2px);
|
||||||
|
|
|
@ -12,6 +12,7 @@ export class ButtonComponent {
|
||||||
@Input() className = 'grey-button'
|
@Input() className = 'grey-button'
|
||||||
@Input() icon: GlobalIconName = undefined
|
@Input() icon: GlobalIconName = undefined
|
||||||
@Input() title: string = undefined
|
@Input() title: string = undefined
|
||||||
|
@Input() loading = false
|
||||||
|
|
||||||
getTitle () {
|
getTitle () {
|
||||||
return this.title || this.label
|
return this.title || this.label
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<div *ngIf="loading">
|
<div class="root" *ngIf="loading">
|
||||||
<div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
|
<div class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -134,6 +134,23 @@ function scrollToTop () {
|
||||||
window.scroll(0, 0)
|
window.scroll(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thanks https://stackoverflow.com/a/16187766
|
||||||
|
function compareSemVer (a: string, b: string) {
|
||||||
|
const regExStrip0 = /(\.0+)+$/
|
||||||
|
const segmentsA = a.replace(regExStrip0, '').split('.')
|
||||||
|
const segmentsB = b.replace(regExStrip0, '').split('.')
|
||||||
|
|
||||||
|
const l = Math.min(segmentsA.length, segmentsB.length)
|
||||||
|
|
||||||
|
for (let i = 0; i < l; i++) {
|
||||||
|
const diff = parseInt(segmentsA[ i ], 10) - parseInt(segmentsB[ i ], 10)
|
||||||
|
|
||||||
|
if (diff) return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
return segmentsA.length - segmentsB.length
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
sortBy,
|
sortBy,
|
||||||
durationToString,
|
durationToString,
|
||||||
|
@ -144,6 +161,7 @@ export {
|
||||||
getAbsoluteAPIUrl,
|
getAbsoluteAPIUrl,
|
||||||
dateToHuman,
|
dateToHuman,
|
||||||
immutableAssign,
|
immutableAssign,
|
||||||
|
compareSemVer,
|
||||||
objectToFormData,
|
objectToFormData,
|
||||||
objectLineFeedToHtml,
|
objectLineFeedToHtml,
|
||||||
removeElementFromArray,
|
removeElementFromArray,
|
||||||
|
|
|
@ -36,6 +36,8 @@
|
||||||
"danger:clean:prod": "scripty",
|
"danger:clean:prod": "scripty",
|
||||||
"danger:clean:modules": "scripty",
|
"danger:clean:modules": "scripty",
|
||||||
"i18n:generate": "scripty",
|
"i18n:generate": "scripty",
|
||||||
|
"plugin:install": "node ./dist/scripts/plugin/install.js",
|
||||||
|
"plugin:uninstall": "node ./dist/scripts/plugin/uninstall.js",
|
||||||
"i18n:xliff2json": "node ./dist/scripts/i18n/xliff2json.js",
|
"i18n:xliff2json": "node ./dist/scripts/i18n/xliff2json.js",
|
||||||
"i18n:create-custom-files": "node ./dist/scripts/i18n/create-custom-files.js",
|
"i18n:create-custom-files": "node ./dist/scripts/i18n/create-custom-files.js",
|
||||||
"reset-password": "node ./dist/scripts/reset-password.js",
|
"reset-password": "node ./dist/scripts/reset-password.js",
|
||||||
|
|
39
scripts/plugin/install.ts
Executable file
39
scripts/plugin/install.ts
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
import { initDatabaseModels } from '../../server/initializers/database'
|
||||||
|
import * as program from 'commander'
|
||||||
|
import { PluginManager } from '../../server/lib/plugins/plugin-manager'
|
||||||
|
import { isAbsolute } from 'path'
|
||||||
|
|
||||||
|
program
|
||||||
|
.option('-n, --plugin-name [pluginName]', 'Plugin name to install')
|
||||||
|
.option('-v, --plugin-version [pluginVersion]', 'Plugin version to install')
|
||||||
|
.option('-p, --plugin-path [pluginPath]', 'Path of the plugin you want to install')
|
||||||
|
.parse(process.argv)
|
||||||
|
|
||||||
|
if (!program['pluginName'] && !program['pluginPath']) {
|
||||||
|
console.error('You need to specify a plugin name with the desired version, or a plugin path.')
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (program['pluginName'] && !program['pluginVersion']) {
|
||||||
|
console.error('You need to specify a the version of the plugin you want to install.')
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (program['pluginPath'] && !isAbsolute(program['pluginPath'])) {
|
||||||
|
console.error('Plugin path should be absolute.')
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function run () {
|
||||||
|
await initDatabaseModels(true)
|
||||||
|
|
||||||
|
const toInstall = program['pluginName'] || program['pluginPath']
|
||||||
|
await PluginManager.Instance.install(toInstall, program['pluginVersion'], !!program['pluginPath'])
|
||||||
|
}
|
26
scripts/plugin/uninstall.ts
Executable file
26
scripts/plugin/uninstall.ts
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
import { initDatabaseModels } from '../../server/initializers/database'
|
||||||
|
import * as program from 'commander'
|
||||||
|
import { PluginManager } from '../../server/lib/plugins/plugin-manager'
|
||||||
|
|
||||||
|
program
|
||||||
|
.option('-n, --npm-name [npmName]', 'Package name to install')
|
||||||
|
.parse(process.argv)
|
||||||
|
|
||||||
|
if (!program['npmName']) {
|
||||||
|
console.error('You need to specify the plugin name.')
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function run () {
|
||||||
|
await initDatabaseModels(true)
|
||||||
|
|
||||||
|
const toUninstall = program['npmName']
|
||||||
|
await PluginManager.Instance.uninstall(toUninstall)
|
||||||
|
}
|
|
@ -97,7 +97,6 @@ import {
|
||||||
staticRouter,
|
staticRouter,
|
||||||
servicesRouter,
|
servicesRouter,
|
||||||
pluginsRouter,
|
pluginsRouter,
|
||||||
themesRouter,
|
|
||||||
webfingerRouter,
|
webfingerRouter,
|
||||||
trackerRouter,
|
trackerRouter,
|
||||||
createWebsocketTrackerServer, botsRouter
|
createWebsocketTrackerServer, botsRouter
|
||||||
|
@ -178,8 +177,7 @@ app.use(apiRoute, apiRouter)
|
||||||
app.use('/services', servicesRouter)
|
app.use('/services', servicesRouter)
|
||||||
|
|
||||||
// Plugins & themes
|
// Plugins & themes
|
||||||
app.use('/plugins', pluginsRouter)
|
app.use('/', pluginsRouter)
|
||||||
app.use('/themes', themesRouter)
|
|
||||||
|
|
||||||
app.use('/', activityPubRouter)
|
app.use('/', activityPubRouter)
|
||||||
app.use('/', feedsRouter)
|
app.use('/', feedsRouter)
|
||||||
|
|
|
@ -13,13 +13,13 @@ import { PluginModel } from '../../models/server/plugin'
|
||||||
import { UserRight } from '../../../shared/models/users'
|
import { UserRight } from '../../../shared/models/users'
|
||||||
import {
|
import {
|
||||||
existingPluginValidator,
|
existingPluginValidator,
|
||||||
installPluginValidator,
|
installOrUpdatePluginValidator,
|
||||||
listPluginsValidator,
|
listPluginsValidator,
|
||||||
uninstallPluginValidator,
|
uninstallPluginValidator,
|
||||||
updatePluginSettingsValidator
|
updatePluginSettingsValidator
|
||||||
} from '../../middlewares/validators/plugins'
|
} from '../../middlewares/validators/plugins'
|
||||||
import { PluginManager } from '../../lib/plugins/plugin-manager'
|
import { PluginManager } from '../../lib/plugins/plugin-manager'
|
||||||
import { InstallPlugin } from '../../../shared/models/plugins/install-plugin.model'
|
import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
|
||||||
import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model'
|
import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
|
|
||||||
|
@ -61,10 +61,17 @@ pluginRouter.put('/:npmName/settings',
|
||||||
pluginRouter.post('/install',
|
pluginRouter.post('/install',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
||||||
installPluginValidator,
|
installOrUpdatePluginValidator,
|
||||||
asyncMiddleware(installPlugin)
|
asyncMiddleware(installPlugin)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pluginRouter.post('/update',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
||||||
|
installOrUpdatePluginValidator,
|
||||||
|
asyncMiddleware(updatePlugin)
|
||||||
|
)
|
||||||
|
|
||||||
pluginRouter.post('/uninstall',
|
pluginRouter.post('/uninstall',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
ensureUserHasRight(UserRight.MANAGE_PLUGINS),
|
||||||
|
@ -100,18 +107,33 @@ function getPlugin (req: express.Request, res: express.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installPlugin (req: express.Request, res: express.Response) {
|
async function installPlugin (req: express.Request, res: express.Response) {
|
||||||
const body: InstallPlugin = req.body
|
const body: InstallOrUpdatePlugin = req.body
|
||||||
|
|
||||||
const fromDisk = !!body.path
|
const fromDisk = !!body.path
|
||||||
const toInstall = body.npmName || body.path
|
const toInstall = body.npmName || body.path
|
||||||
try {
|
try {
|
||||||
await PluginManager.Instance.install(toInstall, undefined, fromDisk)
|
const plugin = await PluginManager.Instance.install(toInstall, undefined, fromDisk)
|
||||||
|
|
||||||
|
return res.json(plugin.toFormattedJSON())
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('Cannot install plugin %s.', toInstall, { err })
|
logger.warn('Cannot install plugin %s.', toInstall, { err })
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.sendStatus(204)
|
async function updatePlugin (req: express.Request, res: express.Response) {
|
||||||
|
const body: InstallOrUpdatePlugin = req.body
|
||||||
|
|
||||||
|
const fromDisk = !!body.path
|
||||||
|
const toUpdate = body.npmName || body.path
|
||||||
|
try {
|
||||||
|
const plugin = await PluginManager.Instance.update(toUpdate, undefined, fromDisk)
|
||||||
|
|
||||||
|
return res.json(plugin.toFormattedJSON())
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Cannot update plugin %s.', toUpdate, { err })
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uninstallPlugin (req: express.Request, res: express.Response) {
|
async function uninstallPlugin (req: express.Request, res: express.Response) {
|
||||||
|
@ -123,9 +145,7 @@ async function uninstallPlugin (req: express.Request, res: express.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginRegisteredSettings (req: express.Request, res: express.Response) {
|
function getPluginRegisteredSettings (req: express.Request, res: express.Response) {
|
||||||
const plugin = res.locals.plugin
|
const settings = PluginManager.Instance.getRegisteredSettings(req.params.npmName)
|
||||||
|
|
||||||
const settings = PluginManager.Instance.getSettings(plugin.name)
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
settings
|
settings
|
||||||
|
|
|
@ -8,4 +8,3 @@ export * from './webfinger'
|
||||||
export * from './tracker'
|
export * from './tracker'
|
||||||
export * from './bots'
|
export * from './bots'
|
||||||
export * from './plugins'
|
export * from './plugins'
|
||||||
export * from './themes'
|
|
||||||
|
|
|
@ -1,25 +1,42 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
|
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
|
||||||
import { basename, join } from 'path'
|
import { join } from 'path'
|
||||||
import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
|
import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
|
||||||
import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
|
import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
|
||||||
|
import { serveThemeCSSValidator } from '../middlewares/validators/themes'
|
||||||
|
import { PluginType } from '../../shared/models/plugins/plugin.type'
|
||||||
|
|
||||||
const pluginsRouter = express.Router()
|
const pluginsRouter = express.Router()
|
||||||
|
|
||||||
pluginsRouter.get('/global.css',
|
pluginsRouter.get('/plugins/global.css',
|
||||||
servePluginGlobalCSS
|
servePluginGlobalCSS
|
||||||
)
|
)
|
||||||
|
|
||||||
pluginsRouter.get('/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
|
pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
|
||||||
servePluginStaticDirectoryValidator,
|
servePluginStaticDirectoryValidator(PluginType.PLUGIN),
|
||||||
servePluginStaticDirectory
|
servePluginStaticDirectory
|
||||||
)
|
)
|
||||||
|
|
||||||
pluginsRouter.get('/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)',
|
pluginsRouter.get('/plugins/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)',
|
||||||
servePluginStaticDirectoryValidator,
|
servePluginStaticDirectoryValidator(PluginType.PLUGIN),
|
||||||
servePluginClientScripts
|
servePluginClientScripts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pluginsRouter.get('/themes/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
|
||||||
|
servePluginStaticDirectoryValidator(PluginType.THEME),
|
||||||
|
servePluginStaticDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
pluginsRouter.get('/themes/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)',
|
||||||
|
servePluginStaticDirectoryValidator(PluginType.THEME),
|
||||||
|
servePluginClientScripts
|
||||||
|
)
|
||||||
|
|
||||||
|
pluginsRouter.get('/themes/:themeName/:themeVersion/css/:staticEndpoint(*)',
|
||||||
|
serveThemeCSSValidator,
|
||||||
|
serveThemeCSSDirectory
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -58,3 +75,14 @@ function servePluginClientScripts (req: express.Request, res: express.Response)
|
||||||
|
|
||||||
return res.sendFile(join(plugin.path, staticEndpoint))
|
return res.sendFile(join(plugin.path, staticEndpoint))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
|
||||||
|
const plugin: RegisteredPlugin = res.locals.registeredPlugin
|
||||||
|
const staticEndpoint = req.params.staticEndpoint
|
||||||
|
|
||||||
|
if (plugin.css.includes(staticEndpoint) === false) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.sendFile(join(plugin.path, staticEndpoint))
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { RegisterHookOptions } from '../../../shared/models/plugins/register-hoo
|
||||||
import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
|
import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
|
||||||
|
|
||||||
export interface RegisteredPlugin {
|
export interface RegisteredPlugin {
|
||||||
|
npmName: string
|
||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
description: string
|
description: string
|
||||||
|
@ -34,6 +35,7 @@ export interface RegisteredPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HookInformationValue {
|
export interface HookInformationValue {
|
||||||
|
npmName: string
|
||||||
pluginName: string
|
pluginName: string
|
||||||
handler: Function
|
handler: Function
|
||||||
priority: number
|
priority: number
|
||||||
|
@ -52,12 +54,13 @@ export class PluginManager {
|
||||||
|
|
||||||
// ###################### Getters ######################
|
// ###################### Getters ######################
|
||||||
|
|
||||||
getRegisteredPluginOrTheme (name: string) {
|
getRegisteredPluginOrTheme (npmName: string) {
|
||||||
return this.registeredPlugins[name]
|
return this.registeredPlugins[npmName]
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegisteredPlugin (name: string) {
|
getRegisteredPlugin (name: string) {
|
||||||
const registered = this.getRegisteredPluginOrTheme(name)
|
const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN)
|
||||||
|
const registered = this.getRegisteredPluginOrTheme(npmName)
|
||||||
|
|
||||||
if (!registered || registered.type !== PluginType.PLUGIN) return undefined
|
if (!registered || registered.type !== PluginType.PLUGIN) return undefined
|
||||||
|
|
||||||
|
@ -65,7 +68,8 @@ export class PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegisteredTheme (name: string) {
|
getRegisteredTheme (name: string) {
|
||||||
const registered = this.getRegisteredPluginOrTheme(name)
|
const npmName = PluginModel.buildNpmName(name, PluginType.THEME)
|
||||||
|
const registered = this.getRegisteredPluginOrTheme(npmName)
|
||||||
|
|
||||||
if (!registered || registered.type !== PluginType.THEME) return undefined
|
if (!registered || registered.type !== PluginType.THEME) return undefined
|
||||||
|
|
||||||
|
@ -80,8 +84,8 @@ export class PluginManager {
|
||||||
return this.getRegisteredPluginsOrThemes(PluginType.THEME)
|
return this.getRegisteredPluginsOrThemes(PluginType.THEME)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSettings (name: string) {
|
getRegisteredSettings (npmName: string) {
|
||||||
return this.settings[name] || []
|
return this.settings[npmName] || []
|
||||||
}
|
}
|
||||||
|
|
||||||
// ###################### Hooks ######################
|
// ###################### Hooks ######################
|
||||||
|
@ -126,35 +130,36 @@ export class PluginManager {
|
||||||
this.sortHooksByPriority()
|
this.sortHooksByPriority()
|
||||||
}
|
}
|
||||||
|
|
||||||
async unregister (name: string) {
|
// Don't need the plugin type since themes cannot register server code
|
||||||
const plugin = this.getRegisteredPlugin(name)
|
async unregister (npmName: string) {
|
||||||
|
logger.info('Unregister plugin %s.', npmName)
|
||||||
|
|
||||||
|
const plugin = this.getRegisteredPluginOrTheme(npmName)
|
||||||
|
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
throw new Error(`Unknown plugin ${name} to unregister`)
|
throw new Error(`Unknown plugin ${npmName} to unregister`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.type === PluginType.THEME) {
|
if (plugin.type === PluginType.PLUGIN) {
|
||||||
throw new Error(`Cannot unregister ${name}: this is a theme`)
|
await plugin.unregister()
|
||||||
|
|
||||||
|
// Remove hooks of this plugin
|
||||||
|
for (const key of Object.keys(this.hooks)) {
|
||||||
|
this.hooks[key] = this.hooks[key].filter(h => h.pluginName !== npmName)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Regenerating registered plugin CSS to global file.')
|
||||||
|
await this.regeneratePluginGlobalCSS()
|
||||||
}
|
}
|
||||||
|
|
||||||
await plugin.unregister()
|
delete this.registeredPlugins[plugin.npmName]
|
||||||
|
|
||||||
// Remove hooks of this plugin
|
|
||||||
for (const key of Object.keys(this.hooks)) {
|
|
||||||
this.hooks[key] = this.hooks[key].filter(h => h.pluginName !== name)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.registeredPlugins[plugin.name]
|
|
||||||
|
|
||||||
logger.info('Regenerating registered plugin CSS to global file.')
|
|
||||||
await this.regeneratePluginGlobalCSS()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ###################### Installation ######################
|
// ###################### Installation ######################
|
||||||
|
|
||||||
async install (toInstall: string, version?: string, fromDisk = false) {
|
async install (toInstall: string, version?: string, fromDisk = false) {
|
||||||
let plugin: PluginModel
|
let plugin: PluginModel
|
||||||
let name: string
|
let npmName: string
|
||||||
|
|
||||||
logger.info('Installing plugin %s.', toInstall)
|
logger.info('Installing plugin %s.', toInstall)
|
||||||
|
|
||||||
|
@ -163,9 +168,9 @@ export class PluginManager {
|
||||||
? await installNpmPluginFromDisk(toInstall)
|
? await installNpmPluginFromDisk(toInstall)
|
||||||
: await installNpmPlugin(toInstall, version)
|
: await installNpmPlugin(toInstall, version)
|
||||||
|
|
||||||
name = fromDisk ? basename(toInstall) : toInstall
|
npmName = fromDisk ? basename(toInstall) : toInstall
|
||||||
const pluginType = PluginModel.getTypeFromNpmName(name)
|
const pluginType = PluginModel.getTypeFromNpmName(npmName)
|
||||||
const pluginName = PluginModel.normalizePluginName(name)
|
const pluginName = PluginModel.normalizePluginName(npmName)
|
||||||
|
|
||||||
const packageJSON = this.getPackageJSON(pluginName, pluginType)
|
const packageJSON = this.getPackageJSON(pluginName, pluginType)
|
||||||
if (!isPackageJSONValid(packageJSON, pluginType)) {
|
if (!isPackageJSONValid(packageJSON, pluginType)) {
|
||||||
|
@ -186,7 +191,7 @@ export class PluginManager {
|
||||||
logger.error('Cannot install plugin %s, removing it...', toInstall, { err })
|
logger.error('Cannot install plugin %s, removing it...', toInstall, { err })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await removeNpmPlugin(name)
|
await removeNpmPlugin(npmName)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err })
|
logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err })
|
||||||
}
|
}
|
||||||
|
@ -197,17 +202,28 @@ export class PluginManager {
|
||||||
logger.info('Successful installation of plugin %s.', toInstall)
|
logger.info('Successful installation of plugin %s.', toInstall)
|
||||||
|
|
||||||
await this.registerPluginOrTheme(plugin)
|
await this.registerPluginOrTheme(plugin)
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
async update (toUpdate: string, version?: string, fromDisk = false) {
|
||||||
|
const npmName = fromDisk ? basename(toUpdate) : toUpdate
|
||||||
|
|
||||||
|
logger.info('Updating plugin %s.', npmName)
|
||||||
|
|
||||||
|
// Unregister old hooks
|
||||||
|
await this.unregister(npmName)
|
||||||
|
|
||||||
|
return this.install(toUpdate, version, fromDisk)
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstall (npmName: string) {
|
async uninstall (npmName: string) {
|
||||||
logger.info('Uninstalling plugin %s.', npmName)
|
logger.info('Uninstalling plugin %s.', npmName)
|
||||||
|
|
||||||
const pluginName = PluginModel.normalizePluginName(npmName)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.unregister(pluginName)
|
await this.unregister(npmName)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('Cannot unregister plugin %s.', pluginName, { err })
|
logger.warn('Cannot unregister plugin %s.', npmName, { err })
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = await PluginModel.loadByNpmName(npmName)
|
const plugin = await PluginModel.loadByNpmName(npmName)
|
||||||
|
@ -229,7 +245,9 @@ export class PluginManager {
|
||||||
// ###################### Private register ######################
|
// ###################### Private register ######################
|
||||||
|
|
||||||
private async registerPluginOrTheme (plugin: PluginModel) {
|
private async registerPluginOrTheme (plugin: PluginModel) {
|
||||||
logger.info('Registering plugin or theme %s.', plugin.name)
|
const npmName = PluginModel.buildNpmName(plugin.name, plugin.type)
|
||||||
|
|
||||||
|
logger.info('Registering plugin or theme %s.', npmName)
|
||||||
|
|
||||||
const packageJSON = this.getPackageJSON(plugin.name, plugin.type)
|
const packageJSON = this.getPackageJSON(plugin.name, plugin.type)
|
||||||
const pluginPath = this.getPluginPath(plugin.name, plugin.type)
|
const pluginPath = this.getPluginPath(plugin.name, plugin.type)
|
||||||
|
@ -248,7 +266,8 @@ export class PluginManager {
|
||||||
clientScripts[c.script] = c
|
clientScripts[c.script] = c
|
||||||
}
|
}
|
||||||
|
|
||||||
this.registeredPlugins[ plugin.name ] = {
|
this.registeredPlugins[ npmName ] = {
|
||||||
|
npmName,
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
type: plugin.type,
|
type: plugin.type,
|
||||||
version: plugin.version,
|
version: plugin.version,
|
||||||
|
@ -263,10 +282,13 @@ export class PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) {
|
private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) {
|
||||||
|
const npmName = PluginModel.buildNpmName(plugin.name, plugin.type)
|
||||||
|
|
||||||
const registerHook = (options: RegisterHookOptions) => {
|
const registerHook = (options: RegisterHookOptions) => {
|
||||||
if (!this.hooks[options.target]) this.hooks[options.target] = []
|
if (!this.hooks[options.target]) this.hooks[options.target] = []
|
||||||
|
|
||||||
this.hooks[options.target].push({
|
this.hooks[options.target].push({
|
||||||
|
npmName,
|
||||||
pluginName: plugin.name,
|
pluginName: plugin.name,
|
||||||
handler: options.handler,
|
handler: options.handler,
|
||||||
priority: options.priority || 0
|
priority: options.priority || 0
|
||||||
|
@ -274,15 +296,15 @@ export class PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerSetting = (options: RegisterSettingOptions) => {
|
const registerSetting = (options: RegisterSettingOptions) => {
|
||||||
if (!this.settings[plugin.name]) this.settings[plugin.name] = []
|
if (!this.settings[npmName]) this.settings[npmName] = []
|
||||||
|
|
||||||
this.settings[plugin.name].push(options)
|
this.settings[npmName].push(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsManager: PluginSettingsManager = {
|
const settingsManager: PluginSettingsManager = {
|
||||||
getSetting: (name: string) => PluginModel.getSetting(plugin.name, name),
|
getSetting: (name: string) => PluginModel.getSetting(plugin.name, plugin.type, name),
|
||||||
|
|
||||||
setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, name, value)
|
setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, plugin.type, name, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const library: PluginLibrary = require(join(pluginPath, packageJSON.library))
|
const library: PluginLibrary = require(join(pluginPath, packageJSON.library))
|
||||||
|
@ -293,7 +315,7 @@ export class PluginManager {
|
||||||
|
|
||||||
library.register({ registerHook, registerSetting, settingsManager })
|
library.register({ registerHook, registerSetting, settingsManager })
|
||||||
|
|
||||||
logger.info('Add plugin %s CSS to global file.', plugin.name)
|
logger.info('Add plugin %s CSS to global file.', npmName)
|
||||||
|
|
||||||
await this.addCSSToGlobalFile(pluginPath, packageJSON.css)
|
await this.addCSSToGlobalFile(pluginPath, packageJSON.css)
|
||||||
|
|
||||||
|
@ -351,9 +373,9 @@ export class PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPluginPath (pluginName: string, pluginType: PluginType) {
|
private getPluginPath (pluginName: string, pluginType: PluginType) {
|
||||||
const prefix = pluginType === PluginType.PLUGIN ? 'peertube-plugin-' : 'peertube-theme-'
|
const npmName = PluginModel.buildNpmName(pluginName, pluginType)
|
||||||
|
|
||||||
return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', prefix + pluginName)
|
return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ###################### Private getters ######################
|
// ###################### Private getters ######################
|
||||||
|
@ -361,8 +383,8 @@ export class PluginManager {
|
||||||
private getRegisteredPluginsOrThemes (type: PluginType) {
|
private getRegisteredPluginsOrThemes (type: PluginType) {
|
||||||
const plugins: RegisteredPlugin[] = []
|
const plugins: RegisteredPlugin[] = []
|
||||||
|
|
||||||
for (const pluginName of Object.keys(this.registeredPlugins)) {
|
for (const npmName of Object.keys(this.registeredPlugins)) {
|
||||||
const plugin = this.registeredPlugins[ pluginName ]
|
const plugin = this.registeredPlugins[ npmName ]
|
||||||
if (plugin.type !== type) continue
|
if (plugin.type !== type) continue
|
||||||
|
|
||||||
plugins.push(plugin)
|
plugins.push(plugin)
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { param, query, body } from 'express-validator/check'
|
import { body, param, query } from 'express-validator/check'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from './utils'
|
||||||
import { isPluginNameValid, isPluginTypeValid, isPluginVersionValid, isNpmPluginNameValid } from '../../helpers/custom-validators/plugins'
|
import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
|
||||||
import { PluginManager } from '../../lib/plugins/plugin-manager'
|
import { PluginManager } from '../../lib/plugins/plugin-manager'
|
||||||
import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc'
|
import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc'
|
||||||
import { PluginModel } from '../../models/server/plugin'
|
import { PluginModel } from '../../models/server/plugin'
|
||||||
import { InstallPlugin } from '../../../shared/models/plugins/install-plugin.model'
|
import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
|
||||||
|
import { PluginType } from '../../../shared/models/plugins/plugin.type'
|
||||||
|
|
||||||
const servePluginStaticDirectoryValidator = [
|
const servePluginStaticDirectoryValidator = (pluginType: PluginType) => [
|
||||||
param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'),
|
param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'),
|
||||||
param('pluginVersion').custom(isPluginVersionValid).withMessage('Should have a valid plugin version'),
|
param('pluginVersion').custom(isPluginVersionValid).withMessage('Should have a valid plugin version'),
|
||||||
param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'),
|
param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'),
|
||||||
|
@ -18,7 +19,8 @@ const servePluginStaticDirectoryValidator = [
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(req.params.pluginName)
|
const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType)
|
||||||
|
const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
|
||||||
|
|
||||||
if (!plugin || plugin.version !== req.params.pluginVersion) {
|
if (!plugin || plugin.version !== req.params.pluginVersion) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
|
@ -48,7 +50,7 @@ const listPluginsValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const installPluginValidator = [
|
const installOrUpdatePluginValidator = [
|
||||||
body('npmName')
|
body('npmName')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
|
.custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
|
||||||
|
@ -57,11 +59,11 @@ const installPluginValidator = [
|
||||||
.custom(isSafePath).withMessage('Should have a valid safe path'),
|
.custom(isSafePath).withMessage('Should have a valid safe path'),
|
||||||
|
|
||||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking installPluginValidator parameters', { parameters: req.body })
|
logger.debug('Checking installOrUpdatePluginValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
const body: InstallPlugin = req.body
|
const body: InstallOrUpdatePlugin = req.body
|
||||||
if (!body.path && !body.npmName) {
|
if (!body.path && !body.npmName) {
|
||||||
return res.status(400)
|
return res.status(400)
|
||||||
.json({ error: 'Should have either a npmName or a path' })
|
.json({ error: 'Should have either a npmName or a path' })
|
||||||
|
@ -124,6 +126,6 @@ export {
|
||||||
updatePluginSettingsValidator,
|
updatePluginSettingsValidator,
|
||||||
uninstallPluginValidator,
|
uninstallPluginValidator,
|
||||||
existingPluginValidator,
|
existingPluginValidator,
|
||||||
installPluginValidator,
|
installOrUpdatePluginValidator,
|
||||||
listPluginsValidator
|
listPluginsValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
import { getSort, throwIfNotValid } from '../utils'
|
||||||
import {
|
import {
|
||||||
isPluginDescriptionValid, isPluginHomepage,
|
isPluginDescriptionValid,
|
||||||
|
isPluginHomepage,
|
||||||
isPluginNameValid,
|
isPluginNameValid,
|
||||||
isPluginTypeValid,
|
isPluginTypeValid,
|
||||||
isPluginVersionValid
|
isPluginVersionValid
|
||||||
|
@ -42,6 +43,11 @@ export class PluginModel extends Model<PluginModel> {
|
||||||
@Column
|
@Column
|
||||||
version: string
|
version: string
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Is('PluginLatestVersion', value => throwIfNotValid(value, isPluginVersionValid, 'version'))
|
||||||
|
@Column
|
||||||
|
latestVersion: string
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Column
|
@Column
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
@ -103,27 +109,28 @@ export class PluginModel extends Model<PluginModel> {
|
||||||
return PluginModel.findOne(query)
|
return PluginModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSetting (pluginName: string, settingName: string) {
|
static getSetting (pluginName: string, pluginType: PluginType, settingName: string) {
|
||||||
const query = {
|
const query = {
|
||||||
attributes: [ 'settings' ],
|
attributes: [ 'settings' ],
|
||||||
where: {
|
where: {
|
||||||
name: pluginName
|
name: pluginName,
|
||||||
|
type: pluginType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PluginModel.findOne(query)
|
return PluginModel.findOne(query)
|
||||||
.then(p => p.settings)
|
.then(p => {
|
||||||
.then(settings => {
|
if (!p || !p.settings) return undefined
|
||||||
if (!settings) return undefined
|
|
||||||
|
|
||||||
return settings[settingName]
|
return p.settings[settingName]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static setSetting (pluginName: string, settingName: string, settingValue: string) {
|
static setSetting (pluginName: string, pluginType: PluginType, settingName: string, settingValue: string) {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
name: pluginName
|
name: pluginName,
|
||||||
|
type: pluginType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,11 +178,18 @@ export class PluginModel extends Model<PluginModel> {
|
||||||
: PluginType.THEME
|
: PluginType.THEME
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static buildNpmName (name: string, type: PluginType) {
|
||||||
|
if (type === PluginType.THEME) return 'peertube-theme-' + name
|
||||||
|
|
||||||
|
return 'peertube-plugin-' + name
|
||||||
|
}
|
||||||
|
|
||||||
toFormattedJSON (): PeerTubePlugin {
|
toFormattedJSON (): PeerTubePlugin {
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
version: this.version,
|
version: this.version,
|
||||||
|
latestVersion: this.latestVersion,
|
||||||
enabled: this.enabled,
|
enabled: this.enabled,
|
||||||
uninstalled: this.uninstalled,
|
uninstalled: this.uninstalled,
|
||||||
peertubeEngine: this.peertubeEngine,
|
peertubeEngine: this.peertubeEngine,
|
||||||
|
|
|
@ -2,7 +2,7 @@ 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 { getAccessToken } from '../../shared/extra-utils/users/login'
|
||||||
import { getMyUserInformation } from '../../shared/extra-utils/users/users'
|
import { getMyUserInformation } from '../../shared/extra-utils/users/users'
|
||||||
import { installPlugin, listPlugins, uninstallPlugin } from '../../shared/extra-utils/server/plugins'
|
import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
|
||||||
import { getServerCredentials } from './cli'
|
import { getServerCredentials } from './cli'
|
||||||
import { User, UserRole } from '../../shared/models/users'
|
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'
|
||||||
|
@ -34,6 +34,16 @@ program
|
||||||
.option('-n, --npm-name <npmName>', 'Install from npm')
|
.option('-n, --npm-name <npmName>', 'Install from npm')
|
||||||
.action((options) => installPluginCLI(options))
|
.action((options) => installPluginCLI(options))
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('update')
|
||||||
|
.description('Update a plugin or a theme')
|
||||||
|
.option('-u, --url <url>', 'Server url')
|
||||||
|
.option('-U, --username <username>', 'Username')
|
||||||
|
.option('-p, --password <token>', 'Password')
|
||||||
|
.option('-P --path <path>', 'Update from a path')
|
||||||
|
.option('-n, --npm-name <npmName>', 'Update from npm')
|
||||||
|
.action((options) => updatePluginCLI(options))
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('uninstall')
|
.command('uninstall')
|
||||||
.description('Uninstall a plugin or a theme')
|
.description('Uninstall a plugin or a theme')
|
||||||
|
@ -122,6 +132,38 @@ async function installPluginCLI (options: any) {
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updatePluginCLI (options: any) {
|
||||||
|
if (!options['path'] && !options['npmName']) {
|
||||||
|
console.error('You need to specify the npm name or the path of the plugin you want to update.\n')
|
||||||
|
program.outputHelp()
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options['path'] && !isAbsolute(options['path'])) {
|
||||||
|
console.error('Path should be absolute.')
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url, username, password } = await getServerCredentials(options)
|
||||||
|
const accessToken = await getAdminTokenOrDie(url, username, password)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updatePlugin({
|
||||||
|
url,
|
||||||
|
accessToken,
|
||||||
|
npmName: options['npmName'],
|
||||||
|
path: options['path']
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Cannot update plugin.', err)
|
||||||
|
process.exit(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Plugin updated.')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
async function uninstallPluginCLI (options: any) {
|
async function uninstallPluginCLI (options: any) {
|
||||||
if (!options['npmName']) {
|
if (!options['npmName']) {
|
||||||
console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n')
|
console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n')
|
||||||
|
|
|
@ -85,7 +85,7 @@ function installPlugin (parameters: {
|
||||||
npmName?: string
|
npmName?: string
|
||||||
expectedStatus?: number
|
expectedStatus?: number
|
||||||
}) {
|
}) {
|
||||||
const { url, accessToken, npmName, path, expectedStatus = 204 } = parameters
|
const { url, accessToken, npmName, path, expectedStatus = 200 } = parameters
|
||||||
const apiPath = '/api/v1/plugins/install'
|
const apiPath = '/api/v1/plugins/install'
|
||||||
|
|
||||||
return makePostBodyRequest({
|
return makePostBodyRequest({
|
||||||
|
@ -97,6 +97,25 @@ function installPlugin (parameters: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePlugin (parameters: {
|
||||||
|
url: string,
|
||||||
|
accessToken: string,
|
||||||
|
path?: string,
|
||||||
|
npmName?: string
|
||||||
|
expectedStatus?: number
|
||||||
|
}) {
|
||||||
|
const { url, accessToken, npmName, path, expectedStatus = 200 } = parameters
|
||||||
|
const apiPath = '/api/v1/plugins/update'
|
||||||
|
|
||||||
|
return makePostBodyRequest({
|
||||||
|
url,
|
||||||
|
path: apiPath,
|
||||||
|
token: accessToken,
|
||||||
|
fields: { npmName, path },
|
||||||
|
statusCodeExpected: expectedStatus
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function uninstallPlugin (parameters: {
|
function uninstallPlugin (parameters: {
|
||||||
url: string,
|
url: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
|
@ -118,6 +137,7 @@ function uninstallPlugin (parameters: {
|
||||||
export {
|
export {
|
||||||
listPlugins,
|
listPlugins,
|
||||||
installPlugin,
|
installPlugin,
|
||||||
|
updatePlugin,
|
||||||
getPlugin,
|
getPlugin,
|
||||||
uninstallPlugin,
|
uninstallPlugin,
|
||||||
getPluginSettings,
|
getPluginSettings,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export interface InstallPlugin {
|
export interface InstallOrUpdatePlugin {
|
||||||
npmName?: string
|
npmName?: string
|
||||||
path?: string
|
path?: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export interface PeerTubePlugin {
|
export interface PeerTubePlugin {
|
||||||
name: string
|
name: string
|
||||||
type: number
|
type: number
|
||||||
|
latestVersion: string
|
||||||
version: string
|
version: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
uninstalled: boolean
|
uninstalled: boolean
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue