/test6/
/storage/
/config/production.yaml
+/config/local.json
/ffmpeg/
/*.sublime-project
/*.sublime-workspace
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 = [
{
...UsersRoutes,
...VideoAbusesRoutes,
...VideoBlacklistRoutes,
- ...JobsRoutes
+ ...JobsRoutes,
+ ...ConfigRoutes
]
}
]
<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">
hasJobsRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
}
+
+ hasConfigRight () {
+ return this.auth.getUser().hasRight(UserRight.MANAGE_CONFIGURATION)
+ }
}
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'
VideoAbuseListComponent,
JobsComponent,
- JobsListComponent
+ JobsListComponent,
+
+ ConfigComponent,
+ EditCustomConfigComponent
],
exports: [
providers: [
FollowService,
UserService,
- JobService
+ JobService,
+ ConfigService
]
})
export class AdminModule { }
--- /dev/null
+import { Component } from '@angular/core'
+
+@Component({
+ template: '<router-outlet></router-outlet>'
+})
+export class ConfigComponent {
+}
--- /dev/null
+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'
+ }
+ }
+ }
+ ]
+ }
+]
--- /dev/null
+<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>
--- /dev/null
+@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;
+}
--- /dev/null
+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)
+ }
+
+}
--- /dev/null
+export * from './edit-custom-config.component'
--- /dev/null
+export * from './edit-custom-config'
+export * from './config.component'
+export * from './config.routes'
--- /dev/null
+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))
+ }
+}
-<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
-
<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
<div class="form-group">
<input
export class AccountDetailsComponent extends FormReactive implements OnInit {
@Input() user: User = null
- error: string = null
-
form: FormGroup
formErrors = {}
validationMessages = {}
autoPlayVideo
}
- this.error = null
this.userService.updateMyDetails(details).subscribe(
() => {
this.notificationsService.success('Success', 'Information updated.')
this.authService.refreshUserInformation()
},
- err => this.error = err.message
+ err => this.notificationsService.error('Error', err.message)
)
}
}
<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>
--- /dev/null
+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.'
+ }
+}
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
git tag -s -a "$version" -m "$version"
npm run build || exit -1
+rm "./client/dist/stats.json" || exit -1
cd ../ || exit -1
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()
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' ]
+ }
+ }
+ }
+}
]
for (const resolution of resolutions) {
- if (configResolutions[resolution.toString()] === true && videoFileHeight > resolution) {
+ if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) {
resolutionsEnabled.push(resolution)
}
}
-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')
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
const CONFIG = {
+ CUSTOM_FILE: getLocalConfigFilePath(),
LISTEN: {
PORT: config.get<number>('listen.port')
},
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') }
}
}
}
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()
// ---------------------------------------------------------------------------
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()
+}
--- /dev/null
+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
+}
--- /dev/null
+/* 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()
+ }
+ })
+})
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 () {
await flushTests()
server = await runServer(1)
+ await setAccessTokensToServers([ server ])
})
it('Should have a correct config on a server with registration enabled', async 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)
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'
import * as WebTorrent from 'webtorrent'
-import { readFile, readdir } from 'fs'
let webtorrent = new WebTorrent()
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)
// ---------------------------------------------------------------------------
export {
- readFilePromise,
- readdirPromise,
dateIsValid,
wait,
webtorrentAdd,
function makePutBodyRequest (options: {
url: string,
path: string,
- token: string,
+ token?: string,
fields: { [ fieldName: string ]: any },
statusCodeExpected?: number
}) {
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'
.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
}
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'
.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 {
--- /dev/null
+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
+ }
+ }
+}
--- /dev/null
+export interface ServerConfig {
+ signup: {
+ allowed: boolean
+ }
+ transcoding: {
+ enabledResolutions: number[]
+ }
+ avatar: {
+ file: {
+ size: {
+ max: number
+ },
+ extensions: string[]
+ }
+ }
+ video: {
+ file: {
+ extensions: string[]
+ }
+ }
+}
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'
+++ /dev/null
-export interface ServerConfig {
- signup: {
- allowed: boolean
- }
- transcoding: {
- enabledResolutions: number[]
- }
- avatar: {
- file: {
- size: {
- max: number
- },
- extensions: string[]
- }
- }
- video: {
- file: {
- extensions: string[]
- }
- }
-}
MANAGE_VIDEO_ABUSES,
MANAGE_VIDEO_BLACKLIST,
MANAGE_JOBS,
+ MANAGE_CONFIGURATION,
REMOVE_ANY_VIDEO,
REMOVE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO_COMMENT
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
```
$ 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 && \