aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFelix Ableitner <me@nutomic.com>2018-08-28 02:01:35 -0500
committerChocobozzz <me@florianbigard.com>2018-08-28 09:01:35 +0200
commitbee0abffff73804d816b90c7fd599e0a51c09d61 (patch)
treefae6d58637f9c63a3800090277f8e130b43442dd
parentc907c2fa3fd7c0a741117a0204d0ebca675124bd (diff)
downloadPeerTube-bee0abffff73804d816b90c7fd599e0a51c09d61.tar.gz
PeerTube-bee0abffff73804d816b90c7fd599e0a51c09d61.tar.zst
PeerTube-bee0abffff73804d816b90c7fd599e0a51c09d61.zip
Implement daily upload limit (#956)
* Implement daily upload limit (ref #652) * remove duplicate code * review fixes * fix tests? * whitespace fixes, finish leftover todo * fix tests * added some new tests * use different config value for tests * remove todo
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html14
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts24
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html9
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts17
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts9
-rw-r--r--client/src/app/core/server/server.service.ts3
-rw-r--r--client/src/app/shared/forms/form-validators/user-validators.service.ts8
-rw-r--r--client/src/app/shared/users/user.model.ts3
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts22
-rw-r--r--config/default.yaml1
-rw-r--r--config/production.yaml.example1
-rw-r--r--server/controllers/api/config.ts7
-rw-r--r--server/controllers/api/users/index.ts7
-rw-r--r--server/controllers/api/users/me.ts4
-rw-r--r--server/helpers/custom-validators/users.ts5
-rw-r--r--server/initializers/checker.ts2
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/initializers/installer.ts3
-rw-r--r--server/initializers/migrations/0260-upload_quota_daily.ts23
-rw-r--r--server/middlewares/validators/users.ts5
-rw-r--r--server/models/account/user.ts68
-rw-r--r--server/tests/api/check-params/config.ts3
-rw-r--r--server/tests/api/check-params/users.ts56
-rw-r--r--server/tests/api/server/config.ts5
-rw-r--r--server/tests/utils/server/config.ts3
-rw-r--r--server/tests/utils/users/users.ts6
-rw-r--r--shared/models/server/custom-config.model.ts1
-rw-r--r--shared/models/server/server-config.model.ts1
-rw-r--r--shared/models/users/user-create.model.ts1
-rw-r--r--shared/models/users/user-update.model.ts1
-rw-r--r--shared/models/users/user-video-quota.model.ts1
-rw-r--r--shared/models/users/user.model.ts1
32 files changed, 273 insertions, 45 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 49b89cef4..ca7890d84 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -142,6 +142,20 @@
142 {{ formErrors.userVideoQuota }} 142 {{ formErrors.userVideoQuota }}
143 </div> 143 </div>
144 </div> 144 </div>
145
146 <div class="form-group">
147 <label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
148 <div class="peertube-select-container">
149 <select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily">
150 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
151 {{ videoQuotaDailyOption.label }}
152 </option>
153 </select>
154 </div>
155 <div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
156 {{ formErrors.userVideoQuotaDaily }}
157 </div>
158 </div>
145 </ng-template> 159 </ng-template>
146 </ngb-tab> 160 </ngb-tab>
147 161
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index fd6784415..3b6dabcb9 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -15,10 +15,7 @@ import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/
15 styleUrls: [ './edit-custom-config.component.scss' ] 15 styleUrls: [ './edit-custom-config.component.scss' ]
16}) 16})
17export class EditCustomConfigComponent extends FormReactive implements OnInit { 17export class EditCustomConfigComponent extends FormReactive implements OnInit {
18 customConfig: CustomConfig 18 static videoQuotaOptions = [
19 resolutions = [ '240p', '360p', '480p', '720p', '1080p' ]
20
21 videoQuotaOptions = [
22 { value: -1, label: 'Unlimited' }, 19 { value: -1, label: 'Unlimited' },
23 { value: 0, label: '0' }, 20 { value: 0, label: '0' },
24 { value: 100 * 1024 * 1024, label: '100MB' }, 21 { value: 100 * 1024 * 1024, label: '100MB' },
@@ -28,6 +25,20 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
28 { value: 20 * 1024 * 1024 * 1024, label: '20GB' }, 25 { value: 20 * 1024 * 1024 * 1024, label: '20GB' },
29 { value: 50 * 1024 * 1024 * 1024, label: '50GB' } 26 { value: 50 * 1024 * 1024 * 1024, label: '50GB' }
30 ] 27 ]
28 static videoQuotaDailyOptions = [
29 { value: -1, label: 'Unlimited' },
30 { value: 0, label: '0' },
31 { value: 10 * 1024 * 1024, label: '10MB' },
32 { value: 50 * 1024 * 1024, label: '50MB' },
33 { value: 100 * 1024 * 1024, label: '100MB' },
34 { value: 500 * 1024 * 1024, label: '500MB' },
35 { value: 2 * 1024 * 1024 * 1024, label: '2GB' },
36 { value: 5 * 1024 * 1024 * 1024, label: '5GB' }
37 ]
38
39 customConfig: CustomConfig
40 resolutions = [ '240p', '360p', '480p', '720p', '1080p' ]
41
31 transcodingThreadOptions = [ 42 transcodingThreadOptions = [
32 { value: 0, label: 'Auto (via ffmpeg)' }, 43 { value: 0, label: 'Auto (via ffmpeg)' },
33 { value: 1, label: '1' }, 44 { value: 1, label: '1' },
@@ -75,6 +86,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
75 importVideosTorrentEnabled: null, 86 importVideosTorrentEnabled: null,
76 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, 87 adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
77 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, 88 userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
89 userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY,
78 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, 90 transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS,
79 transcodingEnabled: null, 91 transcodingEnabled: null,
80 customizationJavascript: null, 92 customizationJavascript: null,
@@ -173,7 +185,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
173 email: this.form.value['adminEmail'] 185 email: this.form.value['adminEmail']
174 }, 186 },
175 user: { 187 user: {
176 videoQuota: this.form.value['userVideoQuota'] 188 videoQuota: this.form.value['userVideoQuota'],
189 videoQuotaDaily: this.form.value['userVideoQuotaDaily']
177 }, 190 },
178 transcoding: { 191 transcoding: {
179 enabled: this.form.value['transcodingEnabled'], 192 enabled: this.form.value['transcodingEnabled'],
@@ -231,6 +244,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
231 signupLimit: this.customConfig.signup.limit, 244 signupLimit: this.customConfig.signup.limit,
232 adminEmail: this.customConfig.admin.email, 245 adminEmail: this.customConfig.admin.email,
233 userVideoQuota: this.customConfig.user.videoQuota, 246 userVideoQuota: this.customConfig.user.videoQuota,
247 userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
234 transcodingThreads: this.customConfig.transcoding.threads, 248 transcodingThreads: this.customConfig.transcoding.threads,
235 transcodingEnabled: this.customConfig.transcoding.enabled, 249 transcodingEnabled: this.customConfig.transcoding.enabled,
236 customizationJavascript: this.customConfig.instance.customizations.javascript, 250 customizationJavascript: this.customConfig.instance.customizations.javascript,
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 4626a40c9..bb745d6aa 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -61,6 +61,15 @@
61 </option> 61 </option>
62 </select> 62 </select>
63 </div> 63 </div>
64
65 <label i18n for="videoQuotaDaily">Daily video quota</label>
66 <div class="peertube-select-container">
67 <select id="videoQuotaDaily" formControlName="videoQuotaDaily">
68 <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
69 {{ videoQuotaDailyOption.label }}
70 </option>
71 </select>
72 </div>
64 73
65 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()"> 74 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
66 Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br /> 75 Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br />
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index ea8c733c3..4e7ca8a1b 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -1,18 +1,15 @@
1import { ServerService } from '../../../core' 1import { ServerService } from '../../../core'
2import { FormReactive } from '../../../shared' 2import { FormReactive } from '../../../shared'
3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' 3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
4import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/'
4 5
5export abstract class UserEdit extends FormReactive { 6export abstract class UserEdit extends FormReactive {
6 videoQuotaOptions = [ 7
7 { value: -1, label: 'Unlimited' }, 8 // These are used by a HTML select, so convert key into strings
8 { value: 0, label: '0' }, 9 videoQuotaOptions = EditCustomConfigComponent.videoQuotaOptions
9 { value: 100 * 1024 * 1024, label: '100MB' }, 10 .map(q => ({ value: q.value.toString(), label: q.label }))
10 { value: 500 * 1024 * 1024, label: '500MB' }, 11 videoQuotaDailyOptions = EditCustomConfigComponent.videoQuotaDailyOptions
11 { value: 1024 * 1024 * 1024, label: '1GB' }, 12 .map(q => ({ value: q.value.toString(), label: q.label }))
12 { value: 5 * 1024 * 1024 * 1024, label: '5GB' },
13 { value: 20 * 1024 * 1024 * 1024, label: '20GB' },
14 { value: 50 * 1024 * 1024 * 1024, label: '50GB' }
15 ].map(q => ({ value: q.value.toString(), label: q.label })) // Used by a HTML select, so convert key into strings
16 13
17 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) 14 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
18 15
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts
index 06bde582e..5821229b3 100644
--- a/client/src/app/+admin/users/user-edit/user-update.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-update.component.ts
@@ -36,11 +36,12 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
36 } 36 }
37 37
38 ngOnInit () { 38 ngOnInit () {
39 const defaultValues = { videoQuota: '-1' } 39 const defaultValues = { videoQuota: '-1', videoQuotaDaily: '-1' }
40 this.buildForm({ 40 this.buildForm({
41 email: this.userValidatorsService.USER_EMAIL, 41 email: this.userValidatorsService.USER_EMAIL,
42 role: this.userValidatorsService.USER_ROLE, 42 role: this.userValidatorsService.USER_ROLE,
43 videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA 43 videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
44 videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY
44 }, defaultValues) 45 }, defaultValues)
45 46
46 this.paramsSub = this.route.params.subscribe(routeParams => { 47 this.paramsSub = this.route.params.subscribe(routeParams => {
@@ -64,6 +65,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
64 65
65 // A select in HTML is always mapped as a string, we convert it to number 66 // A select in HTML is always mapped as a string, we convert it to number
66 userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) 67 userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10)
68 userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10)
67 69
68 this.userService.updateUser(this.userId, userUpdate).subscribe( 70 this.userService.updateUser(this.userId, userUpdate).subscribe(
69 () => { 71 () => {
@@ -93,7 +95,8 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
93 this.form.patchValue({ 95 this.form.patchValue({
94 email: userJson.email, 96 email: userJson.email,
95 role: userJson.role, 97 role: userJson.role,
96 videoQuota: userJson.videoQuota 98 videoQuota: userJson.videoQuota,
99 videoQuotaDaily: userJson.videoQuotaDaily
97 }) 100 })
98 } 101 }
99} 102}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 7823fa80e..a1ce12069 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -67,7 +67,8 @@ export class ServerService {
67 } 67 }
68 }, 68 },
69 user: { 69 user: {
70 videoQuota: -1 70 videoQuota: -1,
71 videoQuotaDaily: -1
71 }, 72 },
72 import: { 73 import: {
73 videos: { 74 videos: {
diff --git a/client/src/app/shared/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts
index ec9566ef3..424553d74 100644
--- a/client/src/app/shared/forms/form-validators/user-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/user-validators.service.ts
@@ -9,6 +9,7 @@ export class UserValidatorsService {
9 readonly USER_EMAIL: BuildFormValidator 9 readonly USER_EMAIL: BuildFormValidator
10 readonly USER_PASSWORD: BuildFormValidator 10 readonly USER_PASSWORD: BuildFormValidator
11 readonly USER_VIDEO_QUOTA: BuildFormValidator 11 readonly USER_VIDEO_QUOTA: BuildFormValidator
12 readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator
12 readonly USER_ROLE: BuildFormValidator 13 readonly USER_ROLE: BuildFormValidator
13 readonly USER_DISPLAY_NAME: BuildFormValidator 14 readonly USER_DISPLAY_NAME: BuildFormValidator
14 readonly USER_DESCRIPTION: BuildFormValidator 15 readonly USER_DESCRIPTION: BuildFormValidator
@@ -61,6 +62,13 @@ export class UserValidatorsService {
61 'min': this.i18n('Quota must be greater than -1.') 62 'min': this.i18n('Quota must be greater than -1.')
62 } 63 }
63 } 64 }
65 this.USER_VIDEO_QUOTA_DAILY = {
66 VALIDATORS: [ Validators.required, Validators.min(-1) ],
67 MESSAGES: {
68 'required': this.i18n('Daily upload limit is required.'),
69 'min': this.i18n('Daily upload limit must be greater than -1.')
70 }
71 }
64 72
65 this.USER_ROLE = { 73 this.USER_ROLE = {
66 VALIDATORS: [ Validators.required ], 74 VALIDATORS: [ Validators.required ],
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index 2748001d0..877f1bf3a 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -16,6 +16,7 @@ export type UserConstructorHash = {
16 email: string, 16 email: string,
17 role: UserRole, 17 role: UserRole,
18 videoQuota?: number, 18 videoQuota?: number,
19 videoQuotaDaily?: number,
19 nsfwPolicy?: NSFWPolicyType, 20 nsfwPolicy?: NSFWPolicyType,
20 autoPlayVideo?: boolean, 21 autoPlayVideo?: boolean,
21 createdAt?: Date, 22 createdAt?: Date,
@@ -33,6 +34,7 @@ export class User implements UserServerModel {
33 nsfwPolicy: NSFWPolicyType 34 nsfwPolicy: NSFWPolicyType
34 autoPlayVideo: boolean 35 autoPlayVideo: boolean
35 videoQuota: number 36 videoQuota: number
37 videoQuotaDaily: number
36 account: Account 38 account: Account
37 videoChannels: VideoChannel[] 39 videoChannels: VideoChannel[]
38 createdAt: Date 40 createdAt: Date
@@ -48,6 +50,7 @@ export class User implements UserServerModel {
48 50
49 this.videoChannels = hash.videoChannels 51 this.videoChannels = hash.videoChannels
50 this.videoQuota = hash.videoQuota 52 this.videoQuota = hash.videoQuota
53 this.videoQuotaDaily = hash.videoQuotaDaily
51 this.nsfwPolicy = hash.nsfwPolicy 54 this.nsfwPolicy = hash.nsfwPolicy
52 this.autoPlayVideo = hash.autoPlayVideo 55 this.autoPlayVideo = hash.autoPlayVideo
53 this.createdAt = hash.createdAt 56 this.createdAt = hash.createdAt
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
index 3ec89ff62..c9ab35b1d 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
@@ -31,6 +31,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
31 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY 31 readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
32 32
33 userVideoQuotaUsed = 0 33 userVideoQuotaUsed = 0
34 userVideoQuotaUsedDaily = 0
34 35
35 isUploadingVideo = false 36 isUploadingVideo = false
36 isUpdatingVideo = false 37 isUpdatingVideo = false
@@ -68,6 +69,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
68 69
69 this.userService.getMyVideoQuotaUsed() 70 this.userService.getMyVideoQuotaUsed()
70 .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed) 71 .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
72
73 this.userService.getMyVideoQuotaUsed()
74 .subscribe(data => this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily)
71 } 75 }
72 76
73 ngOnDestroy () { 77 ngOnDestroy () {
@@ -115,10 +119,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
115 return 119 return
116 } 120 }
117 121
122 const bytePipes = new BytesPipe()
118 const videoQuota = this.authService.getUser().videoQuota 123 const videoQuota = this.authService.getUser().videoQuota
119 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { 124 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
120 const bytePipes = new BytesPipe()
121
122 const msg = this.i18n( 125 const msg = this.i18n(
123 'Your video quota is exceeded with this video (video size: {{ videoSize }}, used: {{ videoQuotaUsed }}, quota: {{ videoQuota }})', 126 'Your video quota is exceeded with this video (video size: {{ videoSize }}, used: {{ videoQuotaUsed }}, quota: {{ videoQuota }})',
124 { 127 {
@@ -131,6 +134,21 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
131 return 134 return
132 } 135 }
133 136
137 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
138 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
139 const msg = this.i18n(
140 'Your daily video quota is exceeded with this video (video size: {{ videoSize }}, ' +
141 'used: {{ videoQuotaUsedDaily }}, quota: {{ videoQuotaDaily }})',
142 {
143 videoSize: bytePipes.transform(videofile.size, 0),
144 videoQuotaUsedDaily: bytePipes.transform(this.userVideoQuotaUsedDaily, 0),
145 videoQuotaDaily: bytePipes.transform(videoQuotaDaily, 0)
146 }
147 )
148 this.notificationsService.error(this.i18n('Error'), msg)
149 return
150 }
151
134 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') 152 const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '')
135 let name: string 153 let name: string
136 154
diff --git a/config/default.yaml b/config/default.yaml
index 6a02f254d..7799ea927 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -83,6 +83,7 @@ user:
83 # Default value of maximum video BYTES the user can upload (does not take into account transcoded files). 83 # Default value of maximum video BYTES the user can upload (does not take into account transcoded files).
84 # -1 == unlimited 84 # -1 == unlimited
85 video_quota: -1 85 video_quota: -1
86 video_quota_daily: -1
86 87
87# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag 88# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
88# In addition, if some resolutions are enabled the mp4 video file will be transcoded to these new resolutions. 89# In addition, if some resolutions are enabled the mp4 video file will be transcoded to these new resolutions.
diff --git a/config/production.yaml.example b/config/production.yaml.example
index fc698ae96..33a26dec1 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -96,6 +96,7 @@ user:
96 # Default value of maximum video BYTES the user can upload (does not take into account transcoded files). 96 # Default value of maximum video BYTES the user can upload (does not take into account transcoded files).
97 # -1 == unlimited 97 # -1 == unlimited
98 video_quota: -1 98 video_quota: -1
99 video_quota_daily: -1
99 100
100# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag 101# If enabled, the video will be transcoded to mp4 (x264) with "faststart" flag
101# In addition, if some resolutions are enabled the mp4 video file will be transcoded to these new resolutions. 102# In addition, if some resolutions are enabled the mp4 video file will be transcoded to these new resolutions.
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index b25f739bb..3fd355e6d 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -103,7 +103,8 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
103 } 103 }
104 }, 104 },
105 user: { 105 user: {
106 videoQuota: CONFIG.USER.VIDEO_QUOTA 106 videoQuota: CONFIG.USER.VIDEO_QUOTA,
107 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
107 } 108 }
108 } 109 }
109 110
@@ -154,6 +155,7 @@ async function updateCustomConfig (req: express.Request, res: express.Response,
154 toUpdate.cache.captions.size = parseInt('' + toUpdate.cache.captions.size, 10) 155 toUpdate.cache.captions.size = parseInt('' + toUpdate.cache.captions.size, 10)
155 toUpdate.signup.limit = parseInt('' + toUpdate.signup.limit, 10) 156 toUpdate.signup.limit = parseInt('' + toUpdate.signup.limit, 10)
156 toUpdate.user.videoQuota = parseInt('' + toUpdate.user.videoQuota, 10) 157 toUpdate.user.videoQuota = parseInt('' + toUpdate.user.videoQuota, 10)
158 toUpdate.user.videoQuotaDaily = parseInt('' + toUpdate.user.videoQuotaDaily, 10)
157 toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10) 159 toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10)
158 160
159 // camelCase to snake_case key 161 // camelCase to snake_case key
@@ -223,7 +225,8 @@ function customConfig (): CustomConfig {
223 email: CONFIG.ADMIN.EMAIL 225 email: CONFIG.ADMIN.EMAIL
224 }, 226 },
225 user: { 227 user: {
226 videoQuota: CONFIG.USER.VIDEO_QUOTA 228 videoQuota: CONFIG.USER.VIDEO_QUOTA,
229 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
227 }, 230 },
228 transcoding: { 231 transcoding: {
229 enabled: CONFIG.TRANSCODING.ENABLED, 232 enabled: CONFIG.TRANSCODING.ENABLED,
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 608d439ac..25d51ae5e 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -134,7 +134,8 @@ async function createUser (req: express.Request, res: express.Response) {
134 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 134 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
135 autoPlayVideo: true, 135 autoPlayVideo: true,
136 role: body.role, 136 role: body.role,
137 videoQuota: body.videoQuota 137 videoQuota: body.videoQuota,
138 videoQuotaDaily: body.videoQuotaDaily
138 }) 139 })
139 140
140 const { user, account } = await createUserAccountAndChannel(userToCreate) 141 const { user, account } = await createUserAccountAndChannel(userToCreate)
@@ -163,7 +164,8 @@ async function registerUser (req: express.Request, res: express.Response) {
163 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 164 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
164 autoPlayVideo: true, 165 autoPlayVideo: true,
165 role: UserRole.USER, 166 role: UserRole.USER,
166 videoQuota: CONFIG.USER.VIDEO_QUOTA 167 videoQuota: CONFIG.USER.VIDEO_QUOTA,
168 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
167 }) 169 })
168 170
169 const { user } = await createUserAccountAndChannel(userToCreate) 171 const { user } = await createUserAccountAndChannel(userToCreate)
@@ -219,6 +221,7 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
219 221
220 if (body.email !== undefined) userToUpdate.email = body.email 222 if (body.email !== undefined) userToUpdate.email = body.email
221 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota 223 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
224 if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
222 if (body.role !== undefined) userToUpdate.role = body.role 225 if (body.role !== undefined) userToUpdate.role = body.role
223 226
224 const user = await userToUpdate.save() 227 const user = await userToUpdate.save()
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 000c706b5..0f18b42f9 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -283,9 +283,11 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
283 // We did not load channels in res.locals.user 283 // We did not load channels in res.locals.user
284 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) 284 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
285 const videoQuotaUsed = await UserModel.getOriginalVideoFileTotalFromUser(user) 285 const videoQuotaUsed = await UserModel.getOriginalVideoFileTotalFromUser(user)
286 const videoQuotaUsedDaily = await UserModel.getOriginalVideoFileTotalDailyFromUser(user)
286 287
287 const data: UserVideoQuota = { 288 const data: UserVideoQuota = {
288 videoQuotaUsed 289 videoQuotaUsed,
290 videoQuotaUsedDaily
289 } 291 }
290 return res.json(data) 292 return res.json(data)
291} 293}
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index c3cdefd4e..8d6247e41 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -15,6 +15,10 @@ function isUserVideoQuotaValid (value: string) {
15 return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA) 15 return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
16} 16}
17 17
18function isUserVideoQuotaDailyValid (value: string) {
19 return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA_DAILY)
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
@@ -66,6 +70,7 @@ export {
66 isUserBlockedReasonValid, 70 isUserBlockedReasonValid,
67 isUserRoleValid, 71 isUserRoleValid,
68 isUserVideoQuotaValid, 72 isUserVideoQuotaValid,
73 isUserVideoQuotaDailyValid,
69 isUserUsernameValid, 74 isUserUsernameValid,
70 isUserNSFWPolicyValid, 75 isUserNSFWPolicyValid,
71 isUserAutoPlayVideoValid, 76 isUserAutoPlayVideoValid,
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts
index 608123607..916e9067e 100644
--- a/server/initializers/checker.ts
+++ b/server/initializers/checker.ts
@@ -47,7 +47,7 @@ function checkMissedConfig () {
47 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', 47 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
48 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', 48 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache',
49 'log.level', 49 'log.level',
50 'user.video_quota', 50 'user.video_quota', 'user.video_quota_daily',
51 'cache.previews.size', 'admin.email', 51 'cache.previews.size', 'admin.email',
52 'signup.enabled', 'signup.limit', 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', 52 'signup.enabled', 'signup.limit', 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
53 'transcoding.enabled', 'transcoding.threads', 53 'transcoding.enabled', 'transcoding.threads',
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index a0dd78f42..4111d04ec 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -202,7 +202,8 @@ const CONFIG = {
202 } 202 }
203 }, 203 },
204 USER: { 204 USER: {
205 get VIDEO_QUOTA () { return config.get<number>('user.video_quota') } 205 get VIDEO_QUOTA () { return config.get<number>('user.video_quota') },
206 get VIDEO_QUOTA_DAILY () { return config.get<number>('user.video_quota_daily') }
206 }, 207 },
207 TRANSCODING: { 208 TRANSCODING: {
208 get ENABLED () { return config.get<boolean>('transcoding.enabled') }, 209 get ENABLED () { return config.get<boolean>('transcoding.enabled') },
@@ -263,6 +264,7 @@ const CONSTRAINTS_FIELDS = {
263 USERNAME: { min: 3, max: 20 }, // Length 264 USERNAME: { min: 3, max: 20 }, // Length
264 PASSWORD: { min: 6, max: 255 }, // Length 265 PASSWORD: { min: 6, max: 255 }, // Length
265 VIDEO_QUOTA: { min: -1 }, 266 VIDEO_QUOTA: { min: -1 },
267 VIDEO_QUOTA_DAILY: { min: -1 },
266 BLOCKED_REASON: { min: 3, max: 250 } // Length 268 BLOCKED_REASON: { min: 3, max: 250 } // Length
267 }, 269 },
268 VIDEO_ABUSES: { 270 VIDEO_ABUSES: {
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index e319164e4..d4aaec8fe 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -123,7 +123,8 @@ async function createOAuthAdminIfNotExist () {
123 password, 123 password,
124 role, 124 role,
125 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 125 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
126 videoQuota: -1 126 videoQuota: -1,
127 videoQuotaDaily: -1
127 } 128 }
128 const user = new UserModel(userData) 129 const user = new UserModel(userData)
129 130
diff --git a/server/initializers/migrations/0260-upload_quota_daily.ts b/server/initializers/migrations/0260-upload_quota_daily.ts
new file mode 100644
index 000000000..d25154ba6
--- /dev/null
+++ b/server/initializers/migrations/0260-upload_quota_daily.ts
@@ -0,0 +1,23 @@
1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction
6 queryInterface: Sequelize.QueryInterface
7 sequelize: Sequelize.Sequelize
8}): Promise<any> {
9 {
10 const data = {
11 type: Sequelize.BIGINT,
12 allowNull: false,
13 defaultValue: -1
14 }
15 await utils.queryInterface.addColumn('user', 'videoQuotaDaily', data)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export { up, down }
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index c8baf22e2..6c5e783e9 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -12,7 +12,8 @@ import {
12 isUserPasswordValid, 12 isUserPasswordValid,
13 isUserRoleValid, 13 isUserRoleValid,
14 isUserUsernameValid, 14 isUserUsernameValid,
15 isUserVideoQuotaValid 15 isUserVideoQuotaValid,
16 isUserVideoQuotaDailyValid
16} from '../../helpers/custom-validators/users' 17} from '../../helpers/custom-validators/users'
17import { isVideoExist } from '../../helpers/custom-validators/videos' 18import { isVideoExist } from '../../helpers/custom-validators/videos'
18import { logger } from '../../helpers/logger' 19import { logger } from '../../helpers/logger'
@@ -27,6 +28,7 @@ const usersAddValidator = [
27 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), 28 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
28 body('email').isEmail().withMessage('Should have a valid email'), 29 body('email').isEmail().withMessage('Should have a valid email'),
29 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), 30 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
31 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
30 body('role').custom(isUserRoleValid).withMessage('Should have a valid role'), 32 body('role').custom(isUserRoleValid).withMessage('Should have a valid role'),
31 33
32 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 34 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -112,6 +114,7 @@ const usersUpdateValidator = [
112 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 114 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
113 body('email').optional().isEmail().withMessage('Should have a valid email attribute'), 115 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
114 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), 116 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
117 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
115 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), 118 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'),
116 119
117 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 120 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 0150df4ce..178012eae 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -27,7 +27,8 @@ import {
27 isUserPasswordValid, 27 isUserPasswordValid,
28 isUserRoleValid, 28 isUserRoleValid,
29 isUserUsernameValid, 29 isUserUsernameValid,
30 isUserVideoQuotaValid 30 isUserVideoQuotaValid,
31 isUserVideoQuotaDailyValid
31} from '../../helpers/custom-validators/users' 32} from '../../helpers/custom-validators/users'
32import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 33import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
33import { OAuthTokenModel } from '../oauth/oauth-token' 34import { OAuthTokenModel } from '../oauth/oauth-token'
@@ -124,6 +125,11 @@ export class UserModel extends Model<UserModel> {
124 @Column(DataType.BIGINT) 125 @Column(DataType.BIGINT)
125 videoQuota: number 126 videoQuota: number
126 127
128 @AllowNull(false)
129 @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
130 @Column(DataType.BIGINT)
131 videoQuotaDaily: number
132
127 @CreatedAt 133 @CreatedAt
128 createdAt: Date 134 createdAt: Date
129 135
@@ -271,7 +277,32 @@ export class UserModel extends Model<UserModel> {
271 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + 277 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
272 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 278 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
273 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + 279 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
274 'WHERE "account"."userId" = $userId GROUP BY "video"."id") t' 280 'WHERE "account"."userId" = $userId ' +
281 'GROUP BY "video"."id") t'
282
283 const options = {
284 bind: { userId: user.id },
285 type: Sequelize.QueryTypes.SELECT
286 }
287 return UserModel.sequelize.query(query, options)
288 .then(([ { total } ]) => {
289 if (total === null) return 0
290
291 return parseInt(total, 10)
292 })
293 }
294
295 // Returns comulative size of all video files uploaded in the last 24 hours.
296 static getOriginalVideoFileTotalDailyFromUser (user: UserModel) {
297 // Don't use sequelize because we need to use a sub query
298 const query = 'SELECT SUM("size") AS "total" FROM ' +
299 '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
300 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
301 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
302 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
303 'WHERE "account"."userId" = $userId ' +
304 'AND "video"."createdAt" > now() - interval \'24 hours\'' +
305 'GROUP BY "video"."id") t'
275 306
276 const options = { 307 const options = {
277 bind: { userId: user.id }, 308 bind: { userId: user.id },
@@ -303,6 +334,7 @@ export class UserModel extends Model<UserModel> {
303 334
304 toFormattedJSON (): User { 335 toFormattedJSON (): User {
305 const videoQuotaUsed = this.get('videoQuotaUsed') 336 const videoQuotaUsed = this.get('videoQuotaUsed')
337 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
306 338
307 const json = { 339 const json = {
308 id: this.id, 340 id: this.id,
@@ -313,12 +345,18 @@ export class UserModel extends Model<UserModel> {
313 role: this.role, 345 role: this.role,
314 roleLabel: USER_ROLE_LABELS[ this.role ], 346 roleLabel: USER_ROLE_LABELS[ this.role ],
315 videoQuota: this.videoQuota, 347 videoQuota: this.videoQuota,
348 videoQuotaDaily: this.videoQuotaDaily,
316 createdAt: this.createdAt, 349 createdAt: this.createdAt,
317 blocked: this.blocked, 350 blocked: this.blocked,
318 blockedReason: this.blockedReason, 351 blockedReason: this.blockedReason,
319 account: this.Account.toFormattedJSON(), 352 account: this.Account.toFormattedJSON(),
320 videoChannels: [], 353 videoChannels: [],
321 videoQuotaUsed: videoQuotaUsed !== undefined ? parseInt(videoQuotaUsed, 10) : undefined 354 videoQuotaUsed: videoQuotaUsed !== undefined
355 ? parseInt(videoQuotaUsed, 10)
356 : undefined,
357 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
358 ? parseInt(videoQuotaUsedDaily, 10)
359 : undefined
322 } 360 }
323 361
324 if (Array.isArray(this.Account.VideoChannels) === true) { 362 if (Array.isArray(this.Account.VideoChannels) === true) {
@@ -335,12 +373,24 @@ export class UserModel extends Model<UserModel> {
335 return json 373 return json
336 } 374 }
337 375
338 isAbleToUploadVideo (videoFile: { size: number }) { 376 async isAbleToUploadVideo (videoFile: { size: number }) {
339 if (this.videoQuota === -1) return Promise.resolve(true) 377 if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
340 378
341 return UserModel.getOriginalVideoFileTotalFromUser(this) 379 const [ totalBytes, totalBytesDaily ] = await Promise.all([
342 .then(totalBytes => { 380 UserModel.getOriginalVideoFileTotalFromUser(this),
343 return (videoFile.size + totalBytes) < this.videoQuota 381 UserModel.getOriginalVideoFileTotalDailyFromUser(this)
344 }) 382 ])
383
384 const uploadedTotal = videoFile.size + totalBytes
385 const uploadedDaily = videoFile.size + totalBytesDaily
386 if (this.videoQuotaDaily === -1) {
387 return uploadedTotal < this.videoQuota
388 }
389 if (this.videoQuota === -1) {
390 return uploadedDaily < this.videoQuotaDaily
391 }
392
393 return (uploadedTotal < this.videoQuota) &&
394 (uploadedDaily < this.videoQuotaDaily)
345 } 395 }
346} 396}
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index b26dfa252..ecfb76d47 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -48,7 +48,8 @@ describe('Test config API validators', function () {
48 email: 'superadmin1@example.com' 48 email: 'superadmin1@example.com'
49 }, 49 },
50 user: { 50 user: {
51 videoQuota: 5242881 51 videoQuota: 5242881,
52 videoQuotaDaily: 318742
52 }, 53 },
53 transcoding: { 54 transcoding: {
54 enabled: true, 55 enabled: true,
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index b3fb61f6c..8b2ed1b04 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -94,6 +94,7 @@ describe('Test users API validators', function () {
94 email: 'test@example.com', 94 email: 'test@example.com',
95 password: 'my super password', 95 password: 'my super password',
96 videoQuota: -1, 96 videoQuota: -1,
97 videoQuotaDaily: -1,
97 role: UserRole.USER 98 role: UserRole.USER
98 } 99 }
99 100
@@ -173,12 +174,24 @@ describe('Test users API validators', function () {
173 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 174 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
174 }) 175 })
175 176
177 it('Should fail without a videoQuotaDaily', async function () {
178 const fields = omit(baseCorrectParams, 'videoQuotaDaily')
179
180 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
181 })
182
176 it('Should fail with an invalid videoQuota', async function () { 183 it('Should fail with an invalid videoQuota', async function () {
177 const fields = immutableAssign(baseCorrectParams, { videoQuota: -5 }) 184 const fields = immutableAssign(baseCorrectParams, { videoQuota: -5 })
178 185
179 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 186 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
180 }) 187 })
181 188
189 it('Should fail with an invalid videoQuotaDaily', async function () {
190 const fields = immutableAssign(baseCorrectParams, { videoQuotaDaily: -7 })
191
192 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
193 })
194
182 it('Should fail without a user role', async function () { 195 it('Should fail without a user role', async function () {
183 const fields = omit(baseCorrectParams, 'role') 196 const fields = omit(baseCorrectParams, 'role')
184 197
@@ -607,7 +620,7 @@ describe('Test users API validators', function () {
607 }) 620 })
608 621
609 describe('When having a video quota', function () { 622 describe('When having a video quota', function () {
610 it('Should fail with a user having too many video', async function () { 623 it('Should fail with a user having too many videos', async function () {
611 await updateUser({ 624 await updateUser({
612 url: server.url, 625 url: server.url,
613 userId: rootId, 626 userId: rootId,
@@ -618,7 +631,7 @@ describe('Test users API validators', function () {
618 await uploadVideo(server.url, server.accessToken, {}, 403) 631 await uploadVideo(server.url, server.accessToken, {}, 403)
619 }) 632 })
620 633
621 it('Should fail with a registered user having too many video', async function () { 634 it('Should fail with a registered user having too many videos', async function () {
622 this.timeout(30000) 635 this.timeout(30000)
623 636
624 const user = { 637 const user = {
@@ -663,6 +676,45 @@ describe('Test users API validators', function () {
663 }) 676 })
664 }) 677 })
665 678
679 describe('When having a daily video quota', function () {
680 it('Should fail with a user having too many videos', async function () {
681 await updateUser({
682 url: server.url,
683 userId: rootId,
684 accessToken: server.accessToken,
685 videoQuotaDaily: 42
686 })
687
688 await uploadVideo(server.url, server.accessToken, {}, 403)
689 })
690 })
691
692 describe('When having an absolute and daily video quota', function () {
693 it('Should fail if exceeding total quota', async function () {
694 await updateUser({
695 url: server.url,
696 userId: rootId,
697 accessToken: server.accessToken,
698 videoQuota: 42,
699 videoQuotaDaily: 1024 * 1024 * 1024
700 })
701
702 await uploadVideo(server.url, server.accessToken, {}, 403)
703 })
704
705 it('Should fail if exceeding daily quota', async function () {
706 await updateUser({
707 url: server.url,
708 userId: rootId,
709 accessToken: server.accessToken,
710 videoQuota: 1024 * 1024 * 1024,
711 videoQuotaDaily: 42
712 })
713
714 await uploadVideo(server.url, server.accessToken, {}, 403)
715 })
716 })
717
666 describe('When asking a password reset', function () { 718 describe('When asking a password reset', function () {
667 const path = '/api/v1/users/ask-reset-password' 719 const path = '/api/v1/users/ask-reset-password'
668 720
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index f9805b6ea..8a5f27c34 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -37,6 +37,7 @@ function checkInitialConfig (data: CustomConfig) {
37 expect(data.signup.limit).to.equal(4) 37 expect(data.signup.limit).to.equal(4)
38 expect(data.admin.email).to.equal('admin1@example.com') 38 expect(data.admin.email).to.equal('admin1@example.com')
39 expect(data.user.videoQuota).to.equal(5242880) 39 expect(data.user.videoQuota).to.equal(5242880)
40 expect(data.user.videoQuotaDaily).to.equal(318742)
40 expect(data.transcoding.enabled).to.be.false 41 expect(data.transcoding.enabled).to.be.false
41 expect(data.transcoding.threads).to.equal(2) 42 expect(data.transcoding.threads).to.equal(2)
42 expect(data.transcoding.resolutions['240p']).to.be.true 43 expect(data.transcoding.resolutions['240p']).to.be.true
@@ -65,6 +66,7 @@ function checkUpdatedConfig (data: CustomConfig) {
65 expect(data.signup.limit).to.equal(5) 66 expect(data.signup.limit).to.equal(5)
66 expect(data.admin.email).to.equal('superadmin1@example.com') 67 expect(data.admin.email).to.equal('superadmin1@example.com')
67 expect(data.user.videoQuota).to.equal(5242881) 68 expect(data.user.videoQuota).to.equal(5242881)
69 expect(data.user.videoQuotaDaily).to.equal(318742)
68 expect(data.transcoding.enabled).to.be.true 70 expect(data.transcoding.enabled).to.be.true
69 expect(data.transcoding.threads).to.equal(1) 71 expect(data.transcoding.threads).to.equal(1)
70 expect(data.transcoding.resolutions['240p']).to.be.false 72 expect(data.transcoding.resolutions['240p']).to.be.false
@@ -152,7 +154,8 @@ describe('Test config', function () {
152 email: 'superadmin1@example.com' 154 email: 'superadmin1@example.com'
153 }, 155 },
154 user: { 156 user: {
155 videoQuota: 5242881 157 videoQuota: 5242881,
158 videoQuotaDaily: 318742
156 }, 159 },
157 transcoding: { 160 transcoding: {
158 enabled: true, 161 enabled: true,
diff --git a/server/tests/utils/server/config.ts b/server/tests/utils/server/config.ts
index d6ac3ef8a..799c31ae5 100644
--- a/server/tests/utils/server/config.ts
+++ b/server/tests/utils/server/config.ts
@@ -80,7 +80,8 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
80 email: 'superadmin1@example.com' 80 email: 'superadmin1@example.com'
81 }, 81 },
82 user: { 82 user: {
83 videoQuota: 5242881 83 videoQuota: 5242881,
84 videoQuotaDaily: 318742
84 }, 85 },
85 transcoding: { 86 transcoding: {
86 enabled: true, 87 enabled: true,
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts
index f786de6e3..5dba34b69 100644
--- a/server/tests/utils/users/users.ts
+++ b/server/tests/utils/users/users.ts
@@ -10,6 +10,7 @@ function createUser (
10 username: string, 10 username: string,
11 password: string, 11 password: string,
12 videoQuota = 1000000, 12 videoQuota = 1000000,
13 videoQuotaDaily = -1,
13 role: UserRole = UserRole.USER, 14 role: UserRole = UserRole.USER,
14 specialStatus = 200 15 specialStatus = 200
15) { 16) {
@@ -19,7 +20,8 @@ function createUser (
19 password, 20 password,
20 role, 21 role,
21 email: username + '@example.com', 22 email: username + '@example.com',
22 videoQuota 23 videoQuota,
24 videoQuotaDaily
23 } 25 }
24 26
25 return request(url) 27 return request(url)
@@ -202,6 +204,7 @@ function updateUser (options: {
202 accessToken: string, 204 accessToken: string,
203 email?: string, 205 email?: string,
204 videoQuota?: number, 206 videoQuota?: number,
207 videoQuotaDaily?: number,
205 role?: UserRole 208 role?: UserRole
206}) { 209}) {
207 const path = '/api/v1/users/' + options.userId 210 const path = '/api/v1/users/' + options.userId
@@ -209,6 +212,7 @@ function updateUser (options: {
209 const toSend = {} 212 const toSend = {}
210 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email 213 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
211 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota 214 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
215 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
212 if (options.role !== undefined && options.role !== null) toSend['role'] = options.role 216 if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
213 217
214 return makePutBodyRequest({ 218 return makePutBodyRequest({
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index d70c757b6..2f5cebf7f 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -42,6 +42,7 @@ export interface CustomConfig {
42 42
43 user: { 43 user: {
44 videoQuota: number 44 videoQuota: number
45 videoQuotaDaily: number
45 } 46 }
46 47
47 transcoding: { 48 transcoding: {
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 8cb087234..9bbeb14d2 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -66,5 +66,6 @@ export interface ServerConfig {
66 66
67 user: { 67 user: {
68 videoQuota: number 68 videoQuota: number
69 videoQuotaDaily: number
69 } 70 }
70} 71}
diff --git a/shared/models/users/user-create.model.ts b/shared/models/users/user-create.model.ts
index 65830f55e..08be4db05 100644
--- a/shared/models/users/user-create.model.ts
+++ b/shared/models/users/user-create.model.ts
@@ -5,5 +5,6 @@ export interface UserCreate {
5 password: string 5 password: string
6 email: string 6 email: string
7 videoQuota: number 7 videoQuota: number
8 videoQuotaDaily: number
8 role: UserRole 9 role: UserRole
9} 10}
diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts
index 96b454b7c..ce866fb18 100644
--- a/shared/models/users/user-update.model.ts
+++ b/shared/models/users/user-update.model.ts
@@ -3,5 +3,6 @@ import { UserRole } from './user-role'
3export interface UserUpdate { 3export interface UserUpdate {
4 email?: string 4 email?: string
5 videoQuota?: number 5 videoQuota?: number
6 videoQuotaDaily?: number
6 role?: UserRole 7 role?: UserRole
7} 8}
diff --git a/shared/models/users/user-video-quota.model.ts b/shared/models/users/user-video-quota.model.ts
index b856fd9fc..a24871d71 100644
--- a/shared/models/users/user-video-quota.model.ts
+++ b/shared/models/users/user-video-quota.model.ts
@@ -1,3 +1,4 @@
1export interface UserVideoQuota { 1export interface UserVideoQuota {
2 videoQuotaUsed: number 2 videoQuotaUsed: number
3 videoQuotaUsedDaily: number
3} 4}
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts
index 455211aa3..8147dc48e 100644
--- a/shared/models/users/user.model.ts
+++ b/shared/models/users/user.model.ts
@@ -11,6 +11,7 @@ export interface User {
11 autoPlayVideo: boolean 11 autoPlayVideo: boolean
12 role: UserRole 12 role: UserRole
13 videoQuota: number 13 videoQuota: number
14 videoQuotaDaily: number
14 createdAt: Date 15 createdAt: Date
15 account: Account 16 account: Account
16 videoChannels?: VideoChannel[] 17 videoChannels?: VideoChannel[]