mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-19 15:26:59 +08:00
feat: multipart blob sync support (#14138)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Flexible blob uploads: GRAPHQL, presigned, and multipart flows with
per‑part URLs, abort/complete operations, presigned proxy endpoints, and
nightly cleanup of expired pending uploads.
* **API / Schema**
* GraphQL additions: new types, mutations, enum and error to manage
upload lifecycle (create, complete, abort, get part URL).
* **Database**
* New blob status enum and columns (status, upload_id); listing now
defaults to completed blobs.
* **Localization**
* Added user-facing message: "Blob is invalid."
* **Tests**
* Expanded unit and end‑to‑end coverage for upload flows, proxy
behavior, multipart and provider integrations.
<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
mutation abortBlobUpload($workspaceId: String!, $key: String!, $uploadId: String!) {
|
||||
abortBlobUpload(workspaceId: $workspaceId, key: $key, uploadId: $uploadId)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation completeBlobUpload($workspaceId: String!, $key: String!, $uploadId: String, $parts: [BlobUploadPartInput!]) {
|
||||
completeBlobUpload(workspaceId: $workspaceId, key: $key, uploadId: $uploadId, parts: $parts)
|
||||
}
|
||||
16
packages/common/graphql/src/graphql/blob-upload-create.gql
Normal file
16
packages/common/graphql/src/graphql/blob-upload-create.gql
Normal file
@@ -0,0 +1,16 @@
|
||||
mutation createBlobUpload($workspaceId: String!, $key: String!, $size: Int!, $mime: String!) {
|
||||
createBlobUpload(workspaceId: $workspaceId, key: $key, size: $size, mime: $mime) {
|
||||
method
|
||||
blobKey
|
||||
alreadyUploaded
|
||||
uploadUrl
|
||||
headers
|
||||
expiresAt
|
||||
uploadId
|
||||
partSize
|
||||
uploadedParts {
|
||||
partNumber
|
||||
etag
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation getBlobUploadPartUrl($workspaceId: String!, $key: String!, $uploadId: String!, $partNumber: Int!) {
|
||||
getBlobUploadPartUrl(workspaceId: $workspaceId, key: $key, uploadId: $uploadId, partNumber: $partNumber) {
|
||||
uploadUrl
|
||||
headers
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
@@ -383,6 +383,65 @@ export const setBlobMutation = {
|
||||
file: true,
|
||||
};
|
||||
|
||||
export const abortBlobUploadMutation = {
|
||||
id: 'abortBlobUploadMutation' as const,
|
||||
op: 'abortBlobUpload',
|
||||
query: `mutation abortBlobUpload($workspaceId: String!, $key: String!, $uploadId: String!) {
|
||||
abortBlobUpload(workspaceId: $workspaceId, key: $key, uploadId: $uploadId)
|
||||
}`,
|
||||
};
|
||||
|
||||
export const completeBlobUploadMutation = {
|
||||
id: 'completeBlobUploadMutation' as const,
|
||||
op: 'completeBlobUpload',
|
||||
query: `mutation completeBlobUpload($workspaceId: String!, $key: String!, $uploadId: String, $parts: [BlobUploadPartInput!]) {
|
||||
completeBlobUpload(
|
||||
workspaceId: $workspaceId
|
||||
key: $key
|
||||
uploadId: $uploadId
|
||||
parts: $parts
|
||||
)
|
||||
}`,
|
||||
};
|
||||
|
||||
export const createBlobUploadMutation = {
|
||||
id: 'createBlobUploadMutation' as const,
|
||||
op: 'createBlobUpload',
|
||||
query: `mutation createBlobUpload($workspaceId: String!, $key: String!, $size: Int!, $mime: String!) {
|
||||
createBlobUpload(workspaceId: $workspaceId, key: $key, size: $size, mime: $mime) {
|
||||
method
|
||||
blobKey
|
||||
alreadyUploaded
|
||||
uploadUrl
|
||||
headers
|
||||
expiresAt
|
||||
uploadId
|
||||
partSize
|
||||
uploadedParts {
|
||||
partNumber
|
||||
etag
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const getBlobUploadPartUrlMutation = {
|
||||
id: 'getBlobUploadPartUrlMutation' as const,
|
||||
op: 'getBlobUploadPartUrl',
|
||||
query: `mutation getBlobUploadPartUrl($workspaceId: String!, $key: String!, $uploadId: String!, $partNumber: Int!) {
|
||||
getBlobUploadPartUrl(
|
||||
workspaceId: $workspaceId
|
||||
key: $key
|
||||
uploadId: $uploadId
|
||||
partNumber: $partNumber
|
||||
) {
|
||||
uploadUrl
|
||||
headers
|
||||
expiresAt
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const cancelSubscriptionMutation = {
|
||||
id: 'cancelSubscriptionMutation' as const,
|
||||
op: 'cancelSubscription',
|
||||
|
||||
@@ -137,6 +137,44 @@ export interface BlobNotFoundDataType {
|
||||
spaceId: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface BlobUploadInit {
|
||||
__typename?: 'BlobUploadInit';
|
||||
alreadyUploaded: Maybe<Scalars['Boolean']['output']>;
|
||||
blobKey: Scalars['String']['output'];
|
||||
expiresAt: Maybe<Scalars['DateTime']['output']>;
|
||||
headers: Maybe<Scalars['JSONObject']['output']>;
|
||||
method: BlobUploadMethod;
|
||||
partSize: Maybe<Scalars['Int']['output']>;
|
||||
uploadId: Maybe<Scalars['String']['output']>;
|
||||
uploadUrl: Maybe<Scalars['String']['output']>;
|
||||
uploadedParts: Maybe<Array<BlobUploadedPart>>;
|
||||
}
|
||||
|
||||
/** Blob upload method */
|
||||
export enum BlobUploadMethod {
|
||||
GRAPHQL = 'GRAPHQL',
|
||||
MULTIPART = 'MULTIPART',
|
||||
PRESIGNED = 'PRESIGNED',
|
||||
}
|
||||
|
||||
export interface BlobUploadPart {
|
||||
__typename?: 'BlobUploadPart';
|
||||
expiresAt: Maybe<Scalars['DateTime']['output']>;
|
||||
headers: Maybe<Scalars['JSONObject']['output']>;
|
||||
uploadUrl: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface BlobUploadPartInput {
|
||||
etag: Scalars['String']['input'];
|
||||
partNumber: Scalars['Int']['input'];
|
||||
}
|
||||
|
||||
export interface BlobUploadedPart {
|
||||
__typename?: 'BlobUploadedPart';
|
||||
etag: Scalars['String']['output'];
|
||||
partNumber: Scalars['Int']['output'];
|
||||
}
|
||||
|
||||
export enum ChatHistoryOrder {
|
||||
asc = 'asc',
|
||||
desc = 'desc',
|
||||
@@ -838,6 +876,7 @@ export enum ErrorNames {
|
||||
ALREADY_IN_SPACE = 'ALREADY_IN_SPACE',
|
||||
AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
|
||||
BAD_REQUEST = 'BAD_REQUEST',
|
||||
BLOB_INVALID = 'BLOB_INVALID',
|
||||
BLOB_NOT_FOUND = 'BLOB_NOT_FOUND',
|
||||
BLOB_QUOTA_EXCEEDED = 'BLOB_QUOTA_EXCEEDED',
|
||||
CANNOT_DELETE_ACCOUNT_WITH_OWNED_TEAM_WORKSPACE = 'CANNOT_DELETE_ACCOUNT_WITH_OWNED_TEAM_WORKSPACE',
|
||||
@@ -1370,6 +1409,7 @@ export interface MissingOauthQueryParameterDataType {
|
||||
|
||||
export interface Mutation {
|
||||
__typename?: 'Mutation';
|
||||
abortBlobUpload: Scalars['Boolean']['output'];
|
||||
acceptInviteById: Scalars['Boolean']['output'];
|
||||
activateLicense: License;
|
||||
/** add a blob to context */
|
||||
@@ -1392,6 +1432,8 @@ export interface Mutation {
|
||||
claimAudioTranscription: Maybe<TranscriptionResultType>;
|
||||
/** Cleanup sessions */
|
||||
cleanupCopilotSession: Array<Scalars['String']['output']>;
|
||||
completeBlobUpload: Scalars['String']['output'];
|
||||
createBlobUpload: BlobUploadInit;
|
||||
/** Create change password url */
|
||||
createChangePasswordUrl: Scalars['String']['output'];
|
||||
/** Create a subscription checkout link of stripe */
|
||||
@@ -1430,6 +1472,7 @@ export interface Mutation {
|
||||
forkCopilotSession: Scalars['String']['output'];
|
||||
generateLicenseKey: Scalars['String']['output'];
|
||||
generateUserAccessToken: RevealedAccessToken;
|
||||
getBlobUploadPartUrl: BlobUploadPart;
|
||||
grantDocUserRoles: Scalars['Boolean']['output'];
|
||||
grantMember: Scalars['Boolean']['output'];
|
||||
/** import users */
|
||||
@@ -1527,6 +1570,12 @@ export interface Mutation {
|
||||
verifyEmail: Scalars['Boolean']['output'];
|
||||
}
|
||||
|
||||
export interface MutationAbortBlobUploadArgs {
|
||||
key: Scalars['String']['input'];
|
||||
uploadId: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
}
|
||||
|
||||
export interface MutationAcceptInviteByIdArgs {
|
||||
inviteId: Scalars['String']['input'];
|
||||
sendAcceptMail?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
@@ -1599,6 +1648,20 @@ export interface MutationCleanupCopilotSessionArgs {
|
||||
options: DeleteSessionInput;
|
||||
}
|
||||
|
||||
export interface MutationCompleteBlobUploadArgs {
|
||||
key: Scalars['String']['input'];
|
||||
parts?: InputMaybe<Array<BlobUploadPartInput>>;
|
||||
uploadId?: InputMaybe<Scalars['String']['input']>;
|
||||
workspaceId: Scalars['String']['input'];
|
||||
}
|
||||
|
||||
export interface MutationCreateBlobUploadArgs {
|
||||
key: Scalars['String']['input'];
|
||||
mime: Scalars['String']['input'];
|
||||
size: Scalars['Int']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
}
|
||||
|
||||
export interface MutationCreateChangePasswordUrlArgs {
|
||||
callbackUrl: Scalars['String']['input'];
|
||||
userId: Scalars['String']['input'];
|
||||
@@ -1693,6 +1756,13 @@ export interface MutationGenerateUserAccessTokenArgs {
|
||||
input: GenerateAccessTokenInput;
|
||||
}
|
||||
|
||||
export interface MutationGetBlobUploadPartUrlArgs {
|
||||
key: Scalars['String']['input'];
|
||||
partNumber: Scalars['Int']['input'];
|
||||
uploadId: Scalars['String']['input'];
|
||||
workspaceId: Scalars['String']['input'];
|
||||
}
|
||||
|
||||
export interface MutationGrantDocUserRolesArgs {
|
||||
input: GrantDocUserRolesInput;
|
||||
}
|
||||
@@ -3411,6 +3481,73 @@ export type SetBlobMutationVariables = Exact<{
|
||||
|
||||
export type SetBlobMutation = { __typename?: 'Mutation'; setBlob: string };
|
||||
|
||||
export type AbortBlobUploadMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
key: Scalars['String']['input'];
|
||||
uploadId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type AbortBlobUploadMutation = {
|
||||
__typename?: 'Mutation';
|
||||
abortBlobUpload: boolean;
|
||||
};
|
||||
|
||||
export type CompleteBlobUploadMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
key: Scalars['String']['input'];
|
||||
uploadId?: InputMaybe<Scalars['String']['input']>;
|
||||
parts?: InputMaybe<Array<BlobUploadPartInput> | BlobUploadPartInput>;
|
||||
}>;
|
||||
|
||||
export type CompleteBlobUploadMutation = {
|
||||
__typename?: 'Mutation';
|
||||
completeBlobUpload: string;
|
||||
};
|
||||
|
||||
export type CreateBlobUploadMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
key: Scalars['String']['input'];
|
||||
size: Scalars['Int']['input'];
|
||||
mime: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type CreateBlobUploadMutation = {
|
||||
__typename?: 'Mutation';
|
||||
createBlobUpload: {
|
||||
__typename?: 'BlobUploadInit';
|
||||
method: BlobUploadMethod;
|
||||
blobKey: string;
|
||||
alreadyUploaded: boolean | null;
|
||||
uploadUrl: string | null;
|
||||
headers: any | null;
|
||||
expiresAt: string | null;
|
||||
uploadId: string | null;
|
||||
partSize: number | null;
|
||||
uploadedParts: Array<{
|
||||
__typename?: 'BlobUploadedPart';
|
||||
partNumber: number;
|
||||
etag: string;
|
||||
}> | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetBlobUploadPartUrlMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
key: Scalars['String']['input'];
|
||||
uploadId: Scalars['String']['input'];
|
||||
partNumber: Scalars['Int']['input'];
|
||||
}>;
|
||||
|
||||
export type GetBlobUploadPartUrlMutation = {
|
||||
__typename?: 'Mutation';
|
||||
getBlobUploadPartUrl: {
|
||||
__typename?: 'BlobUploadPart';
|
||||
uploadUrl: string;
|
||||
headers: any | null;
|
||||
expiresAt: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type CancelSubscriptionMutationVariables = Exact<{
|
||||
plan?: InputMaybe<SubscriptionPlan>;
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -6824,6 +6961,26 @@ export type Mutations =
|
||||
variables: SetBlobMutationVariables;
|
||||
response: SetBlobMutation;
|
||||
}
|
||||
| {
|
||||
name: 'abortBlobUploadMutation';
|
||||
variables: AbortBlobUploadMutationVariables;
|
||||
response: AbortBlobUploadMutation;
|
||||
}
|
||||
| {
|
||||
name: 'completeBlobUploadMutation';
|
||||
variables: CompleteBlobUploadMutationVariables;
|
||||
response: CompleteBlobUploadMutation;
|
||||
}
|
||||
| {
|
||||
name: 'createBlobUploadMutation';
|
||||
variables: CreateBlobUploadMutationVariables;
|
||||
response: CreateBlobUploadMutation;
|
||||
}
|
||||
| {
|
||||
name: 'getBlobUploadPartUrlMutation';
|
||||
variables: GetBlobUploadPartUrlMutationVariables;
|
||||
response: GetBlobUploadPartUrlMutation;
|
||||
}
|
||||
| {
|
||||
name: 'cancelSubscriptionMutation';
|
||||
variables: CancelSubscriptionMutationVariables;
|
||||
|
||||
Reference in New Issue
Block a user