aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-09-04 20:07:54 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-09-04 20:07:54 +0200
commitb0f9f39ed70299a208d1b388c72de8b7f3510cb7 (patch)
tree4b7d388125265533ac2f6d4bf457d018617e1db6 /server
parente7dbeae8d915cdf4470ceb51c2724b04148b30b5 (diff)
downloadPeerTube-b0f9f39ed70299a208d1b388c72de8b7f3510cb7.tar.gz
PeerTube-b0f9f39ed70299a208d1b388c72de8b7f3510cb7.tar.zst
PeerTube-b0f9f39ed70299a208d1b388c72de8b7f3510cb7.zip
Begin user quota
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/users.ts11
-rw-r--r--server/helpers/custom-validators/users.ts8
-rw-r--r--server/initializers/constants.ts10
-rw-r--r--server/initializers/database.ts1
-rw-r--r--server/initializers/installer.ts9
-rw-r--r--server/initializers/migrations/0070-user-video-quota.ts32
-rw-r--r--server/middlewares/validators/users.ts2
-rw-r--r--server/middlewares/validators/videos.ts17
-rw-r--r--server/models/user/user-interface.ts4
-rw-r--r--server/models/user/user.ts60
-rw-r--r--server/models/video/video.ts7
11 files changed, 144 insertions, 17 deletions
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 @@
1import * as express from 'express' 1import * as express from 'express'
2 2
3import { database as db } from '../../initializers/database' 3import { database as db } from '../../initializers/database'
4import { USER_ROLES } from '../../initializers' 4import { USER_ROLES, CONFIG } from '../../initializers'
5import { logger, getFormattedObjects } from '../../helpers' 5import { logger, getFormattedObjects } from '../../helpers'
6import { 6import {
7 authenticate, 7 authenticate,
@@ -80,12 +80,18 @@ export {
80function createUser (req: express.Request, res: express.Response, next: express.NextFunction) { 80function createUser (req: express.Request, res: express.Response, next: express.NextFunction) {
81 const body: UserCreate = req.body 81 const body: UserCreate = req.body
82 82
83 // On registration, we set the user video quota
84 if (body.videoQuota === undefined) {
85 body.videoQuota = CONFIG.USER.VIDEO_QUOTA
86 }
87
83 const user = db.User.build({ 88 const user = db.User.build({
84 username: body.username, 89 username: body.username,
85 password: body.password, 90 password: body.password,
86 email: body.email, 91 email: body.email,
87 displayNSFW: false, 92 displayNSFW: false,
88 role: USER_ROLES.USER 93 role: USER_ROLES.USER,
94 videoQuota: body.videoQuota
89 }) 95 })
90 96
91 user.save() 97 user.save()
@@ -140,6 +146,7 @@ function updateUser (req: express.Request, res: express.Response, next: express.
140 .then(user => { 146 .then(user => {
141 if (body.password) user.password = body.password 147 if (body.password) user.password = body.password
142 if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW 148 if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW
149 if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
143 150
144 return user.save() 151 return user.save()
145 }) 152 })
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) {
15 return values(USER_ROLES).indexOf(value as UserRole) !== -1 15 return values(USER_ROLES).indexOf(value as UserRole) !== -1
16} 16}
17 17
18function isUserVideoQuotaValid (value: string) {
19 return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
20}
21
18function isUserUsernameValid (value: string) { 22function isUserUsernameValid (value: string) {
19 const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max 23 const max = USERS_CONSTRAINTS_FIELDS.USERNAME.max
20 const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min 24 const min = USERS_CONSTRAINTS_FIELDS.USERNAME.min
@@ -30,6 +34,7 @@ function isUserDisplayNSFWValid (value: any) {
30export { 34export {
31 isUserPasswordValid, 35 isUserPasswordValid,
32 isUserRoleValid, 36 isUserRoleValid,
37 isUserVideoQuotaValid,
33 isUserUsernameValid, 38 isUserUsernameValid,
34 isUserDisplayNSFWValid 39 isUserDisplayNSFWValid
35} 40}
@@ -39,6 +44,7 @@ declare module 'express-validator' {
39 isUserPasswordValid, 44 isUserPasswordValid,
40 isUserRoleValid, 45 isUserRoleValid,
41 isUserUsernameValid, 46 isUserUsernameValid,
42 isUserDisplayNSFWValid 47 isUserDisplayNSFWValid,
48 isUserVideoQuotaValid
43 } 49 }
44} 50}
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 {
15 15
16// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
17 17
18const LAST_MIGRATION_VERSION = 65 18const LAST_MIGRATION_VERSION = 70
19 19
20// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
21 21
@@ -77,7 +77,10 @@ const CONFIG = {
77 }, 77 },
78 SIGNUP: { 78 SIGNUP: {
79 ENABLED: config.get<boolean>('signup.enabled'), 79 ENABLED: config.get<boolean>('signup.enabled'),
80 LIMIT: config.get<number>('signup.limit') 80 LIMIT: config.get<number>('signup.limit'),
81 },
82 USER: {
83 VIDEO_QUOTA: config.get<number>('user.video_quota')
81 }, 84 },
82 TRANSCODING: { 85 TRANSCODING: {
83 ENABLED: config.get<boolean>('transcoding.enabled'), 86 ENABLED: config.get<boolean>('transcoding.enabled'),
@@ -97,7 +100,8 @@ CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
97const CONSTRAINTS_FIELDS = { 100const CONSTRAINTS_FIELDS = {
98 USERS: { 101 USERS: {
99 USERNAME: { min: 3, max: 20 }, // Length 102 USERNAME: { min: 3, max: 20 }, // Length
100 PASSWORD: { min: 6, max: 255 } // Length 103 PASSWORD: { min: 6, max: 255 }, // Length
104 VIDEO_QUOTA: { min: -1 }
101 }, 105 },
102 VIDEO_ABUSES: { 106 VIDEO_ABUSES: {
103 REASON: { min: 2, max: 300 } // Length 107 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 @@
1import { join } from 'path' 1import { join } from 'path'
2import { flattenDepth } from 'lodash' 2import { flattenDepth } from 'lodash'
3require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
3import * as Sequelize from 'sequelize' 4import * as Sequelize from 'sequelize'
4import * as Promise from 'bluebird' 5import * as Promise from 'bluebird'
5 6
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 () {
38} 38}
39 39
40function createDirectoriesIfNotExist () { 40function createDirectoriesIfNotExist () {
41 const storages = CONFIG.STORAGE 41 const storage = CONFIG.STORAGE
42 const cacheDirectories = CACHE.DIRECTORIES 42 const cacheDirectories = CACHE.DIRECTORIES
43 43
44 const tasks = [] 44 const tasks = []
45 Object.keys(storages).forEach(key => { 45 Object.keys(storage).forEach(key => {
46 const dir = storages[key] 46 const dir = storage[key]
47 tasks.push(mkdirpPromise(dir)) 47 tasks.push(mkdirpPromise(dir))
48 }) 48 })
49 49
@@ -112,7 +112,8 @@ function createOAuthAdminIfNotExist () {
112 username, 112 username,
113 email, 113 email,
114 password, 114 password,
115 role 115 role,
116 videoQuota: -1
116 } 117 }
117 118
118 return db.User.create(userData, createOptions).then(createdUser => { 119 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 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize,
8 db: any
9}): Promise<void> {
10 const q = utils.queryInterface
11
12 const data = {
13 type: Sequelize.BIGINT,
14 allowNull: false,
15 defaultValue: -1
16 }
17
18 return q.addColumn('Users', 'videoQuota', data)
19 .then(() => {
20 data.defaultValue = null
21 return q.changeColumn('Users', 'videoQuota', data)
22 })
23}
24
25function down (options) {
26 throw new Error('Not implemented.')
27}
28
29export {
30 up,
31 down
32}
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
12 req.checkBody('username', 'Should have a valid username').isUserUsernameValid() 12 req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
13 req.checkBody('password', 'Should have a valid password').isUserPasswordValid() 13 req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
14 req.checkBody('email', 'Should have a valid email').isEmail() 14 req.checkBody('email', 'Should have a valid email').isEmail()
15 req.checkBody('videoQuota', 'Should have a valid user quota').isUserVideoQuotaValid()
15 16
16 logger.debug('Checking usersAdd parameters', { parameters: req.body }) 17 logger.debug('Checking usersAdd parameters', { parameters: req.body })
17 18
@@ -55,6 +56,7 @@ function usersUpdateValidator (req: express.Request, res: express.Response, next
55 // Add old password verification 56 // Add old password verification
56 req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid() 57 req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid()
57 req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid() 58 req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid()
59 req.checkBody('videoQuota', 'Should have a valid user quota').optional().isUserVideoQuotaValid()
58 60
59 logger.debug('Checking usersUpdate parameters', { parameters: req.body }) 61 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
60 62
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:
24 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) 24 logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
25 25
26 checkErrors(req, res, () => { 26 checkErrors(req, res, () => {
27 const videoFile = req.files['videofile'][0] 27 const videoFile: Express.Multer.File = req.files['videofile'][0]
28 const user = res.locals.oauth.token.User
28 29
29 db.Video.getDurationFromFile(videoFile.path) 30 user.isAbleToUploadVideo(videoFile)
31 .then(isAble => {
32 if (isAble === false) {
33 res.status(403).send('The user video quota is exceeded with this video.')
34
35 return undefined
36 }
37
38 return db.Video.getDurationFromFile(videoFile.path)
39 })
30 .then(duration => { 40 .then(duration => {
41 // Previous test failed, abort
42 if (duration === undefined) return
43
31 if (!isVideoDurationValid('' + duration)) { 44 if (!isVideoDurationValid('' + duration)) {
32 return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).') 45 return res.status(400).send('Duration of the video file is too big (max: ' + CONSTRAINTS_FIELDS.VIDEOS.DURATION.max + 's).')
33 } 46 }
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 {
11 11
12 export type ToFormattedJSON = (this: UserInstance) => FormattedUser 12 export type ToFormattedJSON = (this: UserInstance) => FormattedUser
13 export type IsAdmin = (this: UserInstance) => boolean 13 export type IsAdmin = (this: UserInstance) => boolean
14 export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
14 15
15 export type CountTotal = () => Promise<number> 16 export type CountTotal = () => Promise<number>
16 17
@@ -31,6 +32,7 @@ export interface UserClass {
31 isPasswordMatch: UserMethods.IsPasswordMatch, 32 isPasswordMatch: UserMethods.IsPasswordMatch,
32 toFormattedJSON: UserMethods.ToFormattedJSON, 33 toFormattedJSON: UserMethods.ToFormattedJSON,
33 isAdmin: UserMethods.IsAdmin, 34 isAdmin: UserMethods.IsAdmin,
35 isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
34 36
35 countTotal: UserMethods.CountTotal, 37 countTotal: UserMethods.CountTotal,
36 getByUsername: UserMethods.GetByUsername, 38 getByUsername: UserMethods.GetByUsername,
@@ -42,11 +44,13 @@ export interface UserClass {
42} 44}
43 45
44export interface UserAttributes { 46export interface UserAttributes {
47 id?: number
45 password: string 48 password: string
46 username: string 49 username: string
47 email: string 50 email: string
48 displayNSFW?: boolean 51 displayNSFW?: boolean
49 role: UserRole 52 role: UserRole
53 videoQuota: number
50} 54}
51 55
52export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { 56export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
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 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3import * as Promise from 'bluebird'
3 4
4import { getSort } from '../utils' 5import { getSort } from '../utils'
5import { USER_ROLES } from '../../initializers' 6import { USER_ROLES } from '../../initializers'
@@ -8,7 +9,8 @@ import {
8 comparePassword, 9 comparePassword,
9 isUserPasswordValid, 10 isUserPasswordValid,
10 isUserUsernameValid, 11 isUserUsernameValid,
11 isUserDisplayNSFWValid 12 isUserDisplayNSFWValid,
13 isUserVideoQuotaValid
12} from '../../helpers' 14} from '../../helpers'
13 15
14import { addMethodsToModel } from '../utils' 16import { addMethodsToModel } from '../utils'
@@ -30,6 +32,7 @@ let listForApi: UserMethods.ListForApi
30let loadById: UserMethods.LoadById 32let loadById: UserMethods.LoadById
31let loadByUsername: UserMethods.LoadByUsername 33let loadByUsername: UserMethods.LoadByUsername
32let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail 34let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
35let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
33 36
34export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 37export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
35 User = sequelize.define<UserInstance, UserAttributes>('User', 38 User = sequelize.define<UserInstance, UserAttributes>('User',
@@ -75,6 +78,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
75 role: { 78 role: {
76 type: DataTypes.ENUM(values(USER_ROLES)), 79 type: DataTypes.ENUM(values(USER_ROLES)),
77 allowNull: false 80 allowNull: false
81 },
82 videoQuota: {
83 type: DataTypes.BIGINT,
84 allowNull: false,
85 validate: {
86 videoQuotaValid: value => {
87 const res = isUserVideoQuotaValid(value)
88 if (res === false) throw new Error('Video quota is not valid.')
89 }
90 }
78 } 91 }
79 }, 92 },
80 { 93 {
@@ -109,7 +122,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
109 const instanceMethods = [ 122 const instanceMethods = [
110 isPasswordMatch, 123 isPasswordMatch,
111 toFormattedJSON, 124 toFormattedJSON,
112 isAdmin 125 isAdmin,
126 isAbleToUploadVideo
113 ] 127 ]
114 addMethodsToModel(User, classMethods, instanceMethods) 128 addMethodsToModel(User, classMethods, instanceMethods)
115 129
@@ -136,6 +150,7 @@ toFormattedJSON = function (this: UserInstance) {
136 email: this.email, 150 email: this.email,
137 displayNSFW: this.displayNSFW, 151 displayNSFW: this.displayNSFW,
138 role: this.role, 152 role: this.role,
153 videoQuota: this.videoQuota,
139 createdAt: this.createdAt 154 createdAt: this.createdAt
140 } 155 }
141} 156}
@@ -144,6 +159,14 @@ isAdmin = function (this: UserInstance) {
144 return this.role === USER_ROLES.ADMIN 159 return this.role === USER_ROLES.ADMIN
145} 160}
146 161
162isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
163 if (this.videoQuota === -1) return Promise.resolve(true)
164
165 return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
166 return (videoFile.size + totalBytes) < this.videoQuota
167 })
168}
169
147// ------------------------------ STATICS ------------------------------ 170// ------------------------------ STATICS ------------------------------
148 171
149function associate (models) { 172function associate (models) {
@@ -215,3 +238,36 @@ loadByUsernameOrEmail = function (username: string, email: string) {
215 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 238 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
216 return (User as any).findOne(query) 239 return (User as any).findOne(query)
217} 240}
241
242// ---------------------------------------------------------------------------
243
244function getOriginalVideoFileTotalFromUser (user: UserInstance) {
245 const query = {
246 attributes: [
247 Sequelize.fn('COUNT', Sequelize.col('VideoFile.size'), 'totalVideoBytes')
248 ],
249 where: {
250 id: user.id
251 },
252 include: [
253 {
254 model: User['sequelize'].models.Author,
255 include: [
256 {
257 model: User['sequelize'].models.Video,
258 include: [
259 {
260 model: User['sequelize'].models.VideoFile
261 }
262 ]
263 }
264 ]
265 }
266 ]
267 }
268
269 // FIXME: cast to any because of bad typing...
270 return User.findAll(query).then((res: any) => {
271 return res.totalVideoBytes
272 })
273}
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'
9import * as Promise from 'bluebird' 9import * as Promise from 'bluebird'
10 10
11import { TagInstance } from './tag-interface' 11import { TagInstance } from './tag-interface'
12import { UserInstance } from '../user/user-interface'
12import { 13import {
13 logger, 14 logger,
14 isVideoNameValid, 15 isVideoNameValid,
@@ -582,7 +583,7 @@ transcodeVideofile = function (this: VideoInstance, inputVideoFile: VideoFileIns
582 return res() 583 return res()
583 }) 584 })
584 .catch(err => { 585 .catch(err => {
585 // Autodestruction... 586 // Auto destruction...
586 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) 587 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
587 588
588 return rej(err) 589 return rej(err)
@@ -608,8 +609,8 @@ removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
608} 609}
609 610
610removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { 611removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
611 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 612 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
612 return unlinkPromise(torrenPath) 613 return unlinkPromise(torrentPath)
613} 614}
614 615
615// ------------------------------ STATICS ------------------------------ 616// ------------------------------ STATICS ------------------------------