From b0f9f39ed70299a208d1b388c72de8b7f3510cb7 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 4 Sep 2017 20:07:54 +0200 Subject: [PATCH] Begin user quota --- client/package.json | 4 +- .../app/+admin/users/shared/user.service.ts | 25 +++++++- .../users/user-add/user-add.component.html | 19 +++++- .../users/user-add/user-add.component.ts | 15 +++-- .../users/user-list/user-list.component.ts | 5 +- .../app/shared/forms/form-validators/user.ts | 7 +++ .../src/app/shared/rest/rest-data-source.ts | 21 ++++++- client/src/app/shared/users/user.model.ts | 13 +++- client/tslint.json | 1 - client/yarn.lock | 20 +++---- config/default.yaml | 5 ++ config/production.yaml.example | 5 ++ package.json | 4 +- server/controllers/api/users.ts | 11 +++- server/helpers/custom-validators/users.ts | 8 ++- server/initializers/constants.ts | 10 +++- server/initializers/database.ts | 1 + server/initializers/installer.ts | 9 +-- .../migrations/0070-user-video-quota.ts | 32 ++++++++++ server/middlewares/validators/users.ts | 2 + server/middlewares/validators/videos.ts | 17 +++++- server/models/user/user-interface.ts | 4 ++ server/models/user/user.ts | 60 ++++++++++++++++++- server/models/video/video.ts | 7 ++- shared/models/users/user-create.model.ts | 1 + shared/models/users/user-update.model.ts | 1 + shared/models/users/user.model.ts | 1 + tslint.json | 1 + yarn.lock | 20 +++---- 29 files changed, 274 insertions(+), 55 deletions(-) create mode 100644 server/initializers/migrations/0070-user-video-quota.ts diff --git a/client/package.json b/client/package.json index 27246027b..f1c7e8799 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/app/+admin/users/shared/user.service.ts b/client/src/app/+admin/users/shared/user.service.ts index 1c1cd575e..ffd7ba7da 100644 --- a/client/src/app/+admin/users/shared/user.service.ts +++ b/client/src/app/+admin/users/shared/user.service.ts @@ -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 + } } diff --git a/client/src/app/+admin/users/user-add/user-add.component.html b/client/src/app/+admin/users/user-add/user-add.component.html index 9b487aa75..f84d72c7c 100644 --- a/client/src/app/+admin/users/user-add/user-add.component.html +++ b/client/src/app/+admin/users/user-add/user-add.component.html @@ -9,7 +9,7 @@
@@ -20,7 +20,7 @@
@@ -31,7 +31,7 @@
@@ -39,6 +39,19 @@
+
+ + +
+
diff --git a/client/src/app/+admin/users/user-add/user-add.component.ts b/client/src/app/+admin/users/user-add/user-add.component.ts index 0dd99eccd..91377a933 100644 --- a/client/src/app/+admin/users/user-add/user-add.component.ts +++ b/client/src/app/+admin/users/user-add/user-add.component.ts @@ -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.`) diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index 12826741c..dbb85cedd 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts @@ -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 diff --git a/client/src/app/shared/forms/form-validators/user.ts b/client/src/app/shared/forms/form-validators/user.ts index fd316583e..087a99760 100644 --- a/client/src/app/shared/forms/form-validators/user.ts +++ b/client/src/app/shared/forms/form-validators/user.ts @@ -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 diff --git a/client/src/app/shared/rest/rest-data-source.ts b/client/src/app/shared/rest/rest-data-source.ts index 7956637e0..5c205d280 100644 --- a/client/src/app/shared/rest/rest-data-source.ts +++ b/client/src/app/shared/rest/rest-data-source.ts @@ -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) { diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 1c2b481e3..bf12876c7 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts @@ -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 } } diff --git a/client/tslint.json b/client/tslint.json index cfad2a5d9..b1e211ee9 100644 --- a/client/tslint.json +++ b/client/tslint.json @@ -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 diff --git a/client/yarn.lock b/client/yarn.lock index 0fc5ec418..9478e23b2 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -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" diff --git a/config/default.yaml b/config/default.yaml index a97d3ff78..4c19a5b2d 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -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: diff --git a/config/production.yaml.example b/config/production.yaml.example index 90e07f577..987da12cc 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -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: diff --git a/package.json b/package.json index 2a1b0bde3..900d04052 100644 --- a/package.json +++ b/package.json @@ -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" @@ -109,7 +109,7 @@ "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" }, diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 04d885185..1b5b7f903 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -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() }) diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index 2b37bdde8..00061f9df 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts @@ -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 } } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 50a939083..b93a85859 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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('signup.enabled'), - LIMIT: config.get('signup.limit') + LIMIT: config.get('signup.limit'), + }, + USER: { + VIDEO_QUOTA: config.get('user.video_quota') }, TRANSCODING: { ENABLED: config.get('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 diff --git a/server/initializers/database.ts b/server/initializers/database.ts index c0df2b63a..d04c8db1b 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -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' diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 43b5adfed..10b74b85f 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -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 index 000000000..dec4d46dd --- /dev/null +++ b/server/initializers/migrations/0070-user-video-quota.ts @@ -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 { + 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 +} diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 71e529872..eeb0e3557 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -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 }) diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index 29c1ee0ef..1d19ebfd9 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts @@ -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).') } diff --git a/server/models/user/user-interface.ts b/server/models/user/user-interface.ts index 0b97a8f6d..8974a9a97 100644 --- a/server/models/user/user-interface.ts +++ b/server/models/user/user-interface.ts @@ -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 export type CountTotal = () => Promise @@ -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 { diff --git a/server/models/user/user.ts b/server/models/user/user.ts index d481fa13c..12a7547f5 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts @@ -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('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 + }) +} diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 7dfea8ac9..4fb4485d8 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -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 ------------------------------ diff --git a/shared/models/users/user-create.model.ts b/shared/models/users/user-create.model.ts index 2cddcdcb0..49fa2549d 100644 --- a/shared/models/users/user-create.model.ts +++ b/shared/models/users/user-create.model.ts @@ -2,4 +2,5 @@ export interface UserCreate { username: string password: string email: string + videoQuota: number } diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts index 8b9abfb15..895ec0681 100644 --- a/shared/models/users/user-update.model.ts +++ b/shared/models/users/user-update.model.ts @@ -1,4 +1,5 @@ export interface UserUpdate { displayNSFW?: boolean password?: string + videoQuota?: number } diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 5c48a17b2..867a6dde5 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts @@ -6,5 +6,6 @@ export interface User { email: string displayNSFW: boolean role: UserRole + videoQuota: number createdAt: Date } diff --git a/tslint.json b/tslint.json index 70e5d9bb4..6e982ca85 100644 --- a/tslint.json +++ b/tslint.json @@ -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 } diff --git a/yarn.lock b/yarn.lock index c0f35b21d..1a6af175a 100644 --- 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" -- 2.41.0