feat(billing): cap each owner's stored file volume

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-02 15:44:36 +03:30
co-authored by Cursor
parent e6e90aabf3
commit 28938e8f77
20 changed files with 647 additions and 22 deletions
@@ -19,6 +19,7 @@ import { Prisma } from '@prisma/client';
import { randomUUID } from 'node:crypto';
import * as path from 'node:path';
import { LocalFileStorage } from '../storage/local-file.storage';
import { StorageQuotaService } from '../billing/storage-quota.service';
import { PrismaService } from '../prisma/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { AutomationsService } from '../automations/automations.service';
@@ -128,6 +129,7 @@ export class ActivitiesService {
private readonly types: ActivityTypesService,
private readonly game: GamificationService,
private readonly files: LocalFileStorage,
private readonly storage: StorageQuotaService,
) {}
private async ctx(organizationId: string): Promise<DtoContext> {
@@ -817,8 +819,10 @@ export class ActivitiesService {
await this.ensureExists(organizationId, id, actor);
const images = await this.prisma.activityImage.findMany({
where: { activityId: id, organizationId },
select: { storageKey: true },
select: { storageKey: true, sizeBytes: true },
});
const bytes = images.reduce((sum, image) => sum + image.sizeBytes, 0);
if (bytes) await this.storage.addLocal(organizationId, -bytes);
await this.prisma.activity.delete({ where: { id } });
for (const image of images) {
await this.files.remove(image.storageKey);
@@ -853,6 +857,7 @@ export class ActivitiesService {
const storageKey = path
.join(organizationId, 'activities', activityId, `${imageId}${ext}`)
.replace(/\\/g, '/');
await this.storage.assertCanAdd(organizationId, file.size);
await this.files.save(storageKey, file.buffer);
try {
const row = await this.prisma.activityImage.create({
@@ -867,6 +872,7 @@ export class ActivitiesService {
uploadedById: userId,
},
});
await this.storage.addLocal(organizationId, file.size);
return {
id: row.id,
fileName: row.fileName,
@@ -893,6 +899,7 @@ export class ActivitiesService {
if (!row) throw new NotFoundException('عکس پیدا نشد');
await this.files.remove(row.storageKey);
await this.prisma.activityImage.delete({ where: { id: row.id } });
await this.storage.addLocal(organizationId, -row.sizeBytes);
}
async openImage(