chore: new feture for quato
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user