diff options
author | Chocobozzz <me@florianbigard.com> | 2022-05-04 10:07:06 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-05-04 10:32:49 +0200 |
commit | 9a82ce2455874a80167f5ae30bb19ea12eb0551e (patch) | |
tree | 0b4fe8ff0bfd00d0f3b60e047ec9d7cfdd7c0c7a | |
parent | b003d5751803e737b34dc71ce61e66a0d9d12eb4 (diff) | |
download | PeerTube-9a82ce2455874a80167f5ae30bb19ea12eb0551e.tar.gz PeerTube-9a82ce2455874a80167f5ae30bb19ea12eb0551e.tar.zst PeerTube-9a82ce2455874a80167f5ae30bb19ea12eb0551e.zip |
Fix quota inconstistencies with lives
-rw-r--r-- | server/lib/live/live-manager.ts | 2 | ||||
-rw-r--r-- | server/lib/user.ts | 5 | ||||
-rw-r--r-- | server/models/user/user.ts | 61 | ||||
-rw-r--r-- | server/tests/api/live/live-constraints.ts | 25 |
4 files changed, 67 insertions, 26 deletions
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index df2804a0e..e04ae9fef 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts | |||
@@ -331,6 +331,8 @@ class LiveManager { | |||
331 | muxingSession.on('after-cleanup', ({ videoId }) => { | 331 | muxingSession.on('after-cleanup', ({ videoId }) => { |
332 | this.muxingSessions.delete(sessionId) | 332 | this.muxingSessions.delete(sessionId) |
333 | 333 | ||
334 | LiveQuotaStore.Instance.removeLive(user.id, videoLive.id) | ||
335 | |||
334 | muxingSession.destroy() | 336 | muxingSession.destroy() |
335 | 337 | ||
336 | return this.onAfterMuxingCleanup({ videoId, liveSession }) | 338 | return this.onAfterMuxingCleanup({ videoId, liveSession }) |
diff --git a/server/lib/user.ts b/server/lib/user.ts index 310a3c30c..586fd0f12 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -173,7 +173,8 @@ async function getOriginalVideoFileTotalFromUser (user: MUserId) { | |||
173 | // Don't use sequelize because we need to use a sub query | 173 | // Don't use sequelize because we need to use a sub query |
174 | const query = UserModel.generateUserQuotaBaseSQL({ | 174 | const query = UserModel.generateUserQuotaBaseSQL({ |
175 | withSelect: true, | 175 | withSelect: true, |
176 | whereUserId: '$userId' | 176 | whereUserId: '$userId', |
177 | daily: false | ||
177 | }) | 178 | }) |
178 | 179 | ||
179 | const base = await UserModel.getTotalRawQuery(query, user.id) | 180 | const base = await UserModel.getTotalRawQuery(query, user.id) |
@@ -187,7 +188,7 @@ async function getOriginalVideoFileTotalDailyFromUser (user: MUserId) { | |||
187 | const query = UserModel.generateUserQuotaBaseSQL({ | 188 | const query = UserModel.generateUserQuotaBaseSQL({ |
188 | withSelect: true, | 189 | withSelect: true, |
189 | whereUserId: '$userId', | 190 | whereUserId: '$userId', |
190 | where: '"video"."createdAt" > now() - interval \'24 hours\'' | 191 | daily: true |
191 | }) | 192 | }) |
192 | 193 | ||
193 | const base = await UserModel.getTotalRawQuery(query, user.id) | 194 | const base = await UserModel.getTotalRawQuery(query, user.id) |
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index bcf56dfa1..85720abfb 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -72,10 +72,13 @@ import { VideoImportModel } from '../video/video-import' | |||
72 | import { VideoLiveModel } from '../video/video-live' | 72 | import { VideoLiveModel } from '../video/video-live' |
73 | import { VideoPlaylistModel } from '../video/video-playlist' | 73 | import { VideoPlaylistModel } from '../video/video-playlist' |
74 | import { UserNotificationSettingModel } from './user-notification-setting' | 74 | import { UserNotificationSettingModel } from './user-notification-setting' |
75 | import { LiveQuotaStore } from '@server/lib/live' | ||
76 | import { logger } from '@server/helpers/logger' | ||
75 | 77 | ||
76 | enum ScopeNames { | 78 | enum ScopeNames { |
77 | FOR_ME_API = 'FOR_ME_API', | 79 | FOR_ME_API = 'FOR_ME_API', |
78 | WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS', | 80 | WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS', |
81 | WITH_QUOTA = 'WITH_QUOTA', | ||
79 | WITH_STATS = 'WITH_STATS' | 82 | WITH_STATS = 'WITH_STATS' |
80 | } | 83 | } |
81 | 84 | ||
@@ -153,7 +156,7 @@ enum ScopeNames { | |||
153 | } | 156 | } |
154 | ] | 157 | ] |
155 | }, | 158 | }, |
156 | [ScopeNames.WITH_STATS]: { | 159 | [ScopeNames.WITH_QUOTA]: { |
157 | attributes: { | 160 | attributes: { |
158 | include: [ | 161 | include: [ |
159 | [ | 162 | [ |
@@ -161,7 +164,8 @@ enum ScopeNames { | |||
161 | '(' + | 164 | '(' + |
162 | UserModel.generateUserQuotaBaseSQL({ | 165 | UserModel.generateUserQuotaBaseSQL({ |
163 | withSelect: false, | 166 | withSelect: false, |
164 | whereUserId: '"UserModel"."id"' | 167 | whereUserId: '"UserModel"."id"', |
168 | daily: false | ||
165 | }) + | 169 | }) + |
166 | ')' | 170 | ')' |
167 | ), | 171 | ), |
@@ -170,6 +174,24 @@ enum ScopeNames { | |||
170 | [ | 174 | [ |
171 | literal( | 175 | literal( |
172 | '(' + | 176 | '(' + |
177 | UserModel.generateUserQuotaBaseSQL({ | ||
178 | withSelect: false, | ||
179 | whereUserId: '"UserModel"."id"', | ||
180 | daily: true | ||
181 | }) + | ||
182 | ')' | ||
183 | ), | ||
184 | 'videoQuotaUsedDaily' | ||
185 | ] | ||
186 | ] | ||
187 | } | ||
188 | }, | ||
189 | [ScopeNames.WITH_STATS]: { | ||
190 | attributes: { | ||
191 | include: [ | ||
192 | [ | ||
193 | literal( | ||
194 | '(' + | ||
173 | 'SELECT COUNT("video"."id") ' + | 195 | 'SELECT COUNT("video"."id") ' + |
174 | 'FROM "video" ' + | 196 | 'FROM "video" ' + |
175 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | 197 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + |
@@ -474,21 +496,6 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
474 | } | 496 | } |
475 | 497 | ||
476 | const query: FindOptions = { | 498 | const query: FindOptions = { |
477 | attributes: { | ||
478 | include: [ | ||
479 | [ | ||
480 | literal( | ||
481 | '(' + | ||
482 | UserModel.generateUserQuotaBaseSQL({ | ||
483 | withSelect: false, | ||
484 | whereUserId: '"UserModel"."id"' | ||
485 | }) + | ||
486 | ')' | ||
487 | ), | ||
488 | 'videoQuotaUsed' | ||
489 | ] | ||
490 | ] | ||
491 | }, | ||
492 | offset: start, | 499 | offset: start, |
493 | limit: count, | 500 | limit: count, |
494 | order: getSort(sort), | 501 | order: getSort(sort), |
@@ -497,7 +504,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
497 | 504 | ||
498 | return Promise.all([ | 505 | return Promise.all([ |
499 | UserModel.unscoped().count(query), | 506 | UserModel.unscoped().count(query), |
500 | UserModel.findAll(query) | 507 | UserModel.scope([ 'defaultScope', ScopeNames.WITH_QUOTA ]).findAll(query) |
501 | ]).then(([ total, data ]) => ({ total, data })) | 508 | ]).then(([ total, data ]) => ({ total, data })) |
502 | } | 509 | } |
503 | 510 | ||
@@ -579,7 +586,10 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
579 | ScopeNames.WITH_VIDEOCHANNELS | 586 | ScopeNames.WITH_VIDEOCHANNELS |
580 | ] | 587 | ] |
581 | 588 | ||
582 | if (withStats) scopes.push(ScopeNames.WITH_STATS) | 589 | if (withStats) { |
590 | scopes.push(ScopeNames.WITH_QUOTA) | ||
591 | scopes.push(ScopeNames.WITH_STATS) | ||
592 | } | ||
583 | 593 | ||
584 | return UserModel.scope(scopes).findByPk(id) | 594 | return UserModel.scope(scopes).findByPk(id) |
585 | } | 595 | } |
@@ -760,10 +770,10 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
760 | static generateUserQuotaBaseSQL (options: { | 770 | static generateUserQuotaBaseSQL (options: { |
761 | whereUserId: '$userId' | '"UserModel"."id"' | 771 | whereUserId: '$userId' | '"UserModel"."id"' |
762 | withSelect: boolean | 772 | withSelect: boolean |
763 | where?: string | 773 | daily: boolean |
764 | }) { | 774 | }) { |
765 | const andWhere = options.where | 775 | const andWhere = options.daily === true |
766 | ? 'AND ' + options.where | 776 | ? 'AND "video"."createdAt" > now() - interval \'24 hours\'' |
767 | : '' | 777 | : '' |
768 | 778 | ||
769 | const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | 779 | const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + |
@@ -904,12 +914,15 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { | |||
904 | 914 | ||
905 | videoQuota: this.videoQuota, | 915 | videoQuota: this.videoQuota, |
906 | videoQuotaDaily: this.videoQuotaDaily, | 916 | videoQuotaDaily: this.videoQuotaDaily, |
917 | |||
907 | videoQuotaUsed: videoQuotaUsed !== undefined | 918 | videoQuotaUsed: videoQuotaUsed !== undefined |
908 | ? parseInt(videoQuotaUsed + '', 10) | 919 | ? parseInt(videoQuotaUsed + '', 10) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id) |
909 | : undefined, | 920 | : undefined, |
921 | |||
910 | videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined | 922 | videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined |
911 | ? parseInt(videoQuotaUsedDaily + '', 10) | 923 | ? parseInt(videoQuotaUsedDaily + '', 10) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id) |
912 | : undefined, | 924 | : undefined, |
925 | |||
913 | videosCount: videosCount !== undefined | 926 | videosCount: videosCount !== undefined |
914 | ? parseInt(videosCount + '', 10) | 927 | ? parseInt(videosCount + '', 10) |
915 | : undefined, | 928 | : undefined, |
diff --git a/server/tests/api/live/live-constraints.ts b/server/tests/api/live/live-constraints.ts index cab76f425..cf43c262a 100644 --- a/server/tests/api/live/live-constraints.ts +++ b/server/tests/api/live/live-constraints.ts | |||
@@ -12,6 +12,7 @@ import { | |||
12 | PeerTubeServer, | 12 | PeerTubeServer, |
13 | setAccessTokensToServers, | 13 | setAccessTokensToServers, |
14 | setDefaultVideoChannel, | 14 | setDefaultVideoChannel, |
15 | stopFfmpeg, | ||
15 | waitJobs, | 16 | waitJobs, |
16 | waitUntilLiveReplacedByReplayOnAllServers, | 17 | waitUntilLiveReplacedByReplayOnAllServers, |
17 | waitUntilLiveWaitingOnAllServers | 18 | waitUntilLiveWaitingOnAllServers |
@@ -169,6 +170,30 @@ describe('Test live constraints', function () { | |||
169 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false }) | 170 | await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false }) |
170 | }) | 171 | }) |
171 | 172 | ||
173 | it('Should have the same quota in admin and as a user', async function () { | ||
174 | this.timeout(120000) | ||
175 | |||
176 | const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false }) | ||
177 | const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ token: userAccessToken, videoId: userVideoLiveoId }) | ||
178 | |||
179 | await servers[0].live.waitUntilPublished({ videoId: userVideoLiveoId }) | ||
180 | |||
181 | await wait(3000) | ||
182 | |||
183 | const quotaUser = await servers[0].users.getMyQuotaUsed({ token: userAccessToken }) | ||
184 | |||
185 | const { data } = await servers[0].users.list() | ||
186 | const quotaAdmin = data.find(u => u.username === 'user1') | ||
187 | |||
188 | expect(quotaUser.videoQuotaUsed).to.equal(quotaAdmin.videoQuotaUsed) | ||
189 | expect(quotaUser.videoQuotaUsedDaily).to.equal(quotaAdmin.videoQuotaUsedDaily) | ||
190 | |||
191 | expect(quotaUser.videoQuotaUsed).to.be.above(10) | ||
192 | expect(quotaUser.videoQuotaUsedDaily).to.be.above(10) | ||
193 | |||
194 | await stopFfmpeg(ffmpegCommand) | ||
195 | }) | ||
196 | |||
172 | it('Should have max duration limit', async function () { | 197 | it('Should have max duration limit', async function () { |
173 | this.timeout(60000) | 198 | this.timeout(60000) |
174 | 199 | ||