Add ability to update some configuration keys
authorChocobozzz <me@florianbigard.com>
Wed, 17 Jan 2018 09:32:03 +0000 (10:32 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 17 Jan 2018 09:41:27 +0000 (10:41 +0100)
36 files changed:
.gitignore
client/src/app/+admin/admin-routing.module.ts
client/src/app/+admin/admin.component.html
client/src/app/+admin/admin.component.ts
client/src/app/+admin/admin.module.ts
client/src/app/+admin/config/config.component.ts [new file with mode: 0644]
client/src/app/+admin/config/config.routes.ts [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts [new file with mode: 0644]
client/src/app/+admin/config/edit-custom-config/index.ts [new file with mode: 0644]
client/src/app/+admin/config/index.ts [new file with mode: 0644]
client/src/app/+admin/config/shared/config.service.ts [new file with mode: 0644]
client/src/app/account/account-settings/account-details/account-details.component.html
client/src/app/account/account-settings/account-details/account-details.component.ts
client/src/app/menu/menu.component.html
client/src/app/shared/forms/form-validators/custom-config.ts [new file with mode: 0644]
scripts/clean/server/test.sh
scripts/release.sh
server/controllers/api/config.ts
server/helpers/utils.ts
server/initializers/constants.ts
server/middlewares/validators/config.ts [new file with mode: 0644]
server/tests/api/check-params/config.ts [new file with mode: 0644]
server/tests/api/server/config.ts
server/tests/api/videos/single-server.ts
server/tests/utils/miscs/miscs.ts
server/tests/utils/requests/requests.ts
server/tests/utils/server/config.ts
server/tests/utils/videos/videos.ts
shared/models/config/custom-config.model.ts [new file with mode: 0644]
shared/models/config/server-config.model.ts [new file with mode: 0644]
shared/models/index.ts
shared/models/server-config.model.ts [deleted file]
shared/models/users/user-right.enum.ts
support/doc/production.md

index 2805af0fc082a28d8303b3b985fe20f58615a370..b373b368bd48eccb66f36e04458f641ceeacb048 100644 (file)
@@ -7,6 +7,7 @@
 /test6/
 /storage/
 /config/production.yaml
+/config/local.json
 /ffmpeg/
 /*.sublime-project
 /*.sublime-workspace
index 7ef5c61050f4261ea722c100a84b9ad1ed8588b2..0301d760184e1f48738489f99f81176784f5bf15 100644 (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
     ]
   }
 ]
index 0bf4c8aac20958a532d9061ee83bff8d2067d37d..e4644498b5052c6297d9f915bf460044d1f695b0 100644 (file)
     <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">
index 75cd50cc7543caa7fb61b090a69853ddee939c4a..1a4dd67862d96460b57f1b24db192f9420171bfe 100644 (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)
+  }
 }
index 74ceb25efafe90cbef6b632a1f217ac75899c7e3..1d9120490e6dd4314cc666df8b90addd7cf994a0 100644 (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 { }
diff --git a/client/src/app/+admin/config/config.component.ts b/client/src/app/+admin/config/config.component.ts
new file mode 100644 (file)
index 0000000..e0eb772
--- /dev/null
@@ -0,0 +1,7 @@
+import { Component } from '@angular/core'
+
+@Component({
+  template: '<router-outlet></router-outlet>'
+})
+export class ConfigComponent {
+}
diff --git a/client/src/app/+admin/config/config.routes.ts b/client/src/app/+admin/config/config.routes.ts
new file mode 100644 (file)
index 0000000..a46b0dd
--- /dev/null
@@ -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'
+          }
+        }
+      }
+    ]
+  }
+]
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
new file mode 100644 (file)
index 0000000..c568a43
--- /dev/null
@@ -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>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
new file mode 100644 (file)
index 0000000..0195f44
--- /dev/null
@@ -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;
+}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
new file mode 100644 (file)
index 0000000..1b35227
--- /dev/null
@@ -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)
+  }
+
+}
diff --git a/client/src/app/+admin/config/edit-custom-config/index.ts b/client/src/app/+admin/config/edit-custom-config/index.ts
new file mode 100644 (file)
index 0000000..1ec1263
--- /dev/null
@@ -0,0 +1 @@
+export * from './edit-custom-config.component'
diff --git a/client/src/app/+admin/config/index.ts b/client/src/app/+admin/config/index.ts
new file mode 100644 (file)
index 0000000..b47ebf8
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './edit-custom-config'
+export * from './config.component'
+export * from './config.routes'
diff --git a/client/src/app/+admin/config/shared/config.service.ts b/client/src/app/+admin/config/shared/config.service.ts
new file mode 100644 (file)
index 0000000..13f1f6c
--- /dev/null
@@ -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))
+  }
+}
index c8e1e73b03f97116770b6d63ed9583fdc8feca55..8f1475a4d0d848161d9886dae57a583e4a723618 100644 (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
index b8c19d8d608774f348529413dc07003ec96138a5..917f316519a564ae828015b7f39f4db79dd16fca 100644 (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)
     )
   }
 }
index d138d2ba7234fa89dbc45a834df6c2e8ad16eb73..94f82e352a30488c1b3478339b5161c2f2856730 100644 (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>
 
diff --git a/client/src/app/shared/forms/form-validators/custom-config.ts b/client/src/app/shared/forms/form-validators/custom-config.ts
new file mode 100644 (file)
index 0000000..17ae0e7
--- /dev/null
@@ -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.'
+  }
+}
index 35d3ad50f2579704b053cb18e435287c99f8c615..2ceb7124450bdb29a49efd5eb0b86b4eb501a350 100755 (executable)
@@ -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
index 2864232a4b3ee5800e53c5157ea9c4cdbf8f5a62..ec76bb846cac5df12899bfd08ef7c3f509ab222f 100755 (executable)
@@ -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
 
index 35c89835b3d613f80f08f330c50012f2ab9411e1..f0b2c3d79fd412242076cce04283dce660d6dfd0 100644 (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' ]
+      }
+    }
+  }
+}
index b61d6e3fa034baab0ab3120ba0a92864d09ce206..79c3b58581e246817a96574ebe8be58e77dfb1cc 100644 (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)
     }
   }
index 759880201045ecfbbe4386a7f6824f44dc421e2e..7b63a9ccd20edefe55134911c5f69c3092e826bb 100644 (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()
+}
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
new file mode 100644 (file)
index 0000000..800aaf1
--- /dev/null
@@ -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
+}
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
new file mode 100644 (file)
index 0000000..59a0c30
--- /dev/null
@@ -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()
+    }
+  })
+})
index e8846c8db70636c4d8e39a8d498e36d5c37bec64..8c1389e7f5c7d953bd342ec4bd68a618a4fed75f 100644 (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)
 
index 0a0c95750af5694e4598dd48623b16e7c5178242..ca20f39a096b82a01f48c67ed9bb42f952e3442f 100644 (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'
 
index 2c51d1f0a248b68c65943af181e790dcd0ee05e2..2aac37791ac5d7673ca4cbf382187ffdccf147b2 100644 (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,
index eb02cf9e63fe06164b9b8b8a85e798d7caf31389..840072430997efed6a0641eb1e9a278d744b0aef 100644 (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
 }) {
index d09c19c6019f726570de9b44ff7fc20641b47d32..b6905757a14c7d10aa482ad527497e90aecfc1d3 100644 (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
 }
index dc132721550c55fa0c508971d54b5018e5a23512..095d4e29dab2ad39548d427abf8fc61e88d64eba 100644 (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 {
diff --git a/shared/models/config/custom-config.model.ts b/shared/models/config/custom-config.model.ts
new file mode 100644 (file)
index 0000000..73b5b6a
--- /dev/null
@@ -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
+    }
+  }
+}
diff --git a/shared/models/config/server-config.model.ts b/shared/models/config/server-config.model.ts
new file mode 100644 (file)
index 0000000..d0b2e40
--- /dev/null
@@ -0,0 +1,21 @@
+export interface ServerConfig {
+  signup: {
+    allowed: boolean
+  }
+  transcoding: {
+    enabledResolutions: number[]
+  }
+  avatar: {
+    file: {
+      size: {
+        max: number
+      },
+      extensions: string[]
+    }
+  }
+  video: {
+    file: {
+      extensions: string[]
+    }
+  }
+}
index a88c0160872f18479daeece3e66b3be01d5e39d1..1b877774c58949bf81f44628e8f4dce29b239e2c 100644 (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'
diff --git a/shared/models/server-config.model.ts b/shared/models/server-config.model.ts
deleted file mode 100644 (file)
index d0b2e40..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-export interface ServerConfig {
-  signup: {
-    allowed: boolean
-  }
-  transcoding: {
-    enabledResolutions: number[]
-  }
-  avatar: {
-    file: {
-      size: {
-        max: number
-      },
-      extensions: string[]
-    }
-  }
-  video: {
-    file: {
-      extensions: string[]
-    }
-  }
-}
index 2e7fa1bcf939df4eff5f45b8f04b239c3d632807..1fa149999d2f5a52d67476e30a96cbcf1886a413 100644 (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
index 69af57c0e91664e2ccbbbe201fcee74fd7d3425b..d427489c71982c8dae321b7399b2bbc17a83936d 100644 (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 && \