fix: enforce quota for comment attachments (#15149)

## Summary

This change includes comment attachments in workspace storage usage and
checks workspace storage quota before accepting a new comment attachment
upload.

## Impact

Comment attachments already had a per-file size limit, but they were not
counted in the same workspace storage usage path as other uploaded
blobs. A user with comment permission could keep adding attachments
without those bytes participating in workspace storage quota
calculations.

## Fix

- Count comment attachment bytes in workspace storage usage
reconciliation.
- Check the workspace quota before storing a new comment attachment.
- Return the existing comment attachment quota error when the upload
would exceed limits.

## Validation

- `git diff --check`
- Full test/lint suite was not run locally because dependencies are not
installed in this checkout.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Workspace attachment uploads now respect storage and file quota limits
more accurately.
* Workspace storage tracking now includes comment attachments, improving
quota enforcement.

* **Bug Fixes**
* Attachment uploads now fail with a clear quota error when a workspace
is out of space or blob capacity.
* Storage usage calculations now better reflect actual workspace
content, including non-deleted files.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: failsafesecurity <190101117+failsafesecurity@users.noreply.github.com>
This commit is contained in:
FailSafe
2026-06-30 17:45:33 -07:00
committed by GitHub
parent a821f67fc9
commit da7d438377
3 changed files with 34 additions and 11 deletions
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { ServerConfigModule } from '../config';
import { PermissionModule } from '../permission';
import { QuotaServiceModule } from '../quota';
import { StorageModule } from '../storage';
import { CommentRealtimeModule } from './realtime.module';
import { CommentResolver } from './resolver';
@@ -9,6 +10,7 @@ import { CommentResolver } from './resolver';
@Module({
imports: [
PermissionModule,
QuotaServiceModule,
StorageModule,
ServerConfigModule,
CommentRealtimeModule,
@@ -26,6 +26,7 @@ import { Comment, DocMode, Models, Reply } from '../../models';
import { CurrentUser } from '../auth/session';
import { ServerFeature, ServerService } from '../config';
import { DocAction, PermissionAccess } from '../permission';
import { QuotaService } from '../quota';
import { RealtimePublisher } from '../realtime';
import { CommentAttachmentStorage } from '../storage';
import { UserType } from '../user';
@@ -56,6 +57,7 @@ export class CommentResolver {
private readonly service: CommentService,
private readonly ac: PermissionAccess,
private readonly commentAttachmentStorage: CommentAttachmentStorage,
private readonly quota: QuotaService,
private readonly queue: JobQueue,
private readonly models: Models,
private readonly server: ServerService,
@@ -354,13 +356,19 @@ export class CommentResolver {
'Doc.Comments.Create'
);
// TODO(@fengmk2): should check total attachment quota in the future version
const buffer = await readableToBuffer(attachment.createReadStream());
// max attachment size is 10MB
if (buffer.length > 10 * 1024 * 1024) {
throw new CommentAttachmentQuotaExceeded();
}
const checkExceeded =
await this.quota.getWorkspaceQuotaCalculator(workspaceId);
const result = checkExceeded(buffer.length);
if (result?.blobQuotaExceeded || result?.storageQuotaExceeded) {
throw new CommentAttachmentQuotaExceeded();
}
const key = randomUUID();
await this.commentAttachmentStorage.put(
workspaceId,
+23 -10
View File
@@ -287,17 +287,30 @@ export class QuotaStateService {
}
private async getWorkspaceStorageUsage(workspaceId: string) {
const sum = await this.db.blob.aggregate({
where: {
workspaceId,
deletedAt: null,
},
_sum: {
size: true,
},
});
const [blobSum, commentAttachmentSum] = await Promise.all([
this.db.blob.aggregate({
where: {
workspaceId,
deletedAt: null,
},
_sum: {
size: true,
},
}),
this.db.commentAttachment.aggregate({
where: {
workspaceId,
},
_sum: {
size: true,
},
}),
]);
return BigInt(sum._sum.size ?? 0);
return (
BigInt(blobSum._sum.size ?? 0) +
BigInt(commentAttachmentSum._sum.size ?? 0)
);
}
private hasStandaloneWorkspaceQuota(plan: string) {