feat(nbstore): add blob sync storage (#10752)

This commit is contained in:
EYHN
2025-03-14 18:05:54 +08:00
committed by GitHub
parent a2eb3fe1b2
commit 05200ad7b7
56 changed files with 1441 additions and 404 deletions
+61 -10
View File
@@ -1,11 +1,18 @@
import { UserFriendlyError } from '@affine/error';
import {
deleteBlobMutation,
listBlobsQuery,
releaseDeletedBlobsMutation,
setBlobMutation,
workspaceQuotaQuery,
} from '@affine/graphql';
import { type BlobRecord, BlobStorageBase } from '../../storage';
import {
type BlobRecord,
BlobStorageBase,
OverCapacityError,
OverSizeError,
} from '../../storage';
import { HttpConnection } from './http';
interface CloudBlobStorageOptions {
@@ -15,6 +22,7 @@ interface CloudBlobStorageOptions {
export class CloudBlobStorage extends BlobStorageBase {
static readonly identifier = 'CloudBlobStorage';
override readonly isReadonly = false;
constructor(private readonly options: CloudBlobStorageOptions) {
super();
@@ -22,7 +30,7 @@ export class CloudBlobStorage extends BlobStorageBase {
readonly connection = new HttpConnection(this.options.serverBaseUrl);
override async get(key: string) {
override async get(key: string, signal?: AbortSignal) {
const res = await this.connection.fetch(
'/api/workspaces/' + this.options.id + '/blobs/' + key,
{
@@ -30,6 +38,7 @@ export class CloudBlobStorage extends BlobStorageBase {
headers: {
'x-affine-version': BUILD_CONFIG.appVersion,
},
signal,
}
);
@@ -52,14 +61,32 @@ export class CloudBlobStorage extends BlobStorageBase {
}
}
override async set(blob: BlobRecord) {
await this.connection.gql({
query: setBlobMutation,
variables: {
workspaceId: this.options.id,
blob: new File([blob.data], blob.key, { type: blob.mime }),
},
});
override async set(blob: BlobRecord, signal?: AbortSignal) {
try {
const blobSizeLimit = await this.getBlobSizeLimit();
if (blob.data.byteLength > blobSizeLimit) {
throw new OverSizeError();
}
await this.connection.gql({
query: setBlobMutation,
variables: {
workspaceId: this.options.id,
blob: new File([blob.data], blob.key, { type: blob.mime }),
},
context: {
signal,
},
});
} catch (err) {
const userFriendlyError = UserFriendlyError.fromAny(err);
if (userFriendlyError.is('BLOB_QUOTA_EXCEEDED')) {
throw new OverCapacityError();
}
if (userFriendlyError.is('CONTENT_TOO_LARGE')) {
throw new OverSizeError();
}
throw err;
}
}
override async delete(key: string, permanently: boolean) {
@@ -87,4 +114,28 @@ export class CloudBlobStorage extends BlobStorageBase {
createdAt: new Date(blob.createdAt),
}));
}
private blobSizeLimitCache: number | null = null;
private blobSizeLimitCacheTime = 0;
private async getBlobSizeLimit() {
// If cache time is less than 120 seconds, return the cached value directly
if (
this.blobSizeLimitCache !== null &&
Date.now() - this.blobSizeLimitCacheTime < 120 * 1000
) {
return this.blobSizeLimitCache;
}
try {
const res = await this.connection.gql({
query: workspaceQuotaQuery,
variables: { id: this.options.id },
});
this.blobSizeLimitCache = res.workspace.quota.blobLimit;
this.blobSizeLimitCacheTime = Date.now();
return this.blobSizeLimitCache;
} catch (err) {
throw UserFriendlyError.fromAny(err);
}
}
}