Skip to content

Commit

Permalink
✨ Handling quota limit error (#437)
Browse files Browse the repository at this point in the history
  • Loading branch information
MontaGhanmy authored Mar 26, 2024
1 parent 389d42f commit cb5a2ad
Show file tree
Hide file tree
Showing 14 changed files with 235 additions and 35 deletions.
40 changes: 37 additions & 3 deletions tdrive/backend/node/src/services/documents/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import {
RealtimeEntityActionType,
ResourcePath,
} from "../../../core/platform/services/realtime/types";

import config from "config";
export class DocumentsService {
version: "1";
repository: Repository<DriveFile>;
Expand All @@ -69,6 +69,12 @@ export class DocumentsService {
driveTdriveTabRepository: Repository<DriveTdriveTabEntity>;
ROOT: RootType = "root";
TRASH: TrashType = "trash";
quotaEnabled: boolean = config.has("drive.featureUserQuota")
? config.get("drive.featureUserQuota")
: false;
defaultQuota: number = config.has("drive.defaultUserQuota")
? config.get("drive.defaultUserQuota")
: 0;
logger: TdriveLogger = getLogger("Documents Service");

async init(): Promise<this> {
Expand Down Expand Up @@ -328,6 +334,19 @@ export class DocumentsService {
}

if (fileToProcess) {
if (this.quotaEnabled) {
const userQuota = await this.userQuota(context);
const leftQuota = this.defaultQuota - userQuota;

if (fileToProcess.upload_data.size > leftQuota) {
// clean up everything
await globalResolver.services.files.delete(fileToProcess.id, context);
throw new CrudException(
`Not enough space: ${fileToProcess.upload_data.size}, ${leftQuota}.`,
403,
);
}
}
driveItem.size = fileToProcess.upload_data.size;
driveItem.is_directory = false;
driveItem.extension = fileToProcess.metadata.name.split(".").pop();
Expand Down Expand Up @@ -774,6 +793,17 @@ export class DocumentsService {
const driveItemVersion = getDefaultDriveItemVersion(version, context);
const metadata = await getFileMetadata(driveItemVersion.file_metadata.external_id, context);

if (this.quotaEnabled) {
const userQuota = await this.userQuota(context);
const leftQuota = this.defaultQuota - userQuota;

if (metadata.size > leftQuota) {
// clean up everything
await globalResolver.services.files.delete(metadata.external_id, context);
throw new CrudException(`Not enough space: ${metadata.size}, ${leftQuota}.`, 403);
}
}

driveItemVersion.file_size = metadata.size;
driveItemVersion.file_metadata.size = metadata.size;
driveItemVersion.file_metadata.name = metadata.name;
Expand Down Expand Up @@ -820,8 +850,12 @@ export class DocumentsService {

return driveItemVersion;
} catch (error) {
this.logger.error({ error: `${error}` }, "Failed to create Drive item version");
throw new CrudException("Failed to create Drive item version", 500);
logger.error({ error: `${error}` }, "Failed to create Drive item version");
// if error code is 403, it means the user exceeded the quota limit
if (error.code === 403) {
CrudException.throwMe(error, new CrudException("Quota limit exceeded", 403));
}
CrudException.throwMe(error, new CrudException("Failed to create Drive item version", 500));
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class DocumentsController {
version: Partial<FileVersion>;
};
}>,
): Promise<DriveFile> => {
): Promise<DriveFile | any> => {
try {
const context = getDriveExecutionContext(request);

Expand All @@ -73,6 +73,7 @@ export class DocumentsController {

const { item, version } = request.body;

//
return await globalResolver.services.documents.documents.create(
createdFile,
item,
Expand Down Expand Up @@ -305,14 +306,19 @@ export class DocumentsController {
Body: Partial<FileVersion>;
Querystring: { public_token?: string };
}>,
): Promise<FileVersion> => {
const context = getDriveExecutionContext(request);
const { id } = request.params;
const version = request.body;
): Promise<FileVersion | any> => {
try {
const context = getDriveExecutionContext(request);
const { id } = request.params;
const version = request.body;

if (!id) throw new CrudException("Missing id", 400);
if (!id) throw new CrudException("Missing id", 400);

return await globalResolver.services.documents.documents.createVersion(id, version, context);
return await globalResolver.services.documents.documents.createVersion(id, version, context);
} catch (error) {
logger.error({ error: `${error}` }, "Failed to create Drive item version");
CrudException.throwMe(error, new CrudException("Failed to create Drive item version", 500));
}
};

downloadGetToken = async (
Expand Down
10 changes: 8 additions & 2 deletions tdrive/backend/node/test/e2e/common/user-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,14 @@ export default class UserApi {
};

async createDocumentFromFilename(
file_name: "sample.png",
parent_id = "root"
file_name:
| "sample.png"
| "sample.doc"
| "sample.pdf"
| "sample.zip"
| "sample.mp4"
| "sample.gif",
parent_id = "root",
) {
const file = await this.uploadFile(file_name);

Expand Down
116 changes: 116 additions & 0 deletions tdrive/backend/node/test/e2e/documents/documents-quota.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
import { init, TestPlatform } from "../setup";
import UserApi from "../common/user-api";
import config from "config";
import { e2e_createDocumentFile, e2e_createVersion } from "./utils";
import { deserialize } from "class-transformer";
import { ResourceUpdateResponse } from "../../../src/utils/types";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
jest.mock("config");

describe("the Drive feature", () => {
const filesUrl = "/internal/services/files/v1";
let platform: TestPlatform;
let currentUser: UserApi;
let configHasSpy: jest.SpyInstance;
let configGetSpy: jest.SpyInstance;

beforeEach(async () => {
configHasSpy = jest.spyOn(config, "has");
configGetSpy = jest.spyOn(config, "get");

configHasSpy.mockImplementation((setting: string) => {
const value = jest.requireActual("config").has(setting);
return value;
});
configGetSpy.mockImplementation((setting: string) => {
if (setting === "drive.featureUserQuota") {
return true;
}
if (setting === "drive.defaultUserQuota") {
return 2000000;
}
return jest.requireActual("config").get(setting);
});
platform = await init({
services: [
"webserver",
"database",
"applications",
"search",
"storage",
"message-queue",
"user",
"search",
"files",
"websocket",
"messages",
"auth",
"realtime",
"channels",
"counter",
"statistics",
"platform-services",
"documents",
],
});
currentUser = await UserApi.getInstance(platform);
});

afterEach(async () => {
await platform?.tearDown();
platform = null;
configGetSpy.mockRestore();
});

it("did create the drive item with size under quota", async () => {
const result = await currentUser.uploadFileAndCreateDocument("sample.doc");
expect(result).toBeDefined();
});

it("did not upload the drive item with size above quota", async () => {
const item = await currentUser.uploadFile("sample.mp4");
expect(item).toBeDefined();
const result: any = await currentUser.createDocumentFromFile(item);
expect(result).toBeDefined();
expect(result.statusCode).toBe(403);
expect(result.error).toBe("Forbidden");
expect(result.message).toContain("Not enough space");
const fileDownloadResponse = await platform.app.inject({
method: "GET",
url: `${filesUrl}/companies/${platform.workspace.company_id}/files/${item.id}/download`,
});
// make sure the file was removed
expect(fileDownloadResponse).toBeTruthy();
expect(fileDownloadResponse.statusCode).toBe(404);
});

it("did create a version for a drive item", async () => {
const item = await currentUser.createDefaultDocument();
const fileUploadResponse = await e2e_createDocumentFile(
platform,
"../common/assets/sample.mp4",
);
const fileUploadResult: any = deserialize<ResourceUpdateResponse<File>>(
ResourceUpdateResponse,
fileUploadResponse.body,
);
const file_metadata = { external_id: fileUploadResult.resource.id };

const result: any = await e2e_createVersion(platform, item.id, {
filename: "file2",
file_metadata,
});
expect(result).toBeDefined();
expect(result.statusCode).toBe(403);
const fileDownloadResponse = await platform.app.inject({
method: "GET",
url: `${filesUrl}/companies/${platform.workspace.company_id}/files/${item.id}/download`,
});
// make sure the file was removed
expect(fileDownloadResponse).toBeTruthy();
expect(fileDownloadResponse.statusCode).toBe(404);
});
});
5 changes: 3 additions & 2 deletions tdrive/backend/node/test/e2e/documents/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ export const e2e_createVersion = async (
});
};

export const e2e_createDocumentFile = async (platform: TestPlatform) => {
const filePath = `${__dirname}/assets/test.txt`;
export const e2e_createDocumentFile = async (platform: TestPlatform, documentPath?: string) => {
const subFilePath = documentPath ?? "assets/test.txt";
const filePath = `${__dirname}/${subFilePath}`;
const token = await platform.auth.getJWTToken();
const form = formAutoContent({ file: fs.createReadStream(filePath) });
form.headers["authorization"] = `Bearer ${token}`;
Expand Down
3 changes: 3 additions & 0 deletions tdrive/frontend/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@
"compenents.VersionModalContent_donwload":"Download",
"hooks.use-drive-actions.unable_load_file":"Unable to load your files.",
"hooks.use-drive-actions.unable_create_file":"Unable to create a new file.",
"hooks.use-drive-actions.quota_limit_exceeded_title":"You're out of storage",
"hooks.use-drive-actions.quota_limit_exceeded_message":"You have reached the limit of your storage. Please, delete some files to vacate more space and have access to all features of Twake Drive.",
"hooks.use-drive-actions.quota_limit_exceeded_plans": "Our solution is now free for all users, but additional plans are coming soon.",
"hooks.use-drive-actions.unable_download_file":"Unable to download this files.",
"hooks.use-drive-actions.unable_remove_file":"Unable to remove this file.",
"hooks.use-drive-actions.unable_restore_file":"Unable to restore this item.",
Expand Down
3 changes: 3 additions & 0 deletions tdrive/frontend/public/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@
"compenents.VersionModalContent_donwload": "Télécharger",
"hooks.use-drive-actions.unable_load_file": "Impossible de charger cet élément",
"hooks.use-drive-actions.unable_create_file": "Impossible de créer ce fichier",
"hooks.use-drive-actions.quota_limit_exceeded_title": "Vous avez épuisé votre espace de stockage",
"hooks.use-drive-actions.quota_limit_exceeded_message": "Vous avez atteint la limite de votre espace de stockage. Veuillez supprimer certains fichiers pour libérer plus d'espace et avoir accès à toutes les fonctionnalités de Twake Drive.",
"hooks.use-drive-actions.quota_limit_exceeded_plans": "Notre solution est désormais gratuite pour tous les utilisateurs, mais des plans supplémentaires seront bientôt disponibles.",
"hooks.use-drive-actions.unable_download_file": "Impossible de télécharger ce fichier",
"hooks.use-drive-actions.unable_remove_file": "Impossible de supprimer ce fichier",
"hooks.use-drive-actions.unable_restore_file": "Impossible de restaurer cet élément.",
Expand Down
3 changes: 3 additions & 0 deletions tdrive/frontend/public/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@
"compenents.VersionModalContent_donwload":"Download",
"hooks.use-drive-actions.unable_load_file":"Unable to load your files.",
"hooks.use-drive-actions.unable_create_file":"Unable to create a new file.",
"hooks.use-drive-actions.quota_limit_exceeded_title": "У вас закончилось место для хранения",
"hooks.use-drive-actions.quota_limit_exceeded_message": "Вы достигли предела вашего места для хранения. Пожалуйста, удалите некоторые файлы, чтобы освободить больше места и получить доступ ко всем функциям Twake Drive.",
"hooks.use-drive-actions.quota_limit_exceeded_plans": "Наше решение теперь бесплатно для всех пользователей, но скоро будут доступны дополнительные планы.",
"hooks.use-drive-actions.unable_download_file":"Unable to download this files.",
"hooks.use-drive-actions.unable_remove_file":"Unable to remove this file.",
"hooks.use-drive-actions.unable_restore_file":"Unable to restore this item.",
Expand Down
3 changes: 3 additions & 0 deletions tdrive/frontend/public/locales/vn.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@
"compenents.VersionModalContent_donwload": "Tải xuống",
"hooks.use-drive-actions.unable_load_file": "Không thể tải tệp của bạn.",
"hooks.use-drive-actions.unable_create_file": "Không thể tạo tệp mới.",
"hooks.use-drive-actions.quota_limit_exceeded_title": "Bạn đã hết dung lượng lưu trữ",
"hooks.use-drive-actions.quota_limit_exceeded_message": "Bạn đã đạt đến giới hạn dung lượng lưu trữ của mình. Vui lòng xóa một số tệp để giải phóng thêm không gian và truy cập vào tất cả các tính năng của Twake Drive.",
"hooks.use-drive-actions.quota_limit_exceeded_plans": "Giải pháp của chúng tôi hiện đã miễn phí cho tất cả người dùng, nhưng các gói bổ sung sẽ sớm có mặt.",
"hooks.use-drive-actions.unable_download_file": "Không thể tải xuống các tệp này.",
"hooks.use-drive-actions.unable_remove_file": "Không thể xóa tệp này.",
"hooks.use-drive-actions.unable_restore_file": "Không thể khôi phục mục này.",
Expand Down
24 changes: 17 additions & 7 deletions tdrive/frontend/src/app/features/drive/api-client/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,23 @@ export class DriveApiClient {
data: { item: Partial<DriveItem>; version?: Partial<DriveItemVersion> },
) {
if (!data.version) data.version = {} as Partial<DriveItemVersion>;
return await Api.post<
{ item: Partial<DriveItem>; version: Partial<DriveItemVersion> },
DriveItem
>(
`/internal/services/documents/v1/companies/${companyId}/item${appendTdriveToken()}`,
data as { item: Partial<DriveItem>; version: Partial<DriveItemVersion> },
);

return new Promise<DriveItem>((resolve, reject) => {
Api.post<
{ item: Partial<DriveItem>; version: Partial<DriveItemVersion> },
DriveItem
>(
`/internal/services/documents/v1/companies/${companyId}/item${appendTdriveToken()}`,
data as { item: Partial<DriveItem>; version: Partial<DriveItemVersion> },
(res) => {
if ((res as any)?.statusCode || (res as any)?.error) {
reject(res);
}

resolve(res);
},
);
});
}

static async createVersion(companyId: string, id: string, version: Partial<DriveItemVersion>) {
Expand Down
33 changes: 23 additions & 10 deletions tdrive/frontend/src/app/features/drive/hooks/use-drive-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DriveItemAtom, DriveItemChildrenAtom } from '../state/store';
import { BrowseFilter, DriveItem, DriveItemVersion } from '../types';
import { SharedWithMeFilterState } from '../state/shared-with-me-filter';
import Languages from 'features/global/services/languages-service';
import { useUserQuota } from "features/users/hooks/use-user-quota";
import { useUserQuota } from 'features/users/hooks/use-user-quota';

/**
* Returns the children of a drive item
Expand Down Expand Up @@ -48,17 +48,30 @@ export const useDriveActions = () => {

const create = useCallback(
async (item: Partial<DriveItem>, version: Partial<DriveItemVersion>) => {
if (!item || !version) throw new Error("All ");
let driveFile = null;
if (!item || !version) throw new Error('All ');
if (!item.company_id) item.company_id = companyId;

try {
driveFile = await DriveApiClient.create(companyId, { item, version });
await refresh(driveFile.parent_id!);
const driveFile = await DriveApiClient.create(companyId, { item, version });

await refresh(driveFile.parent_id);
await getQuota();
} catch (e) {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_create_file'));

return driveFile;
} catch (e: any) {
if (e.statusCode === 403) {
ToasterService.info(
<>
<p>{Languages.t('hooks.use-drive-actions.quota_limit_exceeded_title')}</p>
<p>{Languages.t('hooks.use-drive-actions.quota_limit_exceeded_message')}</p>
<p>{Languages.t('hooks.use-drive-actions.quota_limit_exceeded_plans')}</p>
</>,
);
} else {
ToasterService.error(Languages.t('hooks.use-drive-actions.unable_create_file'));
}
return null;
}
return driveFile;
},
[refresh],
);
Expand Down Expand Up @@ -132,8 +145,8 @@ export const useDriveActions = () => {
const updateBody = {
company_id: companyId,
user_id: userId,
level: level
}
level: level,
};
await DriveApiClient.updateLevel(companyId, id, updateBody);
await refresh(id || '');
} catch (e) {
Expand Down
Loading

0 comments on commit cb5a2ad

Please sign in to comment.