mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 23:56:36 +08:00
refactor(infra): directory structure (#4615)
This commit is contained in:
@@ -0,0 +1,838 @@
|
||||
import type { Storage } from '@affine/storage';
|
||||
import {
|
||||
ForbiddenException,
|
||||
Inject,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Field,
|
||||
Float,
|
||||
ID,
|
||||
InputType,
|
||||
Int,
|
||||
Mutation,
|
||||
ObjectType,
|
||||
OmitType,
|
||||
Parent,
|
||||
PartialType,
|
||||
PickType,
|
||||
Query,
|
||||
registerEnumType,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import type { User, Workspace } from '@prisma/client';
|
||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||
import { applyUpdate, Doc } from 'yjs';
|
||||
|
||||
import { PrismaService } from '../../prisma';
|
||||
import { StorageProvide } from '../../storage';
|
||||
import { CloudThrottlerGuard, Throttle } from '../../throttler';
|
||||
import type { FileUpload } from '../../types';
|
||||
import { DocID } from '../../utils/doc';
|
||||
import { Auth, CurrentUser, Public } from '../auth';
|
||||
import { MailService } from '../auth/mailer';
|
||||
import { AuthService } from '../auth/service';
|
||||
import { UsersService } from '../users';
|
||||
import { UserType } from '../users/resolver';
|
||||
import { PermissionService } from './permission';
|
||||
import { Permission } from './types';
|
||||
import { defaultWorkspaceAvatar } from './utils';
|
||||
|
||||
registerEnumType(Permission, {
|
||||
name: 'Permission',
|
||||
description: 'User permission in workspace',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class InviteUserType extends OmitType(
|
||||
PartialType(UserType),
|
||||
['id'],
|
||||
ObjectType
|
||||
) {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field(() => Permission, { description: 'User permission in workspace' })
|
||||
permission!: Permission;
|
||||
|
||||
@Field({ description: 'Invite id' })
|
||||
inviteId!: string;
|
||||
|
||||
@Field({ description: 'User accepted' })
|
||||
accepted!: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class WorkspaceType implements Partial<Workspace> {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field({ description: 'is Public workspace' })
|
||||
public!: boolean;
|
||||
|
||||
@Field({ description: 'Workspace created date' })
|
||||
createdAt!: Date;
|
||||
|
||||
@Field(() => [InviteUserType], {
|
||||
description: 'Members of workspace',
|
||||
})
|
||||
members!: InviteUserType[];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class InvitationWorkspaceType {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
|
||||
@Field({ description: 'Workspace name' })
|
||||
name!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
// nullable: true,
|
||||
description: 'Base64 encoded avatar',
|
||||
})
|
||||
avatar!: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class WorkspaceBlobSizes {
|
||||
@Field(() => Float)
|
||||
size!: number;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class InvitationType {
|
||||
@Field({ description: 'Workspace information' })
|
||||
workspace!: InvitationWorkspaceType;
|
||||
@Field({ description: 'User information' })
|
||||
user!: UserType;
|
||||
@Field({ description: 'Invitee information' })
|
||||
invitee!: UserType;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateWorkspaceInput extends PickType(
|
||||
PartialType(WorkspaceType),
|
||||
['public'],
|
||||
InputType
|
||||
) {
|
||||
@Field(() => ID)
|
||||
id!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace resolver
|
||||
* Public apis rate limit: 10 req/m
|
||||
* Other rate limit: 120 req/m
|
||||
*/
|
||||
@UseGuards(CloudThrottlerGuard)
|
||||
@Auth()
|
||||
@Resolver(() => WorkspaceType)
|
||||
export class WorkspaceResolver {
|
||||
private readonly logger = new Logger('WorkspaceResolver');
|
||||
|
||||
constructor(
|
||||
private readonly auth: AuthService,
|
||||
private readonly mailer: MailService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissions: PermissionService,
|
||||
private readonly users: UsersService,
|
||||
@Inject(StorageProvide) private readonly storage: Storage
|
||||
) {}
|
||||
|
||||
@ResolveField(() => Permission, {
|
||||
description: 'Permission of current signed in user in workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async permission(
|
||||
@CurrentUser() user: UserType,
|
||||
@Parent() workspace: WorkspaceType
|
||||
) {
|
||||
// may applied in workspaces query
|
||||
if ('permission' in workspace) {
|
||||
return workspace.permission;
|
||||
}
|
||||
|
||||
const permission = await this.permissions.get(workspace.id, user.id);
|
||||
|
||||
if (!permission) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'member count of workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
memberCount(@Parent() workspace: WorkspaceType) {
|
||||
return this.prisma.userWorkspacePermission.count({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
userId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => [String], {
|
||||
description: 'Shared pages of workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async sharedPages(@Parent() workspace: WorkspaceType) {
|
||||
const data = await this.prisma.userWorkspacePermission.findMany({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
return data.map(item => item.subPageId).filter(Boolean);
|
||||
}
|
||||
|
||||
@ResolveField(() => UserType, {
|
||||
description: 'Owner of workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async owner(@Parent() workspace: WorkspaceType) {
|
||||
const data = await this.permissions.getWorkspaceOwner(workspace.id);
|
||||
|
||||
return data.user;
|
||||
}
|
||||
|
||||
@ResolveField(() => [InviteUserType], {
|
||||
description: 'Members of workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async members(
|
||||
@Parent() workspace: WorkspaceType,
|
||||
@Args('skip', { type: () => Int, nullable: true }) skip?: number,
|
||||
@Args('take', { type: () => Int, nullable: true }) take?: number
|
||||
) {
|
||||
const data = await this.prisma.userWorkspacePermission.findMany({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
userId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: take || 8,
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: 'asc',
|
||||
},
|
||||
{
|
||||
type: 'desc',
|
||||
},
|
||||
],
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return data
|
||||
.filter(({ user }) => !!user)
|
||||
.map(({ id, accepted, type, user }) => ({
|
||||
...user,
|
||||
permission: type,
|
||||
inviteId: id,
|
||||
accepted,
|
||||
}));
|
||||
}
|
||||
|
||||
@Query(() => Boolean, {
|
||||
description: 'Get is owner of workspace',
|
||||
complexity: 2,
|
||||
})
|
||||
async isOwner(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string
|
||||
) {
|
||||
const data = await this.permissions.tryGetWorkspaceOwner(workspaceId);
|
||||
|
||||
return data?.user?.id === user.id;
|
||||
}
|
||||
|
||||
@Query(() => [WorkspaceType], {
|
||||
description: 'Get all accessible workspaces for current user',
|
||||
complexity: 2,
|
||||
})
|
||||
async workspaces(@CurrentUser() user: User) {
|
||||
const data = await this.prisma.userWorkspacePermission.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
},
|
||||
include: {
|
||||
workspace: true,
|
||||
},
|
||||
});
|
||||
|
||||
return data.map(({ workspace, type }) => {
|
||||
return {
|
||||
...workspace,
|
||||
permission: type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 10,
|
||||
ttl: 30,
|
||||
},
|
||||
})
|
||||
@Public()
|
||||
@Query(() => WorkspaceType, {
|
||||
description: 'Get public workspace by id',
|
||||
})
|
||||
async publicWorkspace(@Args('id') id: string) {
|
||||
const workspace = await this.prisma.workspace.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (workspace?.public) {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
throw new NotFoundException("Workspace doesn't exist");
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceType, {
|
||||
description: 'Get workspace by id',
|
||||
})
|
||||
async workspace(@CurrentUser() user: UserType, @Args('id') id: string) {
|
||||
await this.permissions.check(id, user.id);
|
||||
const workspace = await this.prisma.workspace.findUnique({ where: { id } });
|
||||
|
||||
if (!workspace) {
|
||||
throw new NotFoundException("Workspace doesn't exist");
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@Mutation(() => WorkspaceType, {
|
||||
description: 'Create a new workspace',
|
||||
})
|
||||
async createWorkspace(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args({ name: 'init', type: () => GraphQLUpload })
|
||||
update: FileUpload
|
||||
) {
|
||||
// convert stream to buffer
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
const stream = update.createReadStream();
|
||||
const chunks: Uint8Array[] = [];
|
||||
stream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
});
|
||||
|
||||
const workspace = await this.prisma.workspace.create({
|
||||
data: {
|
||||
public: false,
|
||||
users: {
|
||||
create: {
|
||||
type: Permission.Owner,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (buffer.length) {
|
||||
await this.prisma.snapshot.create({
|
||||
data: {
|
||||
id: workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
blob: buffer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@Mutation(() => WorkspaceType, {
|
||||
description: 'Update workspace',
|
||||
})
|
||||
async updateWorkspace(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args({ name: 'input', type: () => UpdateWorkspaceInput })
|
||||
{ id, ...updates }: UpdateWorkspaceInput
|
||||
) {
|
||||
await this.permissions.check(id, user.id, Permission.Admin);
|
||||
|
||||
return this.prisma.workspace.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: updates,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async deleteWorkspace(@CurrentUser() user: UserType, @Args('id') id: string) {
|
||||
await this.permissions.check(id, user.id, Permission.Owner);
|
||||
|
||||
await this.prisma.workspace.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.update.deleteMany({
|
||||
where: {
|
||||
workspaceId: id,
|
||||
},
|
||||
}),
|
||||
this.prisma.snapshot.deleteMany({
|
||||
where: {
|
||||
workspaceId: id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
async invite(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('email') email: string,
|
||||
@Args('permission', { type: () => Permission }) permission: Permission,
|
||||
@Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean
|
||||
) {
|
||||
await this.permissions.check(workspaceId, user.id, Permission.Admin);
|
||||
|
||||
if (permission === Permission.Owner) {
|
||||
throw new ForbiddenException('Cannot change owner');
|
||||
}
|
||||
|
||||
const target = await this.users.findUserByEmail(email);
|
||||
|
||||
if (target) {
|
||||
const originRecord = await this.prisma.userWorkspacePermission.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
userId: target.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (originRecord) {
|
||||
return originRecord.id;
|
||||
}
|
||||
|
||||
const inviteId = await this.permissions.grant(
|
||||
workspaceId,
|
||||
target.id,
|
||||
permission
|
||||
);
|
||||
if (sendInviteMail) {
|
||||
const inviteInfo = await this.getInviteInfo(inviteId);
|
||||
|
||||
try {
|
||||
await this.mailer.sendInviteEmail(email, inviteId, {
|
||||
workspace: {
|
||||
id: inviteInfo.workspace.id,
|
||||
name: inviteInfo.workspace.name,
|
||||
avatar: inviteInfo.workspace.avatar,
|
||||
},
|
||||
user: {
|
||||
avatar: inviteInfo.user?.avatarUrl || '',
|
||||
name: inviteInfo.user?.name || '',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const ret = await this.permissions.revoke(workspaceId, target.id);
|
||||
|
||||
if (!ret) {
|
||||
this.logger.fatal(
|
||||
`failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}`
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}`
|
||||
);
|
||||
}
|
||||
|
||||
return new InternalServerErrorException(e);
|
||||
}
|
||||
}
|
||||
return inviteId;
|
||||
} else {
|
||||
const user = await this.auth.createAnonymousUser(email);
|
||||
const inviteId = await this.permissions.grant(
|
||||
workspaceId,
|
||||
user.id,
|
||||
permission
|
||||
);
|
||||
if (sendInviteMail) {
|
||||
const inviteInfo = await this.getInviteInfo(inviteId);
|
||||
|
||||
try {
|
||||
await this.mailer.sendInviteEmail(email, inviteId, {
|
||||
workspace: {
|
||||
id: inviteInfo.workspace.id,
|
||||
name: inviteInfo.workspace.name,
|
||||
avatar: inviteInfo.workspace.avatar,
|
||||
},
|
||||
user: {
|
||||
avatar: inviteInfo.user?.avatarUrl || '',
|
||||
name: inviteInfo.user?.name || '',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const ret = await this.permissions.revoke(workspaceId, user.id);
|
||||
|
||||
if (!ret) {
|
||||
this.logger.fatal(
|
||||
`failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}`
|
||||
);
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}`
|
||||
);
|
||||
}
|
||||
|
||||
return new InternalServerErrorException(e);
|
||||
}
|
||||
}
|
||||
return inviteId;
|
||||
}
|
||||
}
|
||||
|
||||
@Throttle({
|
||||
default: {
|
||||
limit: 10,
|
||||
ttl: 30,
|
||||
},
|
||||
})
|
||||
@Public()
|
||||
@Query(() => InvitationType, {
|
||||
description: 'Update workspace',
|
||||
})
|
||||
async getInviteInfo(@Args('inviteId') inviteId: string) {
|
||||
const workspaceId = await this.prisma.userWorkspacePermission
|
||||
.findUniqueOrThrow({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
select: {
|
||||
workspaceId: true,
|
||||
},
|
||||
})
|
||||
.then(({ workspaceId }) => workspaceId);
|
||||
|
||||
const snapshot = await this.prisma.snapshot.findFirstOrThrow({
|
||||
where: {
|
||||
id: workspaceId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const doc = new Doc();
|
||||
|
||||
applyUpdate(doc, new Uint8Array(snapshot.blob));
|
||||
const metaJSON = doc.getMap('meta').toJSON();
|
||||
|
||||
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
|
||||
const invitee = await this.permissions.getInvitationById(
|
||||
inviteId,
|
||||
workspaceId
|
||||
);
|
||||
|
||||
let avatar = '';
|
||||
|
||||
if (metaJSON.avatar) {
|
||||
const avatarBlob = await this.storage.getBlob(
|
||||
workspaceId,
|
||||
metaJSON.avatar
|
||||
);
|
||||
avatar = avatarBlob?.data.toString('base64') || '';
|
||||
}
|
||||
|
||||
return {
|
||||
workspace: {
|
||||
name: metaJSON.name || '',
|
||||
avatar: avatar || defaultWorkspaceAvatar,
|
||||
id: workspaceId,
|
||||
},
|
||||
user: owner.user,
|
||||
invitee: invitee.user,
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async revoke(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('userId') userId: string
|
||||
) {
|
||||
await this.permissions.check(workspaceId, user.id, Permission.Admin);
|
||||
|
||||
return this.permissions.revoke(workspaceId, userId);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@Public()
|
||||
async acceptInviteById(
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('inviteId') inviteId: string,
|
||||
@Args('sendAcceptMail', { nullable: true }) sendAcceptMail: boolean
|
||||
) {
|
||||
const {
|
||||
invitee,
|
||||
user: inviter,
|
||||
workspace,
|
||||
} = await this.getInviteInfo(inviteId);
|
||||
|
||||
if (!inviter || !invitee) {
|
||||
throw new ForbiddenException(
|
||||
`can not find inviter/invitee by inviteId: ${inviteId}`
|
||||
);
|
||||
}
|
||||
|
||||
if (sendAcceptMail) {
|
||||
await this.mailer.sendAcceptedEmail(inviter.email, {
|
||||
inviteeName: invitee.name,
|
||||
workspaceName: workspace.name,
|
||||
});
|
||||
}
|
||||
|
||||
return this.permissions.acceptById(workspaceId, inviteId);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async acceptInvite(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string
|
||||
) {
|
||||
return this.permissions.accept(workspaceId, user.id);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async leaveWorkspace(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('workspaceName') workspaceName: string,
|
||||
@Args('sendLeaveMail', { nullable: true }) sendLeaveMail: boolean
|
||||
) {
|
||||
await this.permissions.check(workspaceId, user.id);
|
||||
|
||||
const owner = await this.permissions.getWorkspaceOwner(workspaceId);
|
||||
|
||||
if (!owner.user) {
|
||||
throw new ForbiddenException(
|
||||
`can not find owner by workspaceId: ${workspaceId}`
|
||||
);
|
||||
}
|
||||
|
||||
if (sendLeaveMail) {
|
||||
await this.mailer.sendLeaveWorkspaceEmail(owner.user.email, {
|
||||
workspaceName,
|
||||
inviteeName: user.name,
|
||||
});
|
||||
}
|
||||
|
||||
return this.permissions.revoke(workspaceId, user.id);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async sharePage(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('pageId') pageId: string
|
||||
) {
|
||||
const docId = new DocID(pageId, workspaceId);
|
||||
|
||||
if (docId.isWorkspace) {
|
||||
throw new ForbiddenException('Expect page not to be workspace');
|
||||
}
|
||||
|
||||
const userWorkspace = await this.prisma.userWorkspacePermission.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
workspaceId: docId.workspace,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userWorkspace?.accepted) {
|
||||
throw new ForbiddenException('Permission denied');
|
||||
}
|
||||
|
||||
return this.permissions.grantPage(docId.workspace, docId.guid);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async revokePage(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('pageId') pageId: string
|
||||
) {
|
||||
const docId = new DocID(pageId, workspaceId);
|
||||
|
||||
if (docId.isWorkspace) {
|
||||
throw new ForbiddenException('Expect page not to be workspace');
|
||||
}
|
||||
|
||||
await this.permissions.check(docId.workspace, user.id, Permission.Admin);
|
||||
|
||||
return this.permissions.revokePage(docId.workspace, docId.guid);
|
||||
}
|
||||
|
||||
@Query(() => [String], {
|
||||
description: 'List blobs of workspace',
|
||||
})
|
||||
async listBlobs(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string
|
||||
) {
|
||||
await this.permissions.check(workspaceId, user.id);
|
||||
|
||||
return this.storage.listBlobs(workspaceId);
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceBlobSizes)
|
||||
async collectBlobSizes(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string
|
||||
) {
|
||||
await this.permissions.check(workspaceId, user.id);
|
||||
|
||||
return this.storage.blobsSize([workspaceId]).then(size => ({ size }));
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceBlobSizes)
|
||||
async collectAllBlobSizes(@CurrentUser() user: UserType) {
|
||||
const workspaces = await this.prisma.userWorkspacePermission
|
||||
.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
accepted: true,
|
||||
type: Permission.Owner,
|
||||
},
|
||||
select: {
|
||||
workspace: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(data => data.map(({ workspace }) => workspace.id));
|
||||
|
||||
const size = await this.storage.blobsSize(workspaces);
|
||||
return { size };
|
||||
}
|
||||
|
||||
@Query(() => WorkspaceBlobSizes)
|
||||
async checkBlobSize(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('size', { type: () => Float }) size: number
|
||||
) {
|
||||
const canWrite = await this.permissions.tryCheck(
|
||||
workspaceId,
|
||||
user.id,
|
||||
Permission.Write
|
||||
);
|
||||
if (canWrite) {
|
||||
const { user } = await this.permissions.getWorkspaceOwner(workspaceId);
|
||||
if (user) {
|
||||
const quota = await this.users.getStorageQuotaById(user.id);
|
||||
const { size: currentSize } = await this.collectAllBlobSizes(user);
|
||||
|
||||
return { size: quota - (size + currentSize) };
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
async setBlob(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args({ name: 'blob', type: () => GraphQLUpload })
|
||||
blob: FileUpload
|
||||
) {
|
||||
await this.permissions.check(workspaceId, user.id, Permission.Write);
|
||||
|
||||
// quota was apply to owner's account
|
||||
const { user: owner } =
|
||||
await this.permissions.getWorkspaceOwner(workspaceId);
|
||||
if (!owner) return new NotFoundException('Workspace owner not found');
|
||||
const quota = await this.users.getStorageQuotaById(owner.id);
|
||||
const { size } = await this.collectAllBlobSizes(owner);
|
||||
|
||||
const checkExceeded = (recvSize: number) => {
|
||||
if (size + recvSize > quota) {
|
||||
this.logger.log(
|
||||
`storage size limit exceeded: ${size + recvSize} > ${quota}`
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (checkExceeded(0)) {
|
||||
throw new ForbiddenException('storage size limit exceeded');
|
||||
}
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
const stream = blob.createReadStream();
|
||||
const chunks: Uint8Array[] = [];
|
||||
stream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
|
||||
// check size after receive each chunk to avoid unnecessary memory usage
|
||||
const bufferSize = chunks.reduce((acc, cur) => acc + cur.length, 0);
|
||||
if (checkExceeded(bufferSize)) {
|
||||
reject(new ForbiddenException('storage size limit exceeded'));
|
||||
}
|
||||
});
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
|
||||
if (checkExceeded(buffer.length)) {
|
||||
reject(new ForbiddenException('storage size limit exceeded'));
|
||||
} else {
|
||||
resolve(buffer);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return this.storage.uploadBlob(workspaceId, buffer);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async deleteBlob(
|
||||
@CurrentUser() user: UserType,
|
||||
@Args('workspaceId') workspaceId: string,
|
||||
@Args('hash') hash: string
|
||||
) {
|
||||
await this.permissions.check(workspaceId, user.id);
|
||||
|
||||
return this.storage.deleteBlob(workspaceId, hash);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user