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 77aa613a1..a69ffee77 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 @@ -64,7 +64,7 @@
Transcoding is enabled on server. The video quota only take in account original video.
- In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}. + In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
diff --git a/client/src/app/account/account-settings/account-settings.component.html b/client/src/app/account/account-settings/account-settings.component.html index 0d1637c40..2f41b5ecf 100644 --- a/client/src/app/account/account-settings/account-settings.component.html +++ b/client/src/app/account/account-settings/account-settings.component.html @@ -13,8 +13,12 @@
(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})
+
+ Video quota: {{ userVideoQuotaUsed | bytes: 0 }} / {{ user.videoQuota | bytes: 0 }} +
+
Account settings
-
Videos
+
Video settings
diff --git a/client/src/app/account/account-settings/account-settings.component.scss b/client/src/app/account/account-settings/account-settings.component.scss index fbd1cb9f0..aaf9d79f0 100644 --- a/client/src/app/account/account-settings/account-settings.component.scss +++ b/client/src/app/account/account-settings/account-settings.component.scss @@ -36,6 +36,15 @@ top: -10px; } +.user-quota { + font-size: 15px; + margin-top: 20px; + + .user-quota-label { + font-weight: $font-semibold; + } +} + .account-title { text-transform: uppercase; color: $orange-color; diff --git a/client/src/app/account/account-settings/account-settings.component.ts b/client/src/app/account/account-settings/account-settings.component.ts index d5f5ff30f..a375072a0 100644 --- a/client/src/app/account/account-settings/account-settings.component.ts +++ b/client/src/app/account/account-settings/account-settings.component.ts @@ -14,6 +14,7 @@ export class AccountSettingsComponent implements OnInit { @ViewChild('avatarfileInput') avatarfileInput user: User = null + userVideoQuotaUsed = 0 constructor ( private userService: UserService, @@ -24,6 +25,9 @@ export class AccountSettingsComponent implements OnInit { ngOnInit () { this.user = this.authService.getUser() + + this.userService.getMyVideoQuotaUsed() + .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed) } getAvatarUrl () { diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 8a2ba77d6..8700e8c74 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -14,7 +14,6 @@ import { User } from '../../../../../shared/models/users' import { UserLogin } from '../../../../../shared/models/users/user-login.model' import { environment } from '../../../environments/environment' import { RestExtractor } from '../../shared/rest' -import { UserConstructorHash } from '../../shared/users/user.model' import { AuthStatus } from './auth-status.model' import { AuthUser } from './auth-user.model' @@ -178,12 +177,7 @@ export class AuthService { this.mergeUserInformation(obj) .subscribe( res => { - this.user.displayNSFW = res.displayNSFW - this.user.autoPlayVideo = res.autoPlayVideo - this.user.role = res.role - this.user.videoChannels = res.videoChannels - this.user.account = res.account - + this.user.patch(res) this.user.save() this.userInformationLoaded.next(true) @@ -200,24 +194,13 @@ export class AuthService { } private handleLogin (obj: UserLoginWithUserInformation) { - const hashUser: UserConstructorHash = { - id: obj.id, - username: obj.username, - role: obj.role, - email: obj.email, - displayNSFW: obj.displayNSFW, - autoPlayVideo: obj.autoPlayVideo, - videoQuota: obj.videoQuota, - videoChannels: obj.videoChannels, - account: obj.account - } const hashTokens = { accessToken: obj.access_token, tokenType: obj.token_type, refreshToken: obj.refresh_token } - this.user = new AuthUser(hashUser, hashTokens) + this.user = new AuthUser(obj, hashTokens) this.user.save() this.setStatus(AuthStatus.LoggedIn) diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 83aae4463..4a94b032d 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts @@ -60,4 +60,10 @@ export class User implements UserServerModel { getAvatarUrl () { return Account.GET_ACCOUNT_AVATAR_URL(this.account) } + + patch (obj: UserServerModel) { + for (const key of Object.keys(obj)) { + this[key] = obj[key] + } + } } diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index 58ddaa5ee..742fb0728 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts @@ -48,11 +48,10 @@ export class UserService { .catch(res => this.restExtractor.handleError(res)) } - getMyInformation () { - const url = UserService.BASE_USERS_URL + 'me' + getMyVideoQuotaUsed () { + const url = UserService.BASE_USERS_URL + '/me/video-quota-used' return this.authHttp.get(url) - .map((userHash: any) => new User(userHash)) .catch(res => this.restExtractor.handleError(res)) } } diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html index 193cc55ee..2040ff9d4 100644 --- a/client/src/app/videos/+video-edit/video-add.component.html +++ b/client/src/app/videos/+video-edit/video-add.component.html @@ -3,8 +3,6 @@ Upload your video -
{{ error }}
-
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 066f945fc..a86d9d3c2 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts @@ -2,7 +2,9 @@ import { HttpEventType, HttpResponse } from '@angular/common/http' import { Component, OnInit, ViewChild } from '@angular/core' import { FormBuilder, FormGroup } from '@angular/forms' import { Router } from '@angular/router' +import { UserService } from '@app/shared' import { NotificationsService } from 'angular2-notifications' +import { BytesPipe } from 'ngx-pipes' import { VideoPrivacy } from '../../../../../shared/models/videos' import { AuthService, ServerService } from '../../core' import { FormReactive } from '../../shared' @@ -31,12 +33,12 @@ export class VideoAddComponent extends FormReactive implements OnInit { uuid: '' } - error: string = null form: FormGroup formErrors: { [ id: string ]: string } = {} validationMessages: ValidatorMessage = {} userVideoChannels = [] + userVideoQuotaUsed = 0 videoPrivacies = [] firstStepPrivacyId = 0 firstStepChannelId = 0 @@ -46,6 +48,7 @@ export class VideoAddComponent extends FormReactive implements OnInit { private router: Router, private notificationsService: NotificationsService, private authService: AuthService, + private userService: UserService, private serverService: ServerService, private videoService: VideoService ) { @@ -67,6 +70,9 @@ export class VideoAddComponent extends FormReactive implements OnInit { populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) .then(() => this.firstStepChannelId = this.userVideoChannels[0].id) + this.userService.getMyVideoQuotaUsed() + .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed) + this.serverService.videoPrivaciesLoaded .subscribe( () => { @@ -89,6 +95,18 @@ export class VideoAddComponent extends FormReactive implements OnInit { uploadFirstStep () { const videofile = this.videofileInput.nativeElement.files[0] + const videoQuota = this.authService.getUser().videoQuota + if ((this.userVideoQuotaUsed + videofile.size) > videoQuota) { + const bytePipes = new BytesPipe() + + const msg = 'Your video quota is exceeded with this video ' + + `(video size: ${bytePipes.transform(videofile.size, 0)}, ` + + `used: ${bytePipes.transform(this.userVideoQuotaUsed, 0)}, ` + + `quota: ${bytePipes.transform(videoQuota, 0)})` + this.notificationsService.error('Error', msg) + return + } + const name = videofile.name.replace(/\.[^/.]+$/, '') const privacy = this.firstStepPrivacyId.toString() const nsfw = false @@ -127,8 +145,9 @@ export class VideoAddComponent extends FormReactive implements OnInit { err => { // Reset progress + this.isUploadingVideo = false this.videoUploadPercents = 0 - this.error = err.message + this.notificationsService.error('Error', err.message) } ) } @@ -152,7 +171,7 @@ export class VideoAddComponent extends FormReactive implements OnInit { }, err => { - this.error = 'Cannot update the video.' + this.notificationsService.error('Error', err.message) console.error(err) } ) diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index 941ef2478..7f41b56d8 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts @@ -21,7 +21,6 @@ import { VideoService } from '../../shared/video/video.service' export class VideoUpdateComponent extends FormReactive implements OnInit { video: VideoEdit - error: string = null form: FormGroup formErrors: { [ id: string ]: string } = {} validationMessages: ValidatorMessage = {} @@ -82,7 +81,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { err => { console.error(err) - this.error = 'Cannot fetch video.' + this.notificationsService.error('Error', err.message) } ) } @@ -108,7 +107,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { }, err => { - this.error = 'Cannot update the video.' + this.notificationsService.error('Error', err.message) console.error(err) } ) diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 2d77a5249..5374c4b6a 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -30,6 +30,11 @@ usersRouter.get('/me', asyncMiddleware(getUserInformation) ) +usersRouter.get('/me/video-quota-used', + authenticate, + asyncMiddleware(getUserVideoQuotaUsed) +) + usersRouter.get('/me/videos', authenticate, paginationValidator, @@ -183,8 +188,18 @@ async function getUserInformation (req: express.Request, res: express.Response, return res.json(user.toFormattedJSON()) } +async function getUserVideoQuotaUsed (req: express.Request, res: express.Response, next: express.NextFunction) { + // We did not load channels in res.locals.user + const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) + const videoQuotaUsed = await UserModel.getOriginalVideoFileTotalFromUser(user) + + return res.json({ + videoQuotaUsed + }) +} + function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { - return res.json(res.locals.user.toFormattedJSON()) + return res.json((res.locals.user as UserModel).toFormattedJSON()) } async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) { diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 4226bcb35..e37fd4d3b 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -181,7 +181,7 @@ export class UserModel extends Model { return UserModel.findOne(query) } - private static getOriginalVideoFileTotalFromUser (user: UserModel) { + static getOriginalVideoFileTotalFromUser (user: UserModel) { // Don't use sequelize because we need to use a sub query const query = 'SELECT SUM("size") AS "total" FROM ' + '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index f7e5972d3..b788637e7 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -4,7 +4,8 @@ import * as chai from 'chai' import 'mocha' import { UserRole } from '../../../../shared/index' import { - createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoRating, getUserInformation, getUsersList, + createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating, getUserInformation, + getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo, runServer, ServerInfo, serverLogin, testVideoImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo } from '../../utils/index' @@ -179,11 +180,19 @@ describe('Test users', function () { this.timeout(5000) const videoAttributes = { - name: 'super user video' + name: 'super user video', + fixture: 'video_short.webm' } await uploadVideo(server.url, accessTokenUser, videoAttributes) }) + it('Should have video quota updated', async function () { + const res = await getMyUserVideoQuotaUsed(server.url, accessTokenUser) + const data = res.body + + expect(data.videoQuotaUsed).to.equal(218910) + }) + it('Should be able to list my videos', async function () { const res = await getMyVideos(server.url, accessTokenUser, 0, 5) expect(res.body.total).to.equal(1) diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index 90b1ca0a6..12945a805 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts @@ -56,6 +56,17 @@ function getMyUserInformation (url: string, accessToken: string, specialStatus = .expect('Content-Type', /json/) } +function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) { + const path = '/api/v1/users/me/video-quota-used' + + return request(url) + .get(path) + .set('Accept', 'application/json') + .set('Authorization', 'Bearer ' + accessToken) + .expect(specialStatus) + .expect('Content-Type', /json/) +} + function getUserInformation (url: string, accessToken: string, userId: number) { const path = '/api/v1/users/' + userId @@ -192,6 +203,7 @@ export { registerUser, getMyUserInformation, getMyUserVideoRating, + getMyUserVideoQuotaUsed, getUsersList, getUsersListPaginationAndSort, removeUser,