diff options
author | Felix Ableitner <me@nutomic.com> | 2018-08-28 02:01:35 -0500 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-08-28 09:01:35 +0200 |
commit | bee0abffff73804d816b90c7fd599e0a51c09d61 (patch) | |
tree | fae6d58637f9c63a3800090277f8e130b43442dd | |
parent | c907c2fa3fd7c0a741117a0204d0ebca675124bd (diff) | |
download | PeerTube-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
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 | }) |
17 | export class EditCustomConfigComponent extends FormReactive implements OnInit { | 17 | export 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 @@ | |||
1 | import { ServerService } from '../../../core' | 1 | import { ServerService } from '../../../core' |
2 | import { FormReactive } from '../../../shared' | 2 | import { FormReactive } from '../../../shared' |
3 | import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' | 3 | import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' |
4 | import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/' | ||
4 | 5 | ||
5 | export abstract class UserEdit extends FormReactive { | 6 | export 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 | ||
18 | function isUserVideoQuotaDailyValid (value: string) { | ||
19 | return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA_DAILY) | ||
20 | } | ||
21 | |||
18 | function isUserUsernameValid (value: string) { | 22 | function 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { CONSTRAINTS_FIELDS } from '../constants' | ||
3 | |||
4 | async 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 | |||
19 | function down (options) { | ||
20 | throw new Error('Not implemented.') | ||
21 | } | ||
22 | |||
23 | export { 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' |
17 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 18 | import { isVideoExist } from '../../helpers/custom-validators/videos' |
18 | import { logger } from '../../helpers/logger' | 19 | import { 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' |
32 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 33 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
33 | import { OAuthTokenModel } from '../oauth/oauth-token' | 34 | import { 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' | |||
3 | export interface UserUpdate { | 3 | export 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 @@ | |||
1 | export interface UserVideoQuota { | 1 | export 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[] |