chore: new feture for quato

This commit is contained in:
mahyargdz
2025-07-07 16:21:30 +03:30
parent 6565d2ae8b
commit b66114c900
21 changed files with 815 additions and 98 deletions
@@ -0,0 +1,39 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
export class QuotaStatisticsDto {
@ApiProperty({ description: "Total number of email users", example: 150 })
totalUsers: number;
@ApiProperty({ description: "Total quota used by all users in bytes", example: 5368709120 })
totalQuotaUsed: number;
@ApiProperty({ description: "Total quota allowed for all users in bytes", example: 21474836480 })
totalQuotaAllowed: number;
@ApiProperty({ description: "Average quota usage percentage", example: 25.0 })
averageUsagePercent: number;
@ApiPropertyOptional({
description: "Last time quota was synced",
example: "2024-01-15T10:30:00Z",
type: String,
format: "date-time",
})
lastSyncTime: Date | null;
}
export class QuotaInfoDto {
@ApiProperty({ description: "Quota used in bytes", example: 1073741824 })
used: number;
@ApiProperty({ description: "Quota allowed in bytes", example: 5368709120 })
allowed: number;
}
export class SyncResultDto {
@ApiProperty({ description: "Number of successfully synced users", example: 145 })
success: number;
@ApiProperty({ description: "Number of users with sync errors", example: 5 })
errors: number;
}
@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { MailServerModule } from "../mail-server/mail-server.module";
import { QuotaSyncService } from "./services/quota-sync.service";
@Module({
imports: [MailServerModule],
providers: [QuotaSyncService],
exports: [QuotaSyncService],
})
export class QuotaSyncModule {}
@@ -0,0 +1,206 @@
import { EntityManager } from "@mikro-orm/core";
import { Injectable, Logger } from "@nestjs/common";
import { Cron, CronExpression } from "@nestjs/schedule";
import { firstValueFrom } from "rxjs";
import { WildDuckUsersService } from "../../mail-server/services/wildduck-users.service";
import { User } from "../../users/entities/user.entity";
interface QuotaInfo {
used: number;
allowed: number;
}
@Injectable()
export class QuotaSyncService {
private readonly logger = new Logger(QuotaSyncService.name);
constructor(
private readonly wildDuckUsersService: WildDuckUsersService,
private readonly em: EntityManager,
) {}
/**
* Sync quota usage every 15 minutes
*/
@Cron(CronExpression.EVERY_2_HOURS)
// @Cron("0 */15 * * * *")
async syncAllUsersQuota(): Promise<void> {
const em = this.em.fork();
this.logger.log("Starting quota synchronization for all users");
try {
const users = await em.find(User, {
emailEnabled: true,
deletedAt: null,
wildduckUserId: { $ne: null },
});
this.logger.log(`Found ${users.length} users to sync quota for`);
let successCount = 0;
let errorCount = 0;
for (const user of users) {
try {
await this.syncUserQuotaWithEntityManager(user, em);
successCount++;
} catch (error) {
errorCount++;
this.logger.error(`Failed to sync quota for user ${user.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
// Flush all changes at once
await em.flush();
this.logger.log(`Quota sync completed: ${successCount} success, ${errorCount} errors`);
} catch (error) {
this.logger.error("Failed to sync quota for users", `${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Sync quota for a specific user with provided entity manager
*/
private async syncUserQuotaWithEntityManager(user: User, _em: EntityManager): Promise<void> {
if (!user.wildduckUserId) {
this.logger.warn(`User ${user.id} has no WildDuck user ID, skipping quota sync`);
return;
}
try {
const wildduckUser = await firstValueFrom(this.wildDuckUsersService.getUser(user.wildduckUserId));
if (!wildduckUser || !wildduckUser.success) {
this.logger.warn(`Failed to fetch WildDuck user data for user ${user.id}`);
return;
}
const quotaInfo: QuotaInfo = {
used: wildduckUser.limits?.quota?.used || 0,
allowed: wildduckUser.limits?.quota?.allowed || 0,
};
// Update local user quota info
user.emailQuotaUsed = quotaInfo.used;
user.emailQuota = quotaInfo.allowed;
user.lastQuotaSync = new Date();
this.logger.debug(`Synced quota for user ${user.id}: ${quotaInfo.used}/${quotaInfo.allowed} bytes`);
} catch (error) {
this.logger.error(`Error syncing quota for user ${user.id}:`, `${error instanceof Error ? error.message : "Unknown error"}`);
throw error;
}
}
/**
* Sync quota for a specific user
*/
async syncUserQuota(user: User): Promise<void> {
const em = this.em.fork();
// Re-fetch the user with the current entity manager
const managedUser = await em.findOne(User, { id: user.id });
if (!managedUser) {
throw new Error(`User ${user.id} not found`);
}
await this.syncUserQuotaWithEntityManager(managedUser, em);
await em.flush();
}
/**
* Manually trigger quota sync for a specific user
*/
async syncUserQuotaById(userId: string): Promise<QuotaInfo> {
const em = this.em.fork();
const user = await em.findOne(User, {
id: userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
throw new Error(`User ${userId} not found or not email enabled`);
}
await this.syncUserQuotaWithEntityManager(user, em);
await em.flush();
return {
used: user.emailQuotaUsed || 0,
allowed: user.emailQuota || 0,
};
}
/**
* Get quota statistics for all users
*/
async getQuotaStatistics(): Promise<{
totalUsers: number;
totalQuotaUsed: number;
totalQuotaAllowed: number;
averageUsagePercent: number;
lastSyncTime: Date | null;
}> {
const em = this.em.fork();
const users = await em.find(User, {
emailEnabled: true,
deletedAt: null,
emailQuotaUsed: { $ne: null },
});
const totalQuotaUsed = users.reduce((sum, user) => sum + (user.emailQuotaUsed || 0), 0);
const totalQuotaAllowed = users.reduce((sum, user) => sum + (user.emailQuota || 0), 0);
const averageUsagePercent = totalQuotaAllowed > 0 ? (totalQuotaUsed / totalQuotaAllowed) * 100 : 0;
// Find the most recent sync time
const lastSyncUser = await em.findOne(
User,
{
emailEnabled: true,
deletedAt: null,
lastQuotaSync: { $ne: null },
},
{
orderBy: { lastQuotaSync: "DESC" },
},
);
return {
totalUsers: users.length,
totalQuotaUsed,
totalQuotaAllowed,
averageUsagePercent: Math.round(averageUsagePercent * 100) / 100,
lastSyncTime: lastSyncUser?.lastQuotaSync || null,
};
}
/**
* Manual sync for all users (useful for admin endpoints)
*/
async forceSyncAllUsers(): Promise<{ success: number; errors: number }> {
this.logger.log("Manual quota sync triggered");
await this.syncAllUsersQuota();
// Return basic statistics
const em = this.em.fork();
const users = await em.find(User, {
emailEnabled: true,
deletedAt: null,
wildduckUserId: { $ne: null },
});
const recentSyncs = await em.count(User, {
emailEnabled: true,
deletedAt: null,
lastQuotaSync: { $gte: new Date(Date.now() - 60000) }, // Last minute
});
return {
success: recentSyncs,
errors: users.length - recentSyncs,
};
}
}