Begin user quota
authorChocobozzz <florian.bigard@gmail.com>
Mon, 4 Sep 2017 18:07:54 +0000 (20:07 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 4 Sep 2017 18:07:54 +0000 (20:07 +0200)
29 files changed:
client/package.json
client/src/app/+admin/users/shared/user.service.ts
client/src/app/+admin/users/user-add/user-add.component.html
client/src/app/+admin/users/user-add/user-add.component.ts
client/src/app/+admin/users/user-list/user-list.component.ts
client/src/app/shared/forms/form-validators/user.ts
client/src/app/shared/rest/rest-data-source.ts
client/src/app/shared/users/user.model.ts
client/tslint.json
client/yarn.lock
config/default.yaml
config/production.yaml.example
package.json
server/controllers/api/users.ts
server/helpers/custom-validators/users.ts
server/initializers/constants.ts
server/initializers/database.ts
server/initializers/installer.ts
server/initializers/migrations/0070-user-video-quota.ts [new file with mode: 0644]
server/middlewares/validators/users.ts
server/middlewares/validators/videos.ts
server/models/user/user-interface.ts
server/models/user/user.ts
server/models/video/video.ts
shared/models/users/user-create.model.ts
shared/models/users/user-update.model.ts
shared/models/users/user.model.ts
tslint.json
yarn.lock

index 27246027b7917a3c9f89e4dbadd337ba18eeb092..f1c7e87998fc5c1cfdc3a2753e0981b7213c1fba 100644 (file)
@@ -80,9 +80,9 @@
     "string-replace-loader": "^1.0.3",
     "style-loader": "^0.18.2",
     "tslib": "^1.5.0",
-    "tslint": "^5.4.3",
+    "tslint": "^5.7.0",
     "tslint-loader": "^3.3.0",
-    "typescript": "~2.4.0",
+    "typescript": "^2.5.2",
     "url-loader": "^0.5.7",
     "video.js": "^6.2.0",
     "videojs-dock": "^2.0.2",
index 1c1cd575edf312d5cfb53c357424f3cf8b8c0d9e..ffd7ba7da299e2f30b6673cd965582d187c798ac 100644 (file)
@@ -2,12 +2,15 @@ import { Injectable } from '@angular/core'
 import 'rxjs/add/operator/catch'
 import 'rxjs/add/operator/map'
 
+import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
+
 import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared'
 import { UserCreate } from '../../../../../../shared'
 
 @Injectable()
 export class UserService {
   private static BASE_USERS_URL = API_URL + '/api/v1/users/'
+  private bytesPipe = new BytesPipe()
 
   constructor (
     private authHttp: AuthHttp,
@@ -21,10 +24,30 @@ export class UserService {
   }
 
   getDataSource () {
-    return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL)
+    return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL, this.formatDataSource.bind(this))
   }
 
   removeUser (user: User) {
     return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
   }
+
+  private formatDataSource (users: User[]) {
+    const newUsers = []
+
+    users.forEach(user => {
+      let videoQuota
+      if (user.videoQuota === -1) {
+        videoQuota = 'Unlimited'
+      } else {
+        videoQuota = this.bytesPipe.transform(user.videoQuota)
+      }
+
+      const newUser = Object.assign(user, {
+        videoQuota
+      })
+      newUsers.push(newUser)
+    })
+
+    return newUsers
+  }
 }
index 9b487aa75fdab79bbac15b076843b1a848468573..f84d72c7c882c6e2e725f96903ec472363563f4f 100644 (file)
@@ -9,7 +9,7 @@
       <div class="form-group">
         <label for="username">Username</label>
         <input
-          type="text" class="form-control" id="username" placeholder="Username"
+          type="text" class="form-control" id="username" placeholder="john"
           formControlName="username"
         >
         <div *ngIf="formErrors.username" class="alert alert-danger">
@@ -20,7 +20,7 @@
       <div class="form-group">
         <label for="email">Email</label>
         <input
-          type="text" class="form-control" id="email" placeholder="Email"
+          type="text" class="form-control" id="email" placeholder="mail@example.com"
           formControlName="email"
         >
         <div *ngIf="formErrors.email" class="alert alert-danger">
@@ -31,7 +31,7 @@
       <div class="form-group">
         <label for="password">Password</label>
         <input
