Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const NameCollisionContainer: FC<NameCollisionContainerProps> = ({
[moveDestinationFolderId, currentFolderId],
);
const limits = useAppSelector(fileVersionsSelectors.getLimits);
const maxUploadFileSize = useAppSelector(fileVersionsSelectors.getMaxFileSizeLimit);
const isVersioningEnabled = limits?.versioning?.enabled ?? false;

const handleNewItems = (files: (File | DriveItemData)[], folders: (IRoot | DriveItemData)[]) => [
Expand Down Expand Up @@ -191,6 +192,7 @@ const NameCollisionContainer: FC<NameCollisionContainerProps> = ({
],
selectedWorkspace,
dispatch,
maxUploadFileSize,
});
} else {
const file = itemToUpload as File;
Expand All @@ -214,6 +216,7 @@ const NameCollisionContainer: FC<NameCollisionContainerProps> = ({
],
selectedWorkspace,
dispatch,
maxUploadFileSize,
}).then(() => {
dispatch(fetchSortedFolderContentThunk(folderId));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { useEffect, useState } from 'react';
import { fetchPlanPrices } from 'views/NewSettings/services/plansApi';
import { Translate } from 'app/i18n/types';
import { bytesToString } from '../../services/size.service';
import { fileVersionsSelectors } from 'app/store/slices/fileVersions';

const ReachedFileSizeLimitDialog = (): JSX.Element | null => {
const dispatch = useAppDispatch();
const { translate } = useTranslationContext();
const isOpen = useAppSelector((state) => state.ui.isReachedFileSizeLimitDialogOpen);
const fileSizeLimitInfo = useAppSelector((state) => state.ui.reachedFileSizeLimitDialogInfo);
const exceededFiles = fileSizeLimitInfo?.exceededFiles;
const maxFileSize = useAppSelector((state) => state.fileVersions.limits?.maxUploadFileSize) ?? 0;
const maxUploadFileSize = useAppSelector(fileVersionsSelectors.getMaxFileSizeLimit);
const selectedWorkspace = useAppSelector(workspacesSelectors.getSelectedWorkspace);
const individualSubscription = useAppSelector((state) => state.plan.individualSubscription);
const [availablePlans, setAvailablePlans] = useState<DisplayPrice[]>([]);
Expand Down Expand Up @@ -61,7 +62,7 @@ const ReachedFileSizeLimitDialog = (): JSX.Element | null => {
};
};

const text = getText(translate, maxFileSize, exceededFiles);
const text = getText(translate, maxUploadFileSize, exceededFiles);

const onClose = (): void => {
dispatch(
Expand Down
8 changes: 8 additions & 0 deletions src/app/drive/services/file.service/upload.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ export class BucketNotFoundError extends Error {
Object.setPrototypeOf(this, BucketNotFoundError.prototype);
}
}

export class FilesExceedsSizeLimitError extends Error {
constructor() {
super('Files exceeds the user size limit');
this.name = 'FilesExceedsSizeLimitError';
Object.setPrototypeOf(this, FilesExceedsSizeLimitError.prototype);
}
}
9 changes: 9 additions & 0 deletions src/app/drive/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ export interface ReachedPlanLimitDialogInfo {
hidePrimaryAction?: boolean;
}

export interface ExceededFile {
name: string;
size: number;
}

export interface ReachedFileSizeLimitDialogInfo {
exceededFiles: ExceededFile[];
}

export interface UpgradePlanDialogInfo {
title: string;
description: string;
Expand Down
74 changes: 72 additions & 2 deletions src/app/network/UploadFolderManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { createFolder } from 'app/store/slices/storage/folderUtils/createFolder'
import { checkFolderDuplicated } from 'app/store/slices/storage/folderUtils/checkFolderDuplicated';
import { getUniqueFolderName } from 'app/store/slices/storage/folderUtils/getUniqueFolderName';
import tasksService from 'app/tasks/services/tasks.service';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { beforeEach, describe, expect, it, Mock, test, vi } from 'vitest';
import { TaskFolder, UploadFoldersManager, uploadFoldersWithManager } from './UploadFolderManager';
import * as networkInformation from './networkInformation';
import { FilesExceedsSizeLimitError } from 'app/drive/services/file.service/upload.errors';
import { uploadItemsParallelThunk } from 'app/store/slices/storage/storage.thunks/uploadItemsThunk';

vi.mock('app/drive/services/new-storage.service', () => ({
default: {
Expand Down Expand Up @@ -143,6 +145,7 @@ describe('checkUploadFolders', () => {
],
selectedWorkspace: null,
dispatch: mockDispatch,
maxUploadFileSize: 100,
});

expect(createFolderSpy).toHaveBeenCalledOnce();
Expand Down Expand Up @@ -206,6 +209,7 @@ describe('checkUploadFolders', () => {
],
selectedWorkspace: null,
dispatch: mockDispatch,
maxUploadFileSize: 100,
});

expect(createFolderSpy).toHaveBeenCalledOnce();
Expand Down Expand Up @@ -299,6 +303,7 @@ describe('checkUploadFolders', () => {
],
selectedWorkspace: null,
dispatch: mockDispatch,
maxUploadFileSize: 100,
});

expect(createFolderSpy).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -357,12 +362,77 @@ describe('checkUploadFolders', () => {
],
selectedWorkspace: null,
dispatch: mockDispatch,
maxUploadFileSize: 100,
});

expect(logNetworkInfoMock).toHaveBeenCalledOnce();
expect(logNetworkInfoMock).toHaveBeenCalledWith({ folderName: 'MyFolder' });
});

describe('Handle File Uploads', () => {
const taskId = 'task-id';
const mockCreatedFolder: DriveFolderData = {
id: 0,
uuid: 'folder-uuid',
name: 'Folder1',
bucket: 'bucket',
parentId: 0,
parent_id: 0,
parentUuid: 'parentUuid',
userId: 0,
user_id: 0,
icon: null,
iconId: null,
icon_id: null,
isFolder: true,
color: null,
encrypt_version: null,
plain_name: 'Folder1',
deleted: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};

const buildManager = (maxUploadFileSize?: number) => {
const manager = new UploadFoldersManager([], null, mockDispatch, maxUploadFileSize ?? 100);
manager['tasksInfo'][taskId] = { progress: { itemsUploaded: 0, totalItems: 1 } };
return manager;
};

test('When all files exceed the size limit and there are no subfolders, then an error indicating so is thrown', async () => {
const bigFile = new File([new ArrayBuffer(200)], 'big.mp4');
vi.spyOn(tasksService, 'updateTask').mockReturnValue();
vi.spyOn(tasksService, 'getTasks').mockReturnValue([]);
const manager = buildManager(100);
const level = { childrenFiles: [bigFile], childrenFolders: [], folderId: 'f', name: 'F', fullPathEdited: '' };

await expect(
manager['handleFileUploads'](level, mockCreatedFolder, taskId, new AbortController()),
).rejects.toThrow(FilesExceedsSizeLimitError);
});

test('When a folder has files exceeding the size limit but also has subfolders, then it dispatches the upload', async () => {
const bigFile = new File([new ArrayBuffer(200)], 'big.mp4');
const dispatchWithUnwrap = vi.fn().mockReturnValue({ unwrap: () => Promise.resolve() });
vi.spyOn(tasksService, 'updateTask').mockReturnValue();
const manager = new UploadFoldersManager([], null, dispatchWithUnwrap, 100);
manager['tasksInfo'][taskId] = { progress: { itemsUploaded: 0, totalItems: 1 } };
const level = {
childrenFiles: [bigFile],
childrenFolders: [{ folderId: 'c', childrenFiles: [], childrenFolders: [], name: 'Child', fullPathEdited: '' }],
folderId: 'f',
name: 'F',
fullPathEdited: '',
};

await manager['handleFileUploads'](level, mockCreatedFolder, taskId, new AbortController());

expect(uploadItemsParallelThunk).toHaveBeenCalledWith(
expect.objectContaining({ files: level.childrenFiles, parentFolderId: mockCreatedFolder.uuid }),
);
});
});

it('should abort the upload if abortController is called', async () => {
const mockParentFolder: DriveFolderData = {
id: 1,
Expand Down Expand Up @@ -410,7 +480,7 @@ describe('checkUploadFolders', () => {
const selectedWorkspace = null;
const taskId = 'task-id';

const manager = new UploadFoldersManager(payload, selectedWorkspace, mockDispatch);
const manager = new UploadFoldersManager(payload, selectedWorkspace, mockDispatch, 100);
const abortController = new AbortController();

const taskFolder: TaskFolder = {
Expand Down
33 changes: 30 additions & 3 deletions src/app/network/UploadFolderManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { wait } from 'utils/timeUtils';
import { ConnectionLostError } from './requests';
import referralService from 'services/referral.service';
import { logNetworkInfoForUpload } from './networkInformation';
import { uiActions } from 'app/store/slices/ui';
import { FilesExceedsSizeLimitError } from 'app/drive/services/file.service/upload.errors';
import { filterFilesByMaxSize } from 'app/store/slices/storage/fileUtils/filterFilesByMaxSize';

interface UploadFolderPayload {
root: IRoot;
Expand Down Expand Up @@ -131,12 +134,14 @@ export const uploadFoldersWithManager = ({
payload,
selectedWorkspace,
dispatch,
maxUploadFileSize,
}: {
payload: UploadFolderPayload[];
selectedWorkspace: WorkspaceData | null;
dispatch: ThunkDispatch<RootState, unknown, AnyAction>;
maxUploadFileSize: number;
}): Promise<void> => {
const uploadFoldersManager = new UploadFoldersManager(payload, selectedWorkspace, dispatch);
const uploadFoldersManager = new UploadFoldersManager(payload, selectedWorkspace, dispatch, maxUploadFileSize);
return uploadFoldersManager.run();
};

Expand All @@ -148,17 +153,20 @@ export class UploadFoldersManager {
private readonly selectedWorkspace: WorkspaceData | null;
private readonly dispatch: ThunkDispatch<RootState, unknown, AnyAction>;
private readonly abortController?: AbortController;
private readonly maxUploadFileSize: number;

private tasksInfo: Record<string, TaskInfo> = {};

constructor(
payload: UploadFolderPayload[],
selectedWorkspace: WorkspaceData | null,
dispatch: ThunkDispatch<RootState, unknown, AnyAction>,
maxUploadFileSize: number,
) {
this.payload = payload;
this.selectedWorkspace = selectedWorkspace;
this.dispatch = dispatch;
this.maxUploadFileSize = maxUploadFileSize;
}

private readonly uploadFoldersQueue: QueueObject<TaskFolder> = queue<TaskFolder>(
Expand Down Expand Up @@ -238,6 +246,23 @@ export class UploadFoldersManager {
if (level.childrenFiles.length === 0) return;
if (abortController.signal.aborted) return;

const hasNoChildFolders = level.childrenFolders.length === 0;
const { exceededFiles } = filterFilesByMaxSize({
files: level.childrenFiles,
maxUploadFileSize: this.maxUploadFileSize,
});
const allFilesExceedSizeLimit =
exceededFiles.length === level.childrenFiles.length && level.childrenFiles.length > 0;

if (allFilesExceedSizeLimit && hasNoChildFolders) {
await this.stopUploadTask(taskId, abortController);
this.killQueueAndNotifyError(taskId);
this.dispatch(
uiActions.setOpenFileSizeLimitReachedDialog({ open: true, info: { exceededFiles: level.childrenFiles } }),
);
throw new FilesExceedsSizeLimitError();
}

await this.dispatch(
uploadItemsParallelThunk({
files: level.childrenFiles,
Expand Down Expand Up @@ -314,8 +339,10 @@ export class UploadFoldersManager {
// Deletes the root folder
const rootFolderItem = this.tasksInfo[taskId].rootFolderItem;
if (rootFolderItem) {
promises.push(this.dispatch(deleteItemsThunk([rootFolderItem as DriveItemData])).unwrap());
promises.push(newStorageService.deleteFolderByUuid(rootFolderItem.uuid));
promises.push(
this.dispatch(deleteItemsThunk([rootFolderItem as DriveItemData])).unwrap(),
newStorageService.deleteFolderByUuid(rootFolderItem.uuid),
);
}
await Promise.allSettled(promises);
};
Expand Down
Loading
Loading