Add ability to update some configuration keys

pull/208/head
Chocobozzz 2018-01-17 10:32:03 +01:00
parent 9581cabc59
commit fd206f0b2d
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
35 changed files with 964 additions and 65 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@
/test6/
/storage/
/config/production.yaml
/config/local.json
/ffmpeg/
/*.sublime-project
/*.sublime-workspace

View File

@ -1,14 +1,15 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ConfigRoutes } from '@app/+admin/config'
import { MetaGuard } from '@ngx-meta/core'
import { AdminComponent } from './admin.component'
import { FollowsRoutes } from './follows'
import { JobsRoutes } from './jobs/job.routes'
import { UsersRoutes } from './users'
import { VideoAbusesRoutes } from './video-abuses'
import { VideoBlacklistRoutes } from './video-blacklist'
import { JobsRoutes } from './jobs/job.routes'
const adminRoutes: Routes = [
{
@ -26,7 +27,8 @@ const adminRoutes: Routes = [
...UsersRoutes,
...VideoAbusesRoutes,
...VideoBlacklistRoutes,
...JobsRoutes
...JobsRoutes,
...ConfigRoutes
]
}
]

View File

@ -19,6 +19,10 @@
<a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active" class="title-page">
Jobs
</a>
<a *ngIf="hasConfigRight()" routerLink="/admin/config" routerLinkActive="active" class="title-page">
Configuration
</a>
</div>
<div class="margin-content">

View File

@ -28,4 +28,8 @@ export class AdminComponent {
hasJobsRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
}
hasConfigRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_CONFIGURATION)
}
}

View File

@ -1,4 +1,6 @@
import { NgModule } from '@angular/core'
import { ConfigComponent, EditCustomConfigComponent } from '@app/+admin/config'
import { ConfigService } from '@app/+admin/config/shared/config.service'
import { TabsModule } from 'ngx-bootstrap/tabs'
import { DataTableModule } from 'primeng/components/datatable/datatable'
import { SharedModule } from '../shared'
@ -41,7 +43,10 @@ import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-bl
VideoAbuseListComponent,
JobsComponent,
JobsListComponent
JobsListComponent,
ConfigComponent,
EditCustomConfigComponent
],
exports: [
@ -51,7 +56,8 @@ import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-bl
providers: [
FollowService,
UserService,
JobService
JobService,
ConfigService
]
})
export class AdminModule { }

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core'
@Component({
template: '<router-outlet></router-outlet>'
})
export class ConfigComponent {
}

View File

@ -0,0 +1,32 @@
import { Routes } from '@angular/router'
import { EditCustomConfigComponent } from '@app/+admin/config/edit-custom-config'
import { UserRightGuard } from '@app/core'
import { UserRight } from '../../../../../shared/models/users'
import { ConfigComponent } from './config.component'
export const ConfigRoutes: Routes = [
{
path: 'config',
component: ConfigComponent,
canActivate: [ UserRightGuard ],
data: {
userRight: UserRight.MANAGE_CONFIGURATION
},
children: [
{
path: '',
redirectTo: 'edit-custom',
pathMatch: 'full'
},
{
path: 'edit-custom',
component: EditCustomConfigComponent,
data: {
meta: {
title: 'Following list'
}
}
}
]
}
]

View File

@ -0,0 +1,97 @@
<div class="admin-sub-title">Update PeerTube configuration</div>
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="inner-form-title">Cache</div>
<div class="form-group">
<label for="cachePreviewsSize">Preview cache size</label>
<input
type="text" id="cachePreviewsSize"
formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
>
<div *ngIf="formErrors.cachePreviewsSize" class="form-error">
{{ formErrors.cachePreviewsSize }}
</div>
</div>
<div class="inner-form-title">Signup</div>
<div class="form-group">
<input type="checkbox" id="signupEnabled" formControlName="signupEnabled">
<label for="signupEnabled"></label>
<label for="signupEnabled">Signup enabled</label>
</div>
<div *ngIf="isSignupEnabled()" class="form-group">
<label for="signupLimit">Signup limit</label>
<input
type="text" id="signupLimit"
formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }"
>
<div *ngIf="formErrors.signupLimit" class="form-error">
{{ formErrors.signupLimit }}
</div>
</div>
<div class="inner-form-title">Administrator</div>
<div class="form-group">
<label for="adminEmail">Admin email</label>
<input
type="text" id="adminEmail"
formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }"
>
<div *ngIf="formErrors.adminEmail" class="form-error">
{{ formErrors.adminEmail }}
</div>
</div>
<div class="inner-form-title">Users</div>
<div class="form-group">
<label for="userVideoQuota">User default video quota</label>
<div class="peertube-select-container">
<select id="userVideoQuota" formControlName="userVideoQuota">
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
</div>
</div>
<div class="inner-form-title">Transcoding</div>
<div class="form-group">
<input type="checkbox" id="transcodingEnabled" formControlName="transcodingEnabled">
<label for="transcodingEnabled"></label>
<label for="transcodingEnabled">Transcoding enabled</label>
</div>
<ng-template [ngIf]="isTranscodingEnabled()">
<div class="form-group">
<label for="transcodingThreads">Transcoding threads</label>
<div class="peertube-select-container">
<select id="transcodingThreads" formControlName="transcodingThreads">
<option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
{{ transcodingThreadOption.label }}
</option>
</select>
</div>
</div>
<div class="form-group" *ngFor="let resolution of resolutions">
<input
type="checkbox" [id]="getResolutionKey(resolution)"
[formControlName]="getResolutionKey(resolution)"
>
<label [for]="getResolutionKey(resolution)"></label>
<label [for]="getResolutionKey(resolution)">Resolution {{ resolution }} enabled</label>
</div>
</ng-template>
<input type="submit" value="Update configuration" [disabled]="!form.valid">
</form>

View File

@ -0,0 +1,31 @@
@import '_variables';
@import '_mixins';
input[type=text] {
@include peertube-input-text(340px);
display: block;
}
input[type=checkbox] {
@include peertube-checkbox(1px);
}
.peertube-select-container {
@include peertube-select-container(340px);
}
input[type=submit] {
@include peertube-button;
@include orange-button;
margin-top: 20px;
}
.inner-form-title {
text-transform: uppercase;
color: $orange-color;
font-weight: $font-bold;
font-size: 13px;
margin-top: 30px;
margin-bottom: 10px;
}

View File

@ -0,0 +1,174 @@
import { Component, OnInit } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
import { Router } from '@angular/router'
import { ConfigService } from '@app/+admin/config/shared/config.service'
import { ServerService } from '@app/core/server/server.service'
import { FormReactive, USER_VIDEO_QUOTA } from '@app/shared'
import { ADMIN_EMAIL, CACHE_PREVIEWS_SIZE, SIGNUP_LIMIT, TRANSCODING_THREADS } from '@app/shared/forms/form-validators/custom-config'
import { NotificationsService } from 'angular2-notifications'
import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model'
@Component({
selector: 'my-edit-custom-config',
templateUrl: './edit-custom-config.component.html',
styleUrls: [ './edit-custom-config.component.scss' ]
})
export class EditCustomConfigComponent extends FormReactive implements OnInit {
customConfig: CustomConfig
resolutions = [ '240p', '360p', '480p', '720p', '1080p' ]
videoQuotaOptions = [
{ value: -1, label: 'Unlimited' },
{ value: 0, label: '0' },
{ value: 100 * 1024 * 1024, label: '100MB' },
{ value: 500 * 1024 * 1024, label: '500MB' },
{ value: 1024 * 1024 * 1024, label: '1GB' },
{ value: 5 * 1024 * 1024 * 1024, label: '5GB' },
{ value: 20 * 1024 * 1024 * 1024, label: '20GB' },
{ value: 50 * 1024 * 1024 * 1024, label: '50GB' }
]
transcodingThreadOptions = [
{ value: 1, label: '1' },
{ value: 2, label: '2' },
{ value: 4, label: '4' },
{ value: 8, label: '8' }
]
form: FormGroup
formErrors = {
cachePreviewsSize: '',
signupLimit: '',
adminEmail: '',
userVideoQuota: '',
transcodingThreads: ''
}
validationMessages = {
cachePreviewsSize: CACHE_PREVIEWS_SIZE.MESSAGES,
signupLimit: SIGNUP_LIMIT.MESSAGES,
adminEmail: ADMIN_EMAIL.MESSAGES,
userVideoQuota: USER_VIDEO_QUOTA.MESSAGES
}
constructor (
private formBuilder: FormBuilder,
private router: Router,
private notificationsService: NotificationsService,
private configService: ConfigService,
private serverService: ServerService
) {
super()
}
getResolutionKey (resolution: string) {
return 'transcodingResolution' + resolution
}
buildForm () {
const formGroupData = {
cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ],
signupEnabled: [ ],
signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ],
adminEmail: [ '', ADMIN_EMAIL.VALIDATORS ],
userVideoQuota: [ '', USER_VIDEO_QUOTA.VALIDATORS ],
transcodingThreads: [ '', TRANSCODING_THREADS.VALIDATORS ],
transcodingEnabled: [ ]
}
for (const resolution of this.resolutions) {
const key = this.getResolutionKey(resolution)
formGroupData[key] = [ false ]
}
this.form = this.formBuilder.group(formGroupData)
this.form.valueChanges.subscribe(data => this.onValueChanged(data))
}
ngOnInit () {
this.buildForm()
this.configService.getCustomConfig()
.subscribe(
res => {
this.customConfig = res
this.updateForm()
},
err => this.notificationsService.error('Error', err.message)
)
}
isTranscodingEnabled () {
return this.form.value['transcodingEnabled'] === true
}
isSignupEnabled () {
return this.form.value['signupEnabled'] === true
}
formValidated () {
const data = {
cache: {
previews: {
size: this.form.value['cachePreviewsSize']
}
},
signup: {
enabled: this.form.value['signupEnabled'],
limit: this.form.value['signupLimit']
},
admin: {
email: this.form.value['adminEmail']
},
user: {
videoQuota: this.form.value['userVideoQuota']
},
transcoding: {
enabled: this.form.value['transcodingEnabled'],
threads: this.form.value['transcodingThreads'],
resolutions: {
'240p': this.form.value[this.getResolutionKey('240p')],
'360p': this.form.value[this.getResolutionKey('360p')],
'480p': this.form.value[this.getResolutionKey('480p')],
'720p': this.form.value[this.getResolutionKey('720p')],
'1080p': this.form.value[this.getResolutionKey('1080p')]
}
}
}
this.configService.updateCustomConfig(data)
.subscribe(
res => {
this.customConfig = res
// Reload general configuration
this.serverService.loadConfig()
this.updateForm()
},
err => this.notificationsService.error('Error', err.message)
)
}
private updateForm () {
const data = {
cachePreviewsSize: this.customConfig.cache.previews.size,
signupEnabled: this.customConfig.signup.enabled,
signupLimit: this.customConfig.signup.limit,
adminEmail: this.customConfig.admin.email,
userVideoQuota: this.customConfig.user.videoQuota,
transcodingThreads: this.customConfig.transcoding.threads,
transcodingEnabled: this.customConfig.transcoding.enabled
}
for (const resolution of this.resolutions) {
const key = this.getResolutionKey(resolution)
data[key] = this.customConfig.transcoding.resolutions[resolution]
}
this.form.patchValue(data)
}
}

View File

@ -0,0 +1 @@
export * from './edit-custom-config.component'

View File

@ -0,0 +1,3 @@
export * from './edit-custom-config'
export * from './config.component'
export * from './config.routes'

View File

@ -0,0 +1,26 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model'
import { environment } from '../../../../environments/environment'
import { RestExtractor, RestService } from '../../../shared'
@Injectable()
export class ConfigService {
private static BASE_APPLICATION_URL = environment.apiUrl + '/api/v1/config'
constructor (
private authHttp: HttpClient,
private restService: RestService,
private restExtractor: RestExtractor
) {}
getCustomConfig () {
return this.authHttp.get<CustomConfig>(ConfigService.BASE_APPLICATION_URL + '/custom')
.catch(res => this.restExtractor.handleError(res))
}
updateCustomConfig (data: CustomConfig) {
return this.authHttp.put<CustomConfig>(ConfigService.BASE_APPLICATION_URL + '/custom', data)
.catch(res => this.restExtractor.handleError(res))
}
}

View File

@ -1,5 +1,3 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
<div class="form-group">
<input

View File

@ -14,8 +14,6 @@ import { FormReactive, User, UserService } from '../../../shared'
export class AccountDetailsComponent extends FormReactive implements OnInit {
@Input() user: User = null
error: string = null
form: FormGroup
formErrors = {}
validationMessages = {}
@ -50,7 +48,6 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
autoPlayVideo
}
this.error = null
this.userService.updateMyDetails(details).subscribe(
() => {
this.notificationsService.success('Success', 'Information updated.')
@ -58,7 +55,7 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
this.authService.refreshUserInformation()
},
err => this.error = err.message
err => this.notificationsService.error('Error', err.message)
)
}
}

View File

@ -14,7 +14,7 @@
<ul *dropdownMenu class="dropdown-menu">
<li>
<a routerLink="/account/settings" class="dropdown-item" title="My account">
<a i18n routerLink="/account/settings" class="dropdown-item" title="My account">
My account
</a>

View File

@ -0,0 +1,35 @@
import { Validators } from '@angular/forms'
export const CACHE_PREVIEWS_SIZE = {
VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
MESSAGES: {
'required': 'Preview cache size is required.',
'min': 'Preview cache size must be greater than 1.',
'pattern': 'Preview cache size must be a number.'
}
}
export const SIGNUP_LIMIT = {
VALIDATORS: [ Validators.required, Validators.min(1), Validators.pattern('[0-9]+') ],
MESSAGES: {
'required': 'Signup limit is required.',
'min': 'Signup limit must be greater than 1.',
'pattern': 'Preview cache size must be a number.'
}
}
export const ADMIN_EMAIL = {
VALIDATORS: [ Validators.required, Validators.email ],
MESSAGES: {
'required': 'Admin email is required.',
'email': 'Admin email must be valid.'
}
}
export const TRANSCODING_THREADS = {
VALIDATORS: [ Validators.required, Validators.min(1) ],
MESSAGES: {
'required': 'Transcoding threads is required.',
'min': 'Transcoding threads must be greater than 1.'
}
}

View File

@ -3,5 +3,7 @@
for i in $(seq 1 6); do
dropdb "peertube_test$i"
rm -rf "./test$i"
rm -f "./config/local-test.json"
rm -f "./config/local-test-$i.json"
createdb "peertube_test$i"
done

View File

@ -35,6 +35,7 @@ git commit package.json client/package.json -m "Bumped to version $version" || e
git tag -s -a "$version" -m "$version"
npm run build || exit -1
rm "./client/dist/stats.json" || exit -1
cd ../ || exit -1

View File

@ -1,15 +1,34 @@
import * as express from 'express'
import { ServerConfig, UserRight } from '../../../shared'
import { CustomConfig } from '../../../shared/models/config/custom-config.model'
import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils'
import { isSignupAllowed } from '../../helpers/utils'
import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { asyncMiddleware } from '../../middlewares'
import { ServerConfig } from '../../../shared'
import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers'
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
import { customConfigUpdateValidator } from '../../middlewares/validators/config'
import { omit } from 'lodash'
const configRouter = express.Router()
configRouter.get('/',
asyncMiddleware(getConfig)
)
configRouter.get('/custom',
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
asyncMiddleware(getCustomConfig)
)
configRouter.put('/custom',
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
asyncMiddleware(customConfigUpdateValidator),
asyncMiddleware(updateCustomConfig)
)
configRouter.delete('/custom',
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
asyncMiddleware(deleteCustomConfig)
)
async function getConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
const allowed = await isSignupAllowed()
@ -43,8 +62,72 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
return res.json(json)
}
async function getCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
const data = customConfig()
return res.json(data).end()
}
async function deleteCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
await unlinkPromise(CONFIG.CUSTOM_FILE)
reloadConfig()
const data = customConfig()
return res.json(data).end()
}
async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) {
const toUpdate: CustomConfig = req.body
// Need to change the videoQuota key a little bit
const toUpdateJSON = omit(toUpdate, 'videoQuota')
toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
await writeFilePromise(CONFIG.CUSTOM_FILE, JSON.stringify(toUpdateJSON))
reloadConfig()
const data = customConfig()
return res.json(data).end()
}
// ---------------------------------------------------------------------------
export {
configRouter
}
// ---------------------------------------------------------------------------
function customConfig (): CustomConfig {
return {
cache: {
previews: {
size: CONFIG.CACHE.PREVIEWS.SIZE
}
},
signup: {
enabled: CONFIG.SIGNUP.ENABLED,
limit: CONFIG.SIGNUP.LIMIT
},
admin: {
email: CONFIG.ADMIN.EMAIL
},
user: {
videoQuota: CONFIG.USER.VIDEO_QUOTA
},
transcoding: {
enabled: CONFIG.TRANSCODING.ENABLED,
threads: CONFIG.TRANSCODING.THREADS,
resolutions: {
'240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ],
'360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ],
'480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ],
'720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ],
'1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ]
}
}
}
}

View File

@ -104,7 +104,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
]
for (const resolution of resolutions) {
if (configResolutions[resolution.toString()] === true && videoFileHeight > resolution) {
if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) {
resolutionsEnabled.push(resolution)
}
}

View File

@ -1,11 +1,14 @@
import * as config from 'config'
import { join } from 'path'
import { IConfig } from 'config'
import { dirname, join } from 'path'
import { JobCategory, JobState, VideoRateType } from '../../shared/models'
import { ActivityPubActorType } from '../../shared/models/activitypub'
import { FollowState } from '../../shared/models/actors'
import { VideoPrivacy } from '../../shared/models/videos'
// Do not use barrels, remain constants as independent as possible
import { buildPath, isTestInstance, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
// Use a variable to reload the configuration if we need
let config: IConfig = require('config')
// ---------------------------------------------------------------------------
@ -82,6 +85,7 @@ let SCHEDULER_INTERVAL = 60000 * 60
// ---------------------------------------------------------------------------
const CONFIG = {
CUSTOM_FILE: getLocalConfigFilePath(),
LISTEN: {
PORT: config.get<number>('listen.port')
},
@ -110,29 +114,29 @@ const CONFIG = {
HOST: ''
},
ADMIN: {
EMAIL: config.get<string>('admin.email')
get EMAIL () { return config.get<string>('admin.email') }
},
SIGNUP: {
ENABLED: config.get<boolean>('signup.enabled'),
LIMIT: config.get<number>('signup.limit')
get ENABLED () { return config.get<boolean>('signup.enabled') },
get LIMIT () { return config.get<number>('signup.limit') }
},
USER: {
VIDEO_QUOTA: config.get<number>('user.video_quota')
get VIDEO_QUOTA () { return config.get<number>('user.video_quota') }
},
TRANSCODING: {
ENABLED: config.get<boolean>('transcoding.enabled'),
THREADS: config.get<number>('transcoding.threads'),
get ENABLED () { return config.get<boolean>('transcoding.enabled') },
get THREADS () { return config.get<number>('transcoding.threads') },
RESOLUTIONS: {
'240' : config.get<boolean>('transcoding.resolutions.240p'),
'360': config.get<boolean>('transcoding.resolutions.360p'),
'480': config.get<boolean>('transcoding.resolutions.480p'),
'720': config.get<boolean>('transcoding.resolutions.720p'),
'1080': config.get<boolean>('transcoding.resolutions.1080p')
get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') },
get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') },
get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') },
get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') }
}
},
CACHE: {
PREVIEWS: {
SIZE: config.get<number>('cache.previews.size')
get SIZE () { return config.get<number>('cache.previews.size') }
}
}
}
@ -361,8 +365,7 @@ if (isTestInstance() === true) {
SCHEDULER_INTERVAL = 10000
}
CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT)
CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP)
updateWebserverConfig()
// ---------------------------------------------------------------------------
@ -404,3 +407,50 @@ export {
AVATAR_MIMETYPE_EXT,
SCHEDULER_INTERVAL
}
// ---------------------------------------------------------------------------
function getLocalConfigFilePath () {
const configSources = config.util.getConfigSources()
if (configSources.length === 0) throw new Error('Invalid config source.')
let filename = 'local'
if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}`
if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}`
return join(dirname(configSources[ 0 ].name), filename + '.json')
}
function updateWebserverConfig () {
CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT)
CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP)
}
export function reloadConfig () {
function directory () {
if (process.env.NODE_CONFIG_DIR) {
return process.env.NODE_CONFIG_DIR
}
return join(root(), 'config')
}
function purge () {
for (const fileName in require.cache) {
if (-1 === fileName.indexOf(directory())) {
continue
}
delete require.cache[fileName]
}
delete require.cache[require.resolve('config')]
}
purge()
config = require('config')
updateWebserverConfig()
}

View File

@ -0,0 +1,32 @@
import * as express from 'express'
import { body } from 'express-validator/check'
import { isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
import { logger } from '../../helpers/logger'
import { areValidationErrors } from './utils'
const customConfigUpdateValidator = [
body('cache.previews.size').isInt().withMessage('Should have a valid previews size'),
body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'),
body('signup.limit').isInt().withMessage('Should have a valid signup limit'),
body('admin.email').isEmail().withMessage('Should have a valid administrator email'),
body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'),
body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'),
body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'),
body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'),
body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'),
body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'),
body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'),
body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
return next()
}
]
export {
customConfigUpdateValidator
}

View File

@ -0,0 +1,152 @@
/* tslint:disable:no-unused-expression */
import { omit } from 'lodash'
import 'mocha'
import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
import {
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,
setAccessTokensToServers, userLogin
} from '../../utils'
describe('Test config API validators', function () {
const path = '/api/v1/config/custom'
let server: ServerInfo
let userAccessToken: string
const updateParams: CustomConfig = {
cache: {
previews: {
size: 2
}
},
signup: {
enabled: false,
limit: 5
},
admin: {
email: 'superadmin1@example.com'
},
user: {
videoQuota: 5242881
},
transcoding: {
enabled: true,
threads: 1,
resolutions: {
'240p': false,
'360p': true,
'480p': true,
'720p': false,
'1080p': false
}
}
}
// ---------------------------------------------------------------
before(async function () {
this.timeout(20000)
await flushTests()
server = await runServer(1)
await setAccessTokensToServers([ server ])
const user = {
username: 'user1',
password: 'password'
}
await createUser(server.url, server.accessToken, user.username, user.password)
userAccessToken = await userLogin(server, user)
})
describe('When getting the configuration', function () {
it('Should fail without token', async function () {
await makeGetRequest({
url: server.url,
path,
statusCodeExpected: 401
})
})
it('Should fail if the user is not an administrator', async function () {
await makeGetRequest({
url: server.url,
path,
token: userAccessToken,
statusCodeExpected: 403
})
})
})
describe('When updating the configuration', function () {
it('Should fail without token', async function () {
await makePutBodyRequest({
url: server.url,
path,
fields: updateParams,
statusCodeExpected: 401
})
})
it('Should fail if the user is not an administrator', async function () {
await makePutBodyRequest({
url: server.url,
path,
fields: updateParams,
token: userAccessToken,
statusCodeExpected: 403
})
})
it('Should fail if it misses a key', async function () {
const newUpdateParams = omit(updateParams, 'admin.email')
await makePutBodyRequest({
url: server.url,
path,
fields: newUpdateParams,
token: server.accessToken,
statusCodeExpected: 400
})
})
it('Should success with the correct parameters', async function () {
await makePutBodyRequest({
url: server.url,
path,
fields: updateParams,
token: server.accessToken,
statusCodeExpected: 200
})
})
})
describe('When deleting the configuration', function () {
it('Should fail without token', async function () {
await makeDeleteRequest({
url: server.url,
path,
statusCodeExpected: 401
})
})
it('Should fail if the user is not an administrator', async function () {
await makeDeleteRequest({
url: server.url,
path,
token: userAccessToken,
statusCodeExpected: 403
})
})
})
after(async function () {
killallServers([ server ])
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -2,13 +2,14 @@
import 'mocha'
import * as chai from 'chai'
import { deleteCustomConfig, killallServers, reRunServer } from '../../utils'
const expect = chai.expect
import {
getConfig,
flushTests,
runServer,
registerUser
registerUser, getCustomConfig, setAccessTokensToServers, updateCustomConfig
} from '../../utils/index'
describe('Test config', function () {
@ -19,6 +20,7 @@ describe('Test config', function () {
await flushTests()
server = await runServer(1)
await setAccessTokensToServers([ server ])
})
it('Should have a correct config on a server with registration enabled', async function () {
@ -43,6 +45,114 @@ describe('Test config', function () {
expect(data.signup.allowed).to.be.false
})
it('Should get the customized configuration', async function () {
const res = await getCustomConfig(server.url, server.accessToken)
const data = res.body
expect(data.cache.previews.size).to.equal(1)
expect(data.signup.enabled).to.be.true
expect(data.signup.limit).to.equal(4)
expect(data.admin.email).to.equal('admin1@example.com')
expect(data.user.videoQuota).to.equal(5242880)
expect(data.transcoding.enabled).to.be.false
expect(data.transcoding.threads).to.equal(2)
expect(data.transcoding.resolutions['240p']).to.be.true
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
expect(data.transcoding.resolutions['720p']).to.be.true
expect(data.transcoding.resolutions['1080p']).to.be.true
})
it('Should update the customized configuration', async function () {
const newCustomConfig = {
cache: {
previews: {
size: 2
}
},
signup: {
enabled: false,
limit: 5
},
admin: {
email: 'superadmin1@example.com'
},
user: {
videoQuota: 5242881
},
transcoding: {
enabled: true,
threads: 1,
resolutions: {
'240p': false,
'360p': true,
'480p': true,
'720p': false,
'1080p': false
}
}
}
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
const res = await getCustomConfig(server.url, server.accessToken)
const data = res.body
expect(data.cache.previews.size).to.equal(2)
expect(data.signup.enabled).to.be.false
expect(data.signup.limit).to.equal(5)
expect(data.admin.email).to.equal('superadmin1@example.com')
expect(data.user.videoQuota).to.equal(5242881)
expect(data.transcoding.enabled).to.be.true
expect(data.transcoding.threads).to.equal(1)
expect(data.transcoding.resolutions['240p']).to.be.false
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
expect(data.transcoding.resolutions['720p']).to.be.false
expect(data.transcoding.resolutions['1080p']).to.be.false
})
it('Should have the configuration updated after a restart', async function () {
killallServers([ server ])
await reRunServer(server)
const res = await getCustomConfig(server.url, server.accessToken)
const data = res.body
expect(data.cache.previews.size).to.equal(2)
expect(data.signup.enabled).to.be.false
expect(data.signup.limit).to.equal(5)
expect(data.admin.email).to.equal('superadmin1@example.com')
expect(data.user.videoQuota).to.equal(5242881)
expect(data.transcoding.enabled).to.be.true
expect(data.transcoding.threads).to.equal(1)
expect(data.transcoding.resolutions['240p']).to.be.false
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
expect(data.transcoding.resolutions['720p']).to.be.false
expect(data.transcoding.resolutions['1080p']).to.be.false
})
it('Should remove the custom configuration', async function () {
await deleteCustomConfig(server.url, server.accessToken)
const res = await getCustomConfig(server.url, server.accessToken)
const data = res.body
expect(data.cache.previews.size).to.equal(1)
expect(data.signup.enabled).to.be.true
expect(data.signup.limit).to.equal(4)
expect(data.admin.email).to.equal('admin1@example.com')
expect(data.user.videoQuota).to.equal(5242880)
expect(data.transcoding.enabled).to.be.false
expect(data.transcoding.threads).to.equal(2)
expect(data.transcoding.resolutions['240p']).to.be.true
expect(data.transcoding.resolutions['360p']).to.be.true
expect(data.transcoding.resolutions['480p']).to.be.true
expect(data.transcoding.resolutions['720p']).to.be.true
expect(data.transcoding.resolutions['1080p']).to.be.true
})
after(async function () {
process.kill(-server.app.pid)

View File

@ -5,9 +5,10 @@ import { keyBy } from 'lodash'
import 'mocha'
import { join } from 'path'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { readdirPromise } from '../../../helpers/core-utils'
import {
completeVideoCheck, flushTests, getVideo, getVideoCategories, getVideoLanguages, getVideoLicences, getVideoPrivacies,
getVideosList, getVideosListPagination, getVideosListSort, killallServers, rateVideo, readdirPromise, removeVideo, runServer, searchVideo,
getVideosList, getVideosListPagination, getVideosListSort, killallServers, rateVideo, removeVideo, runServer, searchVideo,
searchVideoWithPagination, searchVideoWithSort, ServerInfo, setAccessTokensToServers, testVideoImage, updateVideo, uploadVideo, viewVideo
} from '../../utils'

View File

@ -1,5 +1,4 @@
import * as WebTorrent from 'webtorrent'
import { readFile, readdir } from 'fs'
let webtorrent = new WebTorrent()
@ -7,26 +6,6 @@ function immutableAssign <T, U> (target: T, source: U) {
return Object.assign<{}, T, U>({}, target, source)
}
function readFilePromise (path: string) {
return new Promise<Buffer>((res, rej) => {
readFile(path, (err, data) => {
if (err) return rej(err)
return res(data)
})
})
}
function readdirPromise (path: string) {
return new Promise<string[]>((res, rej) => {
readdir(path, (err, files) => {
if (err) return rej(err)
return res(files)
})
})
}
// Default interval -> 5 minutes
function dateIsValid (dateString: string, interval = 300000) {
const dateToCheck = new Date(dateString)
@ -48,8 +27,6 @@ function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
// ---------------------------------------------------------------------------
export {
readFilePromise,
readdirPromise,
dateIsValid,
wait,
webtorrentAdd,

View File

@ -99,7 +99,7 @@ function makePostBodyRequest (options: {
function makePutBodyRequest (options: {
url: string,
path: string,
token: string,
token?: string,
fields: { [ fieldName: string ]: any },
statusCodeExpected?: number
}) {

View File

@ -1,4 +1,6 @@
import * as request from 'supertest'
import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../'
import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
function getConfig (url: string) {
const path = '/api/v1/config'
@ -10,8 +12,45 @@ function getConfig (url: string) {
.expect('Content-Type', /json/)
}
function getCustomConfig (url: string, token: string, statusCodeExpected = 200) {
const path = '/api/v1/config/custom'
return makeGetRequest({
url,
token,
path,
statusCodeExpected
})
}
function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = 200) {
const path = '/api/v1/config/custom'
return makePutBodyRequest({
url,
token,
path,
fields: newCustomConfig,
statusCodeExpected
})
}
function deleteCustomConfig (url: string, token: string, statusCodeExpected = 200) {
const path = '/api/v1/config/custom'
return makeDeleteRequest({
url,
token,
path,
statusCodeExpected
})
}
// ---------------------------------------------------------------------------
export {
getConfig
getConfig,
getCustomConfig,
updateCustomConfig,
deleteCustomConfig
}

View File

@ -5,8 +5,9 @@ import { readFile } from 'fs'
import * as parseTorrent from 'parse-torrent'
import { extname, isAbsolute, join } from 'path'
import * as request from 'supertest'
import { getMyUserInformation, makeGetRequest, readFilePromise, ServerInfo } from '../'
import { getMyUserInformation, makeGetRequest, ServerInfo } from '../'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { readFileBufferPromise } from '../../../helpers/core-utils'
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers'
import { dateIsValid, webtorrentAdd } from '../index'
@ -210,7 +211,7 @@ async function testVideoImage (url: string, imageName: string, imagePath: string
.get(imagePath)
.expect(200)
const data = await readFilePromise(join(__dirname, '..', '..', 'api', 'fixtures', imageName + extension))
const data = await readFileBufferPromise(join(__dirname, '..', '..', 'api', 'fixtures', imageName + extension))
return data.equals(res.body)
} else {

View File

@ -0,0 +1,32 @@
export interface CustomConfig {
cache: {
previews: {
size: number
}
}
signup: {
enabled: boolean
limit: number
}
admin: {
email: string
}
user: {
videoQuota: number
}
transcoding: {
enabled: boolean
threads: number
resolutions: {
'240p': boolean
'360p': boolean
'480p': boolean
'720p': boolean
'1080p': boolean
}
}
}

View File

@ -5,4 +5,4 @@ export * from './videos'
export * from './job.model'
export * from './oauth-client-local.model'
export * from './result-list.model'
export * from './server-config.model'
export * from './config/server-config.model'

View File

@ -5,6 +5,7 @@ export enum UserRight {
MANAGE_VIDEO_ABUSES,
MANAGE_VIDEO_BLACKLIST,
MANAGE_JOBS,
MANAGE_CONFIGURATION,
REMOVE_ANY_VIDEO,
REMOVE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO_COMMENT

View File

@ -31,7 +31,7 @@ $ VERSION=$(curl -s https://api.github.com/repos/chocobozzz/peertube/releases/la
cd /home/peertube && \
sudo -u peertube mkdir config storage versions && \
cd versions && \
sudo -u peertube wget "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip" && \
sudo -u peertube wget -q "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip" && \
sudo -u peertube unzip peertube-${VERSION}.zip && sudo -u peertube rm peertube-${VERSION}.zip && \
cd ../ && sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest && \
cd ./peertube-latest && sudo -u peertube yarn install --production --pure-lockfile
@ -227,7 +227,7 @@ $ NODE_ENV=production npm run reset-password -- -u root
```
$ VERSION=$(curl -s https://api.github.com/repos/chocobozzz/peertube/releases/latest | grep tag_name | cut -d '"' -f 4) && \
cd /home/peertube/versions && \
sudo -u peertube wget "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip" && \
sudo -u peertube wget -q "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip" && \
sudo -u peertube unzip -o peertube-${VERSION}.zip && sudo -u peertube rm peertube-${VERSION}.zip && \
cd ../ && sudo rm ./peertube-latest && sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest && \
cd ./peertube-latest && sudo -u peertube yarn install --production --pure-lockfile && \