diff --git a/packages/backend/server/src/__tests__/workspace/blobs.e2e.ts b/packages/backend/server/src/__tests__/workspace/blobs.e2e.ts index a94b9da51..c6e2c431a 100644 --- a/packages/backend/server/src/__tests__/workspace/blobs.e2e.ts +++ b/packages/backend/server/src/__tests__/workspace/blobs.e2e.ts @@ -143,10 +143,12 @@ test('should reject blob exceeded limit', async t => { const buffer1 = Buffer.from( Array.from({ length: RESTRICTED_QUOTA.blobLimit + 1 }, () => 0) ); - await t.throwsAsync(setBlob(app, workspace1.id, buffer1)); + await t.throwsAsync(setBlob(app, workspace1.id, buffer1), { + message: 'You have exceeded your blob size quota.', + }); }); -test('should reject blob exceeded quota', async t => { +test('should reject blob exceeded storage quota', async t => { await app.signupV1('u1@affine.pro'); const workspace = await createWorkspace(app); @@ -155,7 +157,9 @@ test('should reject blob exceeded quota', async t => { const buffer = Buffer.from(Array.from({ length: OneMB }, () => 0)); await t.notThrowsAsync(setBlob(app, workspace.id, buffer)); - await t.throwsAsync(setBlob(app, workspace.id, buffer)); + await t.throwsAsync(setBlob(app, workspace.id, buffer), { + message: 'You have exceeded your storage quota.', + }); }); test('should accept blob even storage out of quota if workspace has unlimited feature', async t => { diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 60aab8dcc..b3f63a06b 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -695,7 +695,11 @@ export const USER_FRIENDLY_ERRORS = { // Quota & Limit errors blob_quota_exceeded: { type: 'quota_exceeded', - message: 'You have exceeded your blob storage quota.', + message: 'You have exceeded your blob size quota.', + }, + storage_quota_exceeded: { + type: 'quota_exceeded', + message: 'You have exceeded your storage quota.', }, member_quota_exceeded: { type: 'quota_exceeded', diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index bdc640aa5..451812efc 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -759,6 +759,12 @@ export class BlobQuotaExceeded extends UserFriendlyError { } } +export class StorageQuotaExceeded extends UserFriendlyError { + constructor(message?: string) { + super('quota_exceeded', 'storage_quota_exceeded', message); + } +} + export class MemberQuotaExceeded extends UserFriendlyError { constructor(message?: string) { super('quota_exceeded', 'member_quota_exceeded', message); @@ -995,6 +1001,7 @@ export enum ErrorNames { COPILOT_FAILED_TO_MATCH_CONTEXT, COPILOT_EMBEDDING_UNAVAILABLE, BLOB_QUOTA_EXCEEDED, + STORAGE_QUOTA_EXCEEDED, MEMBER_QUOTA_EXCEEDED, COPILOT_QUOTA_EXCEEDED, RUNTIME_CONFIG_NOT_FOUND, diff --git a/packages/backend/server/src/core/quota/service.ts b/packages/backend/server/src/core/quota/service.ts index 96db1d79e..fa519809f 100644 --- a/packages/backend/server/src/core/quota/service.ts +++ b/packages/backend/server/src/core/quota/service.ts @@ -258,14 +258,14 @@ export class QuotaService { this.logger.warn( `storage size limit exceeded: ${currentSize} > ${storageQuota}` ); - return true; + return { storageQuotaExceeded: true, blobQuotaExceeded: false }; } else if (recvSize > blobLimit) { this.logger.warn( `blob size limit exceeded: ${recvSize} > ${blobLimit}` ); - return true; + return { storageQuotaExceeded: false, blobQuotaExceeded: true }; } else { - return false; + return; } }; return checkExceeded; diff --git a/packages/backend/server/src/core/workspaces/resolvers/blob.ts b/packages/backend/server/src/core/workspaces/resolvers/blob.ts index 37abf2f2f..a188096b5 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/blob.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/blob.ts @@ -13,7 +13,11 @@ import { import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; import type { FileUpload } from '../../../base'; -import { BlobQuotaExceeded, CloudThrottlerGuard } from '../../../base'; +import { + BlobQuotaExceeded, + CloudThrottlerGuard, + StorageQuotaExceeded, +} from '../../../base'; import { CurrentUser } from '../../auth'; import { AccessController } from '../../permission'; import { QuotaService } from '../../quota'; @@ -92,9 +96,11 @@ export class WorkspaceBlobResolver { const checkExceeded = await this.quota.getWorkspaceQuotaCalculator(workspaceId); - // TODO(@darksky): need a proper way to separate `BlobQuotaExceeded` and `BlobSizeTooLarge` - if (checkExceeded(0)) { + let result = checkExceeded(0); + if (result?.blobQuotaExceeded) { throw new BlobQuotaExceeded(); + } else if (result?.storageQuotaExceeded) { + throw new StorageQuotaExceeded(); } const buffer = await new Promise((resolve, reject) => { const stream = blob.createReadStream(); @@ -104,16 +110,22 @@ export class WorkspaceBlobResolver { // check size after receive each chunk to avoid unnecessary memory usage const bufferSize = chunks.reduce((acc, cur) => acc + cur.length, 0); - if (checkExceeded(bufferSize)) { + result = checkExceeded(bufferSize); + if (result?.blobQuotaExceeded) { reject(new BlobQuotaExceeded()); + } else if (result?.storageQuotaExceeded) { + reject(new StorageQuotaExceeded()); } }); stream.on('error', reject); stream.on('end', () => { const buffer = Buffer.concat(chunks); - if (checkExceeded(buffer.length)) { + result = checkExceeded(buffer.length); + if (result?.blobQuotaExceeded) { reject(new BlobQuotaExceeded()); + } else if (result?.storageQuotaExceeded) { + reject(new StorageQuotaExceeded()); } else { resolve(buffer); } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 3d9749fe1..043769126 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -471,6 +471,7 @@ enum ErrorNames { SPACE_NOT_FOUND SPACE_OWNER_NOT_FOUND SPACE_SHOULD_HAVE_ONLY_ONE_OWNER + STORAGE_QUOTA_EXCEEDED SUBSCRIPTION_ALREADY_EXISTS SUBSCRIPTION_EXPIRED SUBSCRIPTION_HAS_BEEN_CANCELED diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 3231e82cf..abc1313c5 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -612,6 +612,7 @@ export enum ErrorNames { SPACE_NOT_FOUND = 'SPACE_NOT_FOUND', SPACE_OWNER_NOT_FOUND = 'SPACE_OWNER_NOT_FOUND', SPACE_SHOULD_HAVE_ONLY_ONE_OWNER = 'SPACE_SHOULD_HAVE_ONLY_ONE_OWNER', + STORAGE_QUOTA_EXCEEDED = 'STORAGE_QUOTA_EXCEEDED', SUBSCRIPTION_ALREADY_EXISTS = 'SUBSCRIPTION_ALREADY_EXISTS', SUBSCRIPTION_EXPIRED = 'SUBSCRIPTION_EXPIRED', SUBSCRIPTION_HAS_BEEN_CANCELED = 'SUBSCRIPTION_HAS_BEEN_CANCELED', diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index d93f08564..48f7e9566 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -7726,9 +7726,13 @@ export function useAFFiNEI18N(): { */ ["error.COPILOT_EMBEDDING_UNAVAILABLE"](): string; /** - * `You have exceeded your blob storage quota.` + * `You have exceeded your blob size quota.` */ ["error.BLOB_QUOTA_EXCEEDED"](): string; + /** + * `You have exceeded your storage quota.` + */ + ["error.STORAGE_QUOTA_EXCEEDED"](): string; /** * `You have exceeded your workspace member quota.` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 43d1e2ba6..c8bc7a370 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1901,7 +1901,8 @@ "error.COPILOT_FAILED_TO_MODIFY_CONTEXT": "Failed to modify context {{contextId}}: {{message}}", "error.COPILOT_FAILED_TO_MATCH_CONTEXT": "Failed to match context {{contextId}} with \"%7B%7Bcontent%7D%7D\": {{message}}", "error.COPILOT_EMBEDDING_UNAVAILABLE": "Embedding feature not available, you may need to install pgvector extension to your database", - "error.BLOB_QUOTA_EXCEEDED": "You have exceeded your blob storage quota.", + "error.BLOB_QUOTA_EXCEEDED": "You have exceeded your blob size quota.", + "error.STORAGE_QUOTA_EXCEEDED": "You have exceeded your storage quota.", "error.MEMBER_QUOTA_EXCEEDED": "You have exceeded your workspace member quota.", "error.COPILOT_QUOTA_EXCEEDED": "You have reached the limit of actions in this workspace, please upgrade your plan.", "error.RUNTIME_CONFIG_NOT_FOUND": "Runtime config {{key}} not found.",