-          type="password" class="form-control" id="password" placeholder="Password"
+          type="password" class="form-control" id="password"
           formControlName="password"
         >
         <div *ngIf="formErrors.password" class="alert alert-danger">
         </div>
       </div>
 
+      <div class="form-group">
+        <label for="videoQuota">Video quota</label>
+        <select class="form-control" id="videoQuota" formControlName="videoQuota">
+          <option value="-1">Unlimited</option>
+          <option value="100000000">100MB</option>
+          <option value="500000000">500MB</option>
+          <option value="1000000000">1GB</option>
+          <option value="5000000000">5GB</option>
+          <option value="20000000000">20GB</option>
+          <option value="50000000000">50GB</option>
+        </select>
+      </div>
+
       <input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
     </form>
   </div>
index 0dd99eccd8aa81b25a9dacc6914af4bb1395a851..91377a933878c2d737fa4a158d0f2f82a8a022e0 100644 (file)
@@ -9,7 +9,8 @@ import {
   FormReactive,
   USER_USERNAME,
   USER_EMAIL,
-  USER_PASSWORD
+  USER_PASSWORD,
+  USER_VIDEO_QUOTA
 } from '../../../shared'
 import { UserCreate } from '../../../../../../shared'
 
@@ -24,12 +25,14 @@ export class UserAddComponent extends FormReactive implements OnInit {
   formErrors = {
     'username': '',
     'email': '',
-    'password': ''
+    'password': '',
+    'videoQuota': ''
   }
   validationMessages = {
     'username': USER_USERNAME.MESSAGES,
     'email': USER_EMAIL.MESSAGES,
-    'password': USER_PASSWORD.MESSAGES
+    'password': USER_PASSWORD.MESSAGES,
+    'videoQuota': USER_VIDEO_QUOTA.MESSAGES
   }
 
   constructor (
@@ -45,7 +48,8 @@ export class UserAddComponent extends FormReactive implements OnInit {
     this.form = this.formBuilder.group({
       username: [ '', USER_USERNAME.VALIDATORS ],
       email:    [ '', USER_EMAIL.VALIDATORS ],
-      password: [ '', USER_PASSWORD.VALIDATORS ]
+      password: [ '', USER_PASSWORD.VALIDATORS ],
+      videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ]
     })
 
     this.form.valueChanges.subscribe(data => this.onValueChanged(data))
@@ -60,6 +64,9 @@ export class UserAddComponent extends FormReactive implements OnInit {
 
     const userCreate: UserCreate = this.form.value
 
+    // A select in HTML is always mapped as a string, we convert it to number
+    userCreate.videoQuota = parseInt(this.form.value['videoQuota'], 10)
+
     this.userService.addUser(userCreate).subscribe(
       () => {
         this.notificationsService.success('Success', `User ${userCreate.username} created.`)
index 12826741c027969077442c7d5e0def949f45cf60..dbb85cedd7ac28bb1e652c5ecda4870ae36da307 100644 (file)
@@ -30,7 +30,7 @@ export class UserListComponent {
     },
     pager: {
       display: true,
-      perPage: 10
+      perPage: 1
     },
     columns: {
       id: {
@@ -43,6 +43,9 @@ export class UserListComponent {
       email: {
         title: 'Email'
       },
+      videoQuota: {
+        title: 'Video quota'
+      },
       role: {
         title: 'Role',
         sort: false
index fd316583e98b9a80795e4cc4b534b990b853f161..087a9976092ca06ffd596e0fed4438f507806bb5 100644 (file)
@@ -22,3 +22,10 @@ export const USER_PASSWORD = {
     'minlength': 'Password must be at least 6 characters long.'
   }
 }
+export const USER_VIDEO_QUOTA = {
+  VALIDATORS: [ Validators.required, Validators.min(-1) ],
+  MESSAGES: {
+    'required': 'Video quota is required.',
+    'min': 'Quota must be greater than -1.'
+  }
+}
\ No newline at end of file
index 7956637e0c36d4686b6e202c95f45c64ed40795d..5c205d280ce1ecaad453817a09f1d652ffacac44 100644 (file)
@@ -3,14 +3,31 @@ import { Http, RequestOptionsArgs, URLSearchParams, Response } from '@angular/ht
 import { ServerDataSource } from 'ng2-smart-table'
 
 export class RestDataSource extends ServerDataSource {
-  constructor (http: Http, endpoint: string) {
+  private updateResponse: (input: any[]) => any[]
+
+  constructor (http: Http, endpoint: string, updateResponse?: (input: any[]) => any[]) {
     const options = {
       endPoint: endpoint,
       sortFieldKey: 'sort',
       dataKey: 'data'
     }
-
     super(http, options)
+
+    if (updateResponse) {
+      this.updateResponse = updateResponse
+    }
+  }
+
+  protected extractDataFromResponse (res: Response) {
+    const json = res.json()
+    if (!json) return []
+    let data = json.data
+
+    if (this.updateResponse !== undefined) {
+      data = this.updateResponse(data)
+    }
+
+    return data
   }
 
   protected extractTotalFromResponse (res: Response) {
index 1c2b481e34d41bb695ca81d72b919a1b9ecc0b39..bf12876c70c7cd6cf2f7139f370a7d855937c1fe 100644 (file)
@@ -6,6 +6,7 @@ export class User implements UserServerModel {
   email: string
   role: UserRole
   displayNSFW: boolean
+  videoQuota: number
   createdAt: Date
 
   constructor (hash: {
@@ -13,6 +14,7 @@ export class User implements UserServerModel {
     username: string,
     email: string,
     role: UserRole,
+    videoQuota?: number,
     displayNSFW?: boolean,
     createdAt?: Date
   }) {
@@ -20,9 +22,16 @@ export class User implements UserServerModel {
     this.username = hash.username
     this.email = hash.email
     this.role = hash.role
-    this.displayNSFW = hash.displayNSFW
 
-    if (hash.createdAt) {
+    if (hash.videoQuota !== undefined) {
+      this.videoQuota = hash.videoQuota
+    }
+
+    if (hash.displayNSFW !== undefined) {
+      this.displayNSFW = hash.displayNSFW
+    }
+
+    if (hash.createdAt !== undefined) {
       this.createdAt = hash.createdAt
     }
   }
index cfad2a5d94184e9bf2859fdbe0bca620d3c84939..b1e211ee9cb6d030453c7f96ac9af6ffff4a75da 100644 (file)
@@ -4,7 +4,6 @@
   "rules": {
     "no-inferrable-types": true,
     "eofline": true,
-    "indent": ["spaces"],
     "max-line-length": [true, 140],
     "no-floating-promises": false,
     "no-unused-variable": false, // Bug, wait TypeScript 2.4
index 0fc5ec418032402f0c9d09c7033fa86be85782c8..9478e23b2eac9acf3f0a01d2c28b0f8708d8d522 100644 (file)
@@ -6740,9 +6740,9 @@ tslint-loader@^3.3.0:
     rimraf "^2.4.4"
     semver "^5.3.0"
 
-tslint@^5.4.3:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.6.0.tgz#088aa6c6026623338650b2900828ab3edf59f6cf"
+tslint@^5.7.0:
+  version "5.7.0"
+  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552"
   dependencies:
     babel-code-frame "^6.22.0"
     colors "^1.1.2"
@@ -6753,7 +6753,7 @@ tslint@^5.4.3:
     resolve "^1.3.2"
     semver "^5.3.0"
     tslib "^1.7.1"
-    tsutils "^2.7.1"
+    tsutils "^2.8.1"
 
 tsml@1.0.1:
   version "1.0.1"
@@ -6763,9 +6763,9 @@ tsutils@^1.4.0:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
 
-tsutils@^2.7.1:
-  version "2.8.1"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.1.tgz#3771404e7ca9f0bedf5d919a47a4b1890a68efff"
+tsutils@^2.8.1:
+  version "2.8.2"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a"
   dependencies:
     tslib "^1.7.1"
 
@@ -6806,9 +6806,9 @@ typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
-typescript@~2.4.0:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844"
+typescript@^2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
 
 uglify-js@3.0.x, uglify-js@^3.0.6:
   version "3.0.28"
index a97d3ff78e7bab3d0007f3b297d679a2463614e5..4c19a5b2de3045db43ea52cf4845ee343b16ec77 100644 (file)
@@ -35,6 +35,11 @@ signup:
   enabled: false
   limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
 
+user:
+  # Default value of maximum video BYTES the user can upload (does not take into account transcoded files).
+  # -1 == unlimited
+  video_quota: -1
+
 # If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
 # Uses a lot of CPU!
 transcoding:
index 90e07f5776c74c670e7df978391412d08e6a0555..987da12cce519d3c5226e5b0e17d749525ba3876 100644 (file)
@@ -36,6 +36,11 @@ signup:
   enabled: false
   limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
 
+user:
+  # Default value of maximum video BYTES the user can upload (does not take into account transcoded files).
+  # -1 == unlimited
+  video_quota: -1
+
 # If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
 # Uses a lot of CPU!
 transcoding:
index 2a1b0bde3f61708cd1de610ef6ed10590a5bd2be..900d040529dc86325a826b1c76b5aefc40108c17 100644 (file)
@@ -79,7 +79,7 @@
     "scripty": "^1.5.0",
     "sequelize": "^4.7.5",
     "ts-node": "^3.0.6",
-    "typescript": "^2.4.1",
+    "typescript": "^2.5.2",
     "validator": "^8.1.0",
     "winston": "^2.1.1",
     "ws": "^3.1.0"
     "source-map-support": "^0.4.15",
     "standard": "^10.0.0",
     "supertest": "^3.0.0",
-    "tslint": "^5.2.0",
+    "tslint": "^5.7.0",
     "tslint-config-standard": "^6.0.0",
     "webtorrent": "^0.98.0"
   },
index 04d8851855774361f562c5365f683478844a60a6..1b5b7f9031db14cf9bdb98f783f6ffba7e652aea 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 
 import { database as db } from '../../initializers/database'
-import { USER_ROLES } from '../../initializers'
+import { USER_ROLES, CONFIG } from '../../initializers'
 import { logger, getFormattedObjects } from '../../helpers'
 import {
   authenticate,
@@ -80,12 +80,18 @@ export {
 function createUser (req: express.Request, res: express.Response, next: express.NextFunction) {
   const body: UserCreate = req.body
 
+  // On registration, we set the user video quota
+  if (body.videoQuota === undefined) {
+    body.videoQuota = CONFIG.USER.VIDEO_QUOTA
+  }
+
   const user = db.User.build({
     username: body.username,
     password: body.password,
     email: body.email,
     displayNSFW: false,
-    role: USER_ROLES.USER
+    role: USER_ROLES.USER,
+    videoQuota: body.videoQuota
   })
 
   user.save()
@@ -140,6 +146,7 @@ function updateUser (req: express.Request, res: express.Response, next: express.
     .then(user => {
       if (body.password) user.password = body.password
       if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW
+      if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
 
       return user.save()
     })
index 2b37bdde8e1fa9e106a5c4b203738183e96ef5d8..00061f9df82d4a52a45d134d6b98180ff42f9958 100644 (file)
@@ -15,6 +15,10 @@ function isUserRoleValid (value: string) {
   return values(USER_ROLES).indexOf(value as UserRole) !== -1
 }
 
+function isUserVideoQuotaValid (value: string) {
+  return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
+}
+
 function isUserUsernameValid (value: string) {
   const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max
   const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min
@@ -30,6 +34,7 @@ function isUserDisplayNSFWValid (value: any) {
 export {
   isUserPasswordValid,
   isUserRoleValid,
+  isUserVideoQuotaValid,
   isUserUsernameValid,
   isUserDisplayNSFWValid
 }
@@ -39,6 +44,7 @@ declare module 'express-validator' {
     isUserPasswordValid,
     isUserRoleValid,
     isUserUsernameValid,
-    isUserDisplayNSFWValid
+    isUserDisplayNSFWValid,
+    isUserVideoQuotaValid
   }
 }
index 50a939083c7db721ddad6ee2032b849fed4f0cb6..b93a858598c2a916cde2321f0931cee59045191d 100644 (file)
@@ -15,7 +15,7 @@ import {
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 65
+const LAST_MIGRATION_VERSION = 70
 
 // ---------------------------------------------------------------------------
 
@@ -77,7 +77,10 @@ const CONFIG = {
   },
   SIGNUP: {
     ENABLED: config.get<boolean>('signup.enabled'),
-    LIMIT: config.get<number>('signup.limit')
+    LIMIT: config.get<number>('signup.limit'),
+  },
+  USER: {
+    VIDEO_QUOTA: config.get<number>('user.video_quota')
   },
   TRANSCODING: {
     ENABLED: config.get<boolean>('transcoding.enabled'),
@@ -97,7 +100,8 @@ CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
 const CONSTRAINTS_FIELDS = {
   USERS: {
     USERNAME: { min: 3, max: 20 }, // Length
-    PASSWORD: { min: 6, max: 255 } // Length
+    PASSWORD: { min: 6, max: 255 }, // Length
+    VIDEO_QUOTA: { min: -1 }
   },
   VIDEO_ABUSES: {
     REASON: { min: 2, max: 300 } // Length
index c0df2b63a15d50c52c3f1e68aa8299243c2ec155..d04c8db1b40beca01481bccf1d814ec26d42fb3b 100644 (file)
@@ -1,5 +1,6 @@
 import { join } from 'path'
 import { flattenDepth } from 'lodash'
+require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
 import * as Sequelize from 'sequelize'
 import * as Promise from 'bluebird'
 
index 43b5adfedf40cdb99d64fda89ee886eaa12e611a..10b74b85fd50cb8b2334f52716d4e4fc2522b21d 100644 (file)
@@ -38,12 +38,12 @@ function removeCacheDirectories () {
 }
 
 function createDirectoriesIfNotExist () {
-  const storages = CONFIG.STORAGE
+  const storage = CONFIG.STORAGE
   const cacheDirectories = CACHE.DIRECTORIES
 
   const tasks = []
-  Object.keys(storages).forEach(key => {
-    const dir = storages[key]
+  Object.keys(storage).forEach(key => {
+    const dir = storage[key]
     tasks.push(mkdirpPromise(dir))
   })
 
@@ -112,7 +112,8 @@ function createOAuthAdminIfNotExist () {
       username,
       email,
       password,
-      role
+      role,
+      videoQuota: -1
     }
 
     return db.User.create(userData, createOptions).then(createdUser => {
diff --git a/server/initializers/migrations/0070-user-video-quota.ts b/server/initializers/migrations/0070-user-video-quota.ts
new file mode 100644 (file)
index 0000000..dec4d46
--- /dev/null
@@ -0,0 +1,32 @@
+import * as Sequelize from 'sequelize'
+import * as Promise from 'bluebird'
+
+function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: any
+}): Promise<void> {
+  const q = utils.queryInterface
+
+  const data = {
+    type: Sequelize.BIGINT,
+    allowNull: false,
+    defaultValue: -1
+  }
+
+  return q.addColumn('Users', 'videoQuota', data)
+    .then(() => {
+      data.defaultValue = null
+      return q.changeColumn('Users', 'videoQuota', data)
+    })
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index 71e529872087cb006c901b904975eafc955d5ab1..eeb0e3557571744acae76b70dd489ef91c8b809e 100644 (file)
@@ -12,6 +12,7 @@ function usersAddValidator (req: express.Request, res: express.Response, next: e
   req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
   req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
   req.checkBody('email', 'Should have a valid email').isEmail()
+  req.checkBody('videoQuota', 'Should have a valid user quota').isUserVideoQuotaValid()
 
   logger.debug('Checking usersAdd parameters', { parameters: req.body })
 
@@ -55,6 +56,7 @@ function usersUpdateValidator (req: express.Request, res: express.Response, next
   // Add old password verification
   req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid()
   req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid()
+  req.checkBody('videoQuota', 'Should have a valid user quota').optional().isUserVideoQuotaValid()
 
   logger.debug('Checking usersUpdate parameters', { parameters: req.body })
 
index 29c1ee0ef6fcae3adc6d1be528d208788fdf8b6c..1d19ebfd91681d1650cc04c842bfa4660e5ac7df 100644 (file)
@@ -24,10 +24,23 @@ function videosAddValidator (req: express.Request, res: express.Response, next:
   logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
 
   checkErrors(req, res, () => {
-    const videoFile = req.files['videofile'][0]
+    const videoFile: Express.Multer.File = req.files['videofile'][0]
+    const user = res.locals.oauth.token.User
 
-    db.Video.getDurationFromFile(videoFile.path)
+    user.isAbleToUploadVideo(videoFile)
+      .then(isAble => {
+        if (isAble === false) {
+          res.status(403).send('The user video quota is exceeded with this video.')
+
+          return undefined
+        }
+
+        return db.Video.getDurationFromFile(videoFile.path)
+      })
       .then(duration => {
+        // Previous test failed, abort
+        if (duration === undefined) return
+
         if (!isVideoDurationValid('' + duration)) {
           return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
         }
index 0b97a8f6d1e86fdb6e74416a903ae56a113075df..8974a9a97f32b6f82493aa72c1cc193cb227fdfe 100644 (file)
@@ -11,6 +11,7 @@ export namespace UserMethods {
 
   export type ToFormattedJSON = (this: UserInstance) => FormattedUser
   export type IsAdmin = (this: UserInstance) => boolean
+  export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
 
   export type CountTotal = () => Promise<number>
 
@@ -31,6 +32,7 @@ export interface UserClass {
   isPasswordMatch: UserMethods.IsPasswordMatch,
   toFormattedJSON: UserMethods.ToFormattedJSON,
   isAdmin: UserMethods.IsAdmin,
+  isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
 
   countTotal: UserMethods.CountTotal,
   getByUsername: UserMethods.GetByUsername,
@@ -42,11 +44,13 @@ export interface UserClass {
 }
 
 export interface UserAttributes {
+  id?: number
   password: string
   username: string
   email: string
   displayNSFW?: boolean
   role: UserRole
+  videoQuota: number
 }
 
 export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
index d481fa13cbb48138d22234a3342392098e63f502..12a7547f5d3cf953b1dc37c69e01e26ee0ebb74b 100644 (file)
@@ -1,5 +1,6 @@
 import { values } from 'lodash'
 import * as Sequelize from 'sequelize'
+import * as Promise from 'bluebird'
 
 import { getSort } from '../utils'
 import { USER_ROLES } from '../../initializers'
@@ -8,7 +9,8 @@ import {
   comparePassword,
   isUserPasswordValid,
   isUserUsernameValid,
-  isUserDisplayNSFWValid
+  isUserDisplayNSFWValid,
+  isUserVideoQuotaValid
 } from '../../helpers'
 
 import { addMethodsToModel } from '../utils'
@@ -30,6 +32,7 @@ let listForApi: UserMethods.ListForApi
 let loadById: UserMethods.LoadById
 let loadByUsername: UserMethods.LoadByUsername
 let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
+let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
 
 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
   User = sequelize.define<UserInstance, UserAttributes>('User',
@@ -75,6 +78,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
       role: {
         type: DataTypes.ENUM(values(USER_ROLES)),
         allowNull: false
+      },
+      videoQuota: {
+        type: DataTypes.BIGINT,
+        allowNull: false,
+        validate: {
+          videoQuotaValid: value => {
+            const res = isUserVideoQuotaValid(value)
+            if (res === false) throw new Error('Video quota is not valid.')
+          }
+        }
       }
     },
     {
@@ -109,7 +122,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
   const instanceMethods = [
     isPasswordMatch,
     toFormattedJSON,
-    isAdmin
+    isAdmin,
+    isAbleToUploadVideo
   ]
   addMethodsToModel(User, classMethods, instanceMethods)
 
@@ -136,6 +150,7 @@ toFormattedJSON = function (this: UserInstance) {
     email: this.email,
     displayNSFW: this.displayNSFW,
     role: this.role,
+    videoQuota: this.videoQuota,
     createdAt: this.createdAt
   }
 }
@@ -144,6 +159,14 @@ isAdmin = function (this: UserInstance) {
   return this.role === USER_ROLES.ADMIN
 }
 
+isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
+  if (this.videoQuota === -1) return Promise.resolve(true)
+
+  return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
+    return (videoFile.size + totalBytes) < this.videoQuota
+  })
+}
+
 // ------------------------------ STATICS ------------------------------
 
 function associate (models) {
@@ -215,3 +238,36 @@ loadByUsernameOrEmail = function (username: string, email: string) {
   // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
   return (User as any).findOne(query)
 }
+
+// ---------------------------------------------------------------------------
+
+function getOriginalVideoFileTotalFromUser (user: UserInstance) {
+  const query = {
+    attributes: [
+      Sequelize.fn('COUNT', Sequelize.col('VideoFile.size'), 'totalVideoBytes')
+    ],
+    where: {
+      id: user.id
+    },
+    include: [
+      {
+        model: User['sequelize'].models.Author,
+        include: [
+          {
+            model: User['sequelize'].models.Video,
+            include: [
+              {
+                model: User['sequelize'].models.VideoFile
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+
+  // FIXME: cast to any because of bad typing...
+  return User.findAll(query).then((res: any) => {
+    return res.totalVideoBytes
+  })
+}
index 7dfea8ac955de50bf3e2f523157ad7572f31530c..4fb4485d8ada89799b744f68fc104b2869f39cd8 100644 (file)
@@ -9,6 +9,7 @@ import * as Sequelize from 'sequelize'
 import * as Promise from 'bluebird'
 
 import { TagInstance } from './tag-interface'
+import { UserInstance } from '../user/user-interface'
 import {
   logger,
   isVideoNameValid,
@@ -582,7 +583,7 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns
             return res()
           })
           .catch(err => {
-            // Autodestruction...
+            // Auto destruction...
             this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
 
             return rej(err)
@@ -608,8 +609,8 @@ removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
 }
 
 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
-  const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
-  return unlinkPromise(torrenPath)
+  const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
+  return unlinkPromise(torrentPath)
 }
 
 // ------------------------------ STATICS ------------------------------
index 2cddcdcb0343d2742b655be77dffb87cecdd816d..49fa2549d5353ae3cf6f88efbc87d3a1f16a842a 100644 (file)
@@ -2,4 +2,5 @@ export interface UserCreate {
   username: string
   password: string
   email: string
+  videoQuota: number
 }
index 8b9abfb15ec7cf09a73eb0adfc270f0fede2db1c..895ec0681a66a106c19ea3d13fdca8ac2177a06a 100644 (file)
@@ -1,4 +1,5 @@
 export interface UserUpdate {
   displayNSFW?: boolean
   password?: string
+  videoQuota?: number
 }
index 5c48a17b28c48ad373f22e16c901bad56a3d90d3..867a6dde5bad19bcdd96b65f8ab53889c0a4097d 100644 (file)
@@ -6,5 +6,6 @@ export interface User {
   email: string
   displayNSFW: boolean
   role: UserRole
+  videoQuota: number
   createdAt: Date
 }
index 70e5d9bb4103f67ae7d18b4b3f1f7cc641f3855d..6e982ca85d47121cdfae366389a6502238b28d21 100644 (file)
@@ -4,6 +4,7 @@
     "no-inferrable-types": true,
     "eofline": true,
     "indent": ["spaces"],
+    "ter-indent": [true, 2],
     "max-line-length": [true, 140],
     "no-floating-promises": false
   }
index c0f35b21deefca21c0726fa4d92c1ac6fd4ee3fe..1a6af175a21965f75a9839f06af85762f3d042f9 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -3755,9 +3755,9 @@ tslint-eslint-rules@^4.0.0:
     tslib "^1.0.0"
     tsutils "^1.4.0"
 
-tslint@^5.2.0:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.6.0.tgz#088aa6c6026623338650b2900828ab3edf59f6cf"
+tslint@^5.7.0:
+  version "5.7.0"
+  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552"
   dependencies:
     babel-code-frame "^6.22.0"
     colors "^1.1.2"
@@ -3768,15 +3768,15 @@ tslint@^5.2.0:
     resolve "^1.3.2"
     semver "^5.3.0"
     tslib "^1.7.1"
-    tsutils "^2.7.1"
+    tsutils "^2.8.1"
 
 tsutils@^1.4.0:
   version "1.9.1"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
 
-tsutils@^2.7.1:
-  version "2.8.1"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.1.tgz#3771404e7ca9f0bedf5d919a47a4b1890a68efff"
+tsutils@^2.8.1:
+  version "2.8.2"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a"
   dependencies:
     tslib "^1.7.1"
 
@@ -3821,9 +3821,9 @@ typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
-typescript@^2.4.1:
-  version "2.5.1"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.1.tgz#ce7cc93ada3de19475cc9d17e3adea7aee1832aa"
+typescript@^2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
 
 uid-number@^0.0.6:
   version "0.0.6"