1
0
Fork 0

Begin user quota

This commit is contained in:
Chocobozzz 2017-09-04 20:07:54 +02:00
parent e7dbeae8d9
commit b0f9f39ed7
29 changed files with 274 additions and 55 deletions

View file

@ -80,9 +80,9 @@
"string-replace-loader": "^1.0.3", "string-replace-loader": "^1.0.3",
"style-loader": "^0.18.2", "style-loader": "^0.18.2",
"tslib": "^1.5.0", "tslib": "^1.5.0",
"tslint": "^5.4.3", "tslint": "^5.7.0",
"tslint-loader": "^3.3.0", "tslint-loader": "^3.3.0",
"typescript": "~2.4.0", "typescript": "^2.5.2",
"url-loader": "^0.5.7", "url-loader": "^0.5.7",
"video.js": "^6.2.0", "video.js": "^6.2.0",
"videojs-dock": "^2.0.2", "videojs-dock": "^2.0.2",

View file

@ -2,12 +2,15 @@ import { Injectable } from '@angular/core'
import 'rxjs/add/operator/catch' import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared' import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared'
import { UserCreate } from '../../../../../../shared' import { UserCreate } from '../../../../../../shared'
@Injectable() @Injectable()
export class UserService { export class UserService {
private static BASE_USERS_URL = API_URL + '/api/v1/users/' private static BASE_USERS_URL = API_URL + '/api/v1/users/'
private bytesPipe = new BytesPipe()
constructor ( constructor (
private authHttp: AuthHttp, private authHttp: AuthHttp,
@ -21,10 +24,30 @@ export class UserService {
} }
getDataSource () { 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) { removeUser (user: User) {
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id) 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
}
} }

View file

@ -9,7 +9,7 @@
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
<input <input
type="text" class="form-control" id="username" placeholder="Username" type="text" class="form-control" id="username" placeholder="john"
formControlName="username" formControlName="username"
> >
<div *ngIf="formErrors.username" class="alert alert-danger"> <div *ngIf="formErrors.username" class="alert alert-danger">
@ -20,7 +20,7 @@
<div class="form-group"> <div class="form-group">
<label for="email">Email</label> <label for="email">Email</label>
<input <input
type="text" class="form-control" id="email" placeholder="Email" type="text" class="form-control" id="email" placeholder="mail@example.com"
formControlName="email" formControlName="email"
> >
<div *ngIf="formErrors.email" class="alert alert-danger"> <div *ngIf="formErrors.email" class="alert alert-danger">
@ -31,7 +31,7 @@
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>
<input <input
type="password" class="form-control" id="password" placeholder="Password" type="password" class="form-control" id="password"
formControlName="password" formControlName="password"
> >
<div *ngIf="formErrors.password" class="alert alert-danger"> <div *ngIf="formErrors.password" class="alert alert-danger">
@ -39,6 +39,19 @@
</div> </div>
</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"> <input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
</form> </form>
</div> </div>

View file

@ -9,7 +9,8 @@ import {
FormReactive, FormReactive,
USER_USERNAME, USER_USERNAME,
USER_EMAIL, USER_EMAIL,
USER_PASSWORD USER_PASSWORD,
USER_VIDEO_QUOTA
} from '../../../shared' } from '../../../shared'
import { UserCreate } from '../../../../../../shared' import { UserCreate } from '../../../../../../shared'
@ -24,12 +25,14 @@ export class UserAddComponent extends FormReactive implements OnInit {
formErrors = { formErrors = {
'username': '', 'username': '',
'email': '', 'email': '',
'password': '' 'password': '',
'videoQuota': ''
} }
validationMessages = { validationMessages = {
'username': USER_USERNAME.MESSAGES, 'username': USER_USERNAME.MESSAGES,
'email': USER_EMAIL.MESSAGES, 'email': USER_EMAIL.MESSAGES,
'password': USER_PASSWORD.MESSAGES 'password': USER_PASSWORD.MESSAGES,
'videoQuota': USER_VIDEO_QUOTA.MESSAGES
} }
constructor ( constructor (
@ -45,7 +48,8 @@ export class UserAddComponent extends FormReactive implements OnInit {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
username: [ '', USER_USERNAME.VALIDATORS ], username: [ '', USER_USERNAME.VALIDATORS ],
email: [ '', USER_EMAIL.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)) 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 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.userService.addUser(userCreate).subscribe(
() => { () => {
this.notificationsService.success('Success', `User ${userCreate.username} created.`) this.notificationsService.success('Success', `User ${userCreate.username} created.`)

View file

@ -30,7 +30,7 @@ export class UserListComponent {
}, },
pager: { pager: {
display: true, display: true,
perPage: 10 perPage: 1
}, },
columns: { columns: {
id: { id: {
@ -43,6 +43,9 @@ export class UserListComponent {
email: { email: {
title: 'Email' title: 'Email'
}, },
videoQuota: {
title: 'Video quota'
},
role: { role: {
title: 'Role', title: 'Role',
sort: false sort: false

View file

@ -22,3 +22,10 @@ export const USER_PASSWORD = {
'minlength': 'Password must be at least 6 characters long.' '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.'
}
}

View file

@ -3,14 +3,31 @@ import { Http, RequestOptionsArgs, URLSearchParams, Response } from '@angular/ht
import { ServerDataSource } from 'ng2-smart-table' import { ServerDataSource } from 'ng2-smart-table'
export class RestDataSource extends ServerDataSource { 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 = { const options = {
endPoint: endpoint, endPoint: endpoint,
sortFieldKey: 'sort', sortFieldKey: 'sort',
dataKey: 'data' dataKey: 'data'
} }
super(http, options) 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) { protected extractTotalFromResponse (res: Response) {

View file

@ -6,6 +6,7 @@ export class User implements UserServerModel {
email: string email: string
role: UserRole role: UserRole
displayNSFW: boolean displayNSFW: boolean
videoQuota: number
createdAt: Date createdAt: Date
constructor (hash: { constructor (hash: {
@ -13,6 +14,7 @@ export class User implements UserServerModel {
username: string, username: string,
email: string, email: string,
role: UserRole, role: UserRole,
videoQuota?: number,
displayNSFW?: boolean, displayNSFW?: boolean,
createdAt?: Date createdAt?: Date
}) { }) {
@ -20,9 +22,16 @@ export class User implements UserServerModel {
this.username = hash.username this.username = hash.username
this.email = hash.email this.email = hash.email
this.role = hash.role 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 this.createdAt = hash.createdAt
} }
} }

View file

@ -4,7 +4,6 @@
"rules": { "rules": {
"no-inferrable-types": true, "no-inferrable-types": true,
"eofline": true, "eofline": true,
"indent": ["spaces"],
"max-line-length": [true, 140], "max-line-length": [true, 140],
"no-floating-promises": false, "no-floating-promises": false,
"no-unused-variable": false, // Bug, wait TypeScript 2.4 "no-unused-variable": false, // Bug, wait TypeScript 2.4

View file

@ -6740,9 +6740,9 @@ tslint-loader@^3.3.0:
rimraf "^2.4.4" rimraf "^2.4.4"
semver "^5.3.0" semver "^5.3.0"
tslint@^5.4.3: tslint@^5.7.0:
version "5.6.0" version "5.7.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.6.0.tgz#088aa6c6026623338650b2900828ab3edf59f6cf" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552"
dependencies: dependencies:
babel-code-frame "^6.22.0" babel-code-frame "^6.22.0"
colors "^1.1.2" colors "^1.1.2"
@ -6753,7 +6753,7 @@ tslint@^5.4.3:
resolve "^1.3.2" resolve "^1.3.2"
semver "^5.3.0" semver "^5.3.0"
tslib "^1.7.1" tslib "^1.7.1"
tsutils "^2.7.1" tsutils "^2.8.1"
tsml@1.0.1: tsml@1.0.1:
version "1.0.1" version "1.0.1"
@ -6763,9 +6763,9 @@ tsutils@^1.4.0:
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
tsutils@^2.7.1: tsutils@^2.8.1:
version "2.8.1" version "2.8.2"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.1.tgz#3771404e7ca9f0bedf5d919a47a4b1890a68efff" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a"
dependencies: dependencies:
tslib "^1.7.1" tslib "^1.7.1"
@ -6806,9 +6806,9 @@ typedarray@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typescript@~2.4.0: typescript@^2.5.2:
version "2.4.2" version "2.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
uglify-js@3.0.x, uglify-js@^3.0.6: uglify-js@3.0.x, uglify-js@^3.0.6:
version "3.0.28" version "3.0.28"

View file

@ -35,6 +35,11 @@ signup:
enabled: false enabled: false
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited 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 # If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
# Uses a lot of CPU! # Uses a lot of CPU!
transcoding: transcoding:

View file

@ -36,6 +36,11 @@ signup:
enabled: false enabled: false
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited 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 # If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
# Uses a lot of CPU! # Uses a lot of CPU!
transcoding: transcoding:

View file

@ -79,7 +79,7 @@
"scripty": "^1.5.0", "scripty": "^1.5.0",
"sequelize": "^4.7.5", "sequelize": "^4.7.5",
"ts-node": "^3.0.6", "ts-node": "^3.0.6",
"typescript": "^2.4.1", "typescript": "^2.5.2",
"validator": "^8.1.0", "validator": "^8.1.0",
"winston": "^2.1.1", "winston": "^2.1.1",
"ws": "^3.1.0" "ws": "^3.1.0"
@ -109,7 +109,7 @@
"source-map-support": "^0.4.15", "source-map-support": "^0.4.15",
"standard": "^10.0.0", "standard": "^10.0.0",
"supertest": "^3.0.0", "supertest": "^3.0.0",
"tslint": "^5.2.0", "tslint": "^5.7.0",
"tslint-config-standard": "^6.0.0", "tslint-config-standard": "^6.0.0",
"webtorrent": "^0.98.0" "webtorrent": "^0.98.0"
}, },

View file

@ -1,7 +1,7 @@
import * as express from 'express' import * as express from 'express'
import { database as db } from '../../initializers/database' import { database as db } from '../../initializers/database'
import { USER_ROLES } from '../../initializers' import { USER_ROLES, CONFIG } from '../../initializers'
import { logger, getFormattedObjects } from '../../helpers' import { logger, getFormattedObjects } from '../../helpers'
import { import {
authenticate, authenticate,
@ -80,12 +80,18 @@ export {
function createUser (req: express.Request, res: express.Response, next: express.NextFunction) { function createUser (req: express.Request, res: express.Response, next: express.NextFunction) {
const body: UserCreate = req.body 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({ const user = db.User.build({
username: body.username, username: body.username,
password: body.password, password: body.password,
email: body.email, email: body.email,
displayNSFW: false, displayNSFW: false,
role: USER_ROLES.USER role: USER_ROLES.USER,
videoQuota: body.videoQuota
}) })
user.save() user.save()
@ -140,6 +146,7 @@ function updateUser (req: express.Request, res: express.Response, next: express.
.then(user => { .then(user => {
if (body.password) user.password = body.password if (body.password) user.password = body.password
if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW
if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
return user.save() return user.save()
}) })

View file

@ -15,6 +15,10 @@ function isUserRoleValid (value: string) {
return values(USER_ROLES).indexOf(value as UserRole) !== -1 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) { function isUserUsernameValid (value: string) {
const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max
const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min
@ -30,6 +34,7 @@ function isUserDisplayNSFWValid (value: any) {
export { export {
isUserPasswordValid, isUserPasswordValid,
isUserRoleValid, isUserRoleValid,
isUserVideoQuotaValid,
isUserUsernameValid, isUserUsernameValid,
isUserDisplayNSFWValid isUserDisplayNSFWValid
} }
@ -39,6 +44,7 @@ declare module 'express-validator' {
isUserPasswordValid, isUserPasswordValid,
isUserRoleValid, isUserRoleValid,
isUserUsernameValid, isUserUsernameValid,
isUserDisplayNSFWValid isUserDisplayNSFWValid,
isUserVideoQuotaValid
} }
} }

View file

@ -15,7 +15,7 @@ import {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 65 const LAST_MIGRATION_VERSION = 70
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -77,7 +77,10 @@ const CONFIG = {
}, },
SIGNUP: { SIGNUP: {
ENABLED: config.get<boolean>('signup.enabled'), 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: { TRANSCODING: {
ENABLED: config.get<boolean>('transcoding.enabled'), ENABLED: config.get<boolean>('transcoding.enabled'),
@ -97,7 +100,8 @@ CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
const CONSTRAINTS_FIELDS = { const CONSTRAINTS_FIELDS = {
USERS: { USERS: {
USERNAME: { min: 3, max: 20 }, // Length USERNAME: { min: 3, max: 20 }, // Length
PASSWORD: { min: 6, max: 255 } // Length PASSWORD: { min: 6, max: 255 }, // Length
VIDEO_QUOTA: { min: -1 }
}, },
VIDEO_ABUSES: { VIDEO_ABUSES: {
REASON: { min: 2, max: 300 } // Length REASON: { min: 2, max: 300 } // Length

View file

@ -1,5 +1,6 @@
import { join } from 'path' import { join } from 'path'
import { flattenDepth } from 'lodash' import { flattenDepth } from 'lodash'
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird' import * as Promise from 'bluebird'

View file

@ -38,12 +38,12 @@ function removeCacheDirectories () {
} }
function createDirectoriesIfNotExist () { function createDirectoriesIfNotExist () {
const storages = CONFIG.STORAGE const storage = CONFIG.STORAGE
const cacheDirectories = CACHE.DIRECTORIES const cacheDirectories = CACHE.DIRECTORIES
const tasks = [] const tasks = []
Object.keys(storages).forEach(key => { Object.keys(storage).forEach(key => {
const dir = storages[key] const dir = storage[key]
tasks.push(mkdirpPromise(dir)) tasks.push(mkdirpPromise(dir))
}) })
@ -112,7 +112,8 @@ function createOAuthAdminIfNotExist () {
username, username,
email, email,
password, password,
role role,
videoQuota: -1
} }
return db.User.create(userData, createOptions).then(createdUser => { return db.User.create(userData, createOptions).then(createdUser => {

View file

@ -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
}

View 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('username', 'Should have a valid username').isUserUsernameValid()
req.checkBody('password', 'Should have a valid password').isUserPasswordValid() req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
req.checkBody('email', 'Should have a valid email').isEmail() 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 }) 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 // Add old password verification
req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid() 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('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 }) logger.debug('Checking usersUpdate parameters', { parameters: req.body })

View 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 }) logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
checkErrors(req, res, () => { 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 => { .then(duration => {
// Previous test failed, abort
if (duration === undefined) return
if (!isVideoDurationValid('' + duration)) { if (!isVideoDurationValid('' + duration)) {
return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).') return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
} }

View file

@ -11,6 +11,7 @@ export namespace UserMethods {
export type ToFormattedJSON = (this: UserInstance) => FormattedUser export type ToFormattedJSON = (this: UserInstance) => FormattedUser
export type IsAdmin = (this: UserInstance) => boolean export type IsAdmin = (this: UserInstance) => boolean
export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
export type CountTotal = () => Promise<number> export type CountTotal = () => Promise<number>
@ -31,6 +32,7 @@ export interface UserClass {
isPasswordMatch: UserMethods.IsPasswordMatch, isPasswordMatch: UserMethods.IsPasswordMatch,
toFormattedJSON: UserMethods.ToFormattedJSON, toFormattedJSON: UserMethods.ToFormattedJSON,
isAdmin: UserMethods.IsAdmin, isAdmin: UserMethods.IsAdmin,
isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
countTotal: UserMethods.CountTotal, countTotal: UserMethods.CountTotal,
getByUsername: UserMethods.GetByUsername, getByUsername: UserMethods.GetByUsername,
@ -42,11 +44,13 @@ export interface UserClass {
} }
export interface UserAttributes { export interface UserAttributes {
id?: number
password: string password: string
username: string username: string
email: string email: string
displayNSFW?: boolean displayNSFW?: boolean
role: UserRole role: UserRole
videoQuota: number
} }
export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {

View file

@ -1,5 +1,6 @@
import { values } from 'lodash' import { values } from 'lodash'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { getSort } from '../utils' import { getSort } from '../utils'
import { USER_ROLES } from '../../initializers' import { USER_ROLES } from '../../initializers'
@ -8,7 +9,8 @@ import {
comparePassword, comparePassword,
isUserPasswordValid, isUserPasswordValid,
isUserUsernameValid, isUserUsernameValid,
isUserDisplayNSFWValid isUserDisplayNSFWValid,
isUserVideoQuotaValid
} from '../../helpers' } from '../../helpers'
import { addMethodsToModel } from '../utils' import { addMethodsToModel } from '../utils'
@ -30,6 +32,7 @@ let listForApi: UserMethods.ListForApi
let loadById: UserMethods.LoadById let loadById: UserMethods.LoadById
let loadByUsername: UserMethods.LoadByUsername let loadByUsername: UserMethods.LoadByUsername
let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
User = sequelize.define<UserInstance, UserAttributes>('User', User = sequelize.define<UserInstance, UserAttributes>('User',
@ -75,6 +78,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
role: { role: {
type: DataTypes.ENUM(values(USER_ROLES)), type: DataTypes.ENUM(values(USER_ROLES)),
allowNull: false 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 = [ const instanceMethods = [
isPasswordMatch, isPasswordMatch,
toFormattedJSON, toFormattedJSON,
isAdmin isAdmin,
isAbleToUploadVideo
] ]
addMethodsToModel(User, classMethods, instanceMethods) addMethodsToModel(User, classMethods, instanceMethods)
@ -136,6 +150,7 @@ toFormattedJSON = function (this: UserInstance) {
email: this.email, email: this.email,
displayNSFW: this.displayNSFW, displayNSFW: this.displayNSFW,
role: this.role, role: this.role,
videoQuota: this.videoQuota,
createdAt: this.createdAt createdAt: this.createdAt
} }
} }
@ -144,6 +159,14 @@ isAdmin = function (this: UserInstance) {
return this.role === USER_ROLES.ADMIN 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 ------------------------------ // ------------------------------ STATICS ------------------------------
function associate (models) { function associate (models) {
@ -215,3 +238,36 @@ loadByUsernameOrEmail = function (username: string, email: string) {
// FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
return (User as any).findOne(query) 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
})
}

View file

@ -9,6 +9,7 @@ import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird' import * as Promise from 'bluebird'
import { TagInstance } from './tag-interface' import { TagInstance } from './tag-interface'
import { UserInstance } from '../user/user-interface'
import { import {
logger, logger,
isVideoNameValid, isVideoNameValid,
@ -582,7 +583,7 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns
return res() return res()
}) })
.catch(err => { .catch(err => {
// Autodestruction... // Auto destruction...
this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
return rej(err) return rej(err)
@ -608,8 +609,8 @@ removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
} }
removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
return unlinkPromise(torrenPath) return unlinkPromise(torrentPath)
} }
// ------------------------------ STATICS ------------------------------ // ------------------------------ STATICS ------------------------------

View file

@ -2,4 +2,5 @@ export interface UserCreate {
username: string username: string
password: string password: string
email: string email: string
videoQuota: number
} }

View file

@ -1,4 +1,5 @@
export interface UserUpdate { export interface UserUpdate {
displayNSFW?: boolean displayNSFW?: boolean
password?: string password?: string
videoQuota?: number
} }

View file

@ -6,5 +6,6 @@ export interface User {
email: string email: string
displayNSFW: boolean displayNSFW: boolean
role: UserRole role: UserRole
videoQuota: number
createdAt: Date createdAt: Date
} }

View file

@ -4,6 +4,7 @@
"no-inferrable-types": true, "no-inferrable-types": true,
"eofline": true, "eofline": true,
"indent": ["spaces"], "indent": ["spaces"],
"ter-indent": [true, 2],
"max-line-length": [true, 140], "max-line-length": [true, 140],
"no-floating-promises": false "no-floating-promises": false
} }

View file

@ -3755,9 +3755,9 @@ tslint-eslint-rules@^4.0.0:
tslib "^1.0.0" tslib "^1.0.0"
tsutils "^1.4.0" tsutils "^1.4.0"
tslint@^5.2.0: tslint@^5.7.0:
version "5.6.0" version "5.7.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.6.0.tgz#088aa6c6026623338650b2900828ab3edf59f6cf" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.7.0.tgz#c25e0d0c92fa1201c2bc30e844e08e682b4f3552"
dependencies: dependencies:
babel-code-frame "^6.22.0" babel-code-frame "^6.22.0"
colors "^1.1.2" colors "^1.1.2"
@ -3768,15 +3768,15 @@ tslint@^5.2.0:
resolve "^1.3.2" resolve "^1.3.2"
semver "^5.3.0" semver "^5.3.0"
tslib "^1.7.1" tslib "^1.7.1"
tsutils "^2.7.1" tsutils "^2.8.1"
tsutils@^1.4.0: tsutils@^1.4.0:
version "1.9.1" version "1.9.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0"
tsutils@^2.7.1: tsutils@^2.8.1:
version "2.8.1" version "2.8.2"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.1.tgz#3771404e7ca9f0bedf5d919a47a4b1890a68efff" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.2.tgz#2c1486ba431260845b0ac6f902afd9d708a8ea6a"
dependencies: dependencies:
tslib "^1.7.1" tslib "^1.7.1"
@ -3821,9 +3821,9 @@ typedarray@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typescript@^2.4.1: typescript@^2.5.2:
version "2.5.1" version "2.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.1.tgz#ce7cc93ada3de19475cc9d17e3adea7aee1832aa" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.2.tgz#038a95f7d9bbb420b1bf35ba31d4c5c1dd3ffe34"
uid-number@^0.0.6: uid-number@^0.0.6:
version "0.0.6" version "0.0.6"