Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PB-2375] bugfix/Check duplicated items before uploading on Drive web #1282

Merged
merged 11 commits into from
Oct 2, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@iconscout/react-unicons": "^1.1.6",
"@internxt/inxt-js": "=1.2.21",
"@internxt/lib": "^1.2.0",
"@internxt/sdk": "^1.5.15",
"@internxt/sdk": "^1.5.16",
"@phosphor-icons/react": "^2.1.7",
"@popperjs/core": "^2.11.6",
"@reduxjs/toolkit": "^1.6.0",
Expand Down
16 changes: 12 additions & 4 deletions src/app/drive/components/DriveExplorer/DriveExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,15 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => {
folderInputRef.current?.click();
}, [currentFolderId]);

const onUploadFileInputChanged = (e) => {
const onUploadFileInputChanged = async (e) => {
const files = e.target.files;

if (files.length <= UPLOAD_ITEMS_LIMIT) {
const unrepeatedUploadedFiles = handleRepeatedUploadingFiles(Array.from(files), items, dispatch) as File[];
const unrepeatedUploadedFiles = (await handleRepeatedUploadingFiles(
Array.from(files),
dispatch,
currentFolderId,
)) as File[];
dispatch(
storageThunks.uploadItemsThunk({
files: Array.from(unrepeatedUploadedFiles),
Expand Down Expand Up @@ -966,7 +970,7 @@ const uploadItems = async (props: DriveExplorerProps, rootList: IRoot[], files:
itemsDragged: items,
},
});
const unrepeatedUploadedFiles = handleRepeatedUploadingFiles(files, items, dispatch) as File[];
const unrepeatedUploadedFiles = (await handleRepeatedUploadingFiles(files, dispatch, currentFolderId)) as File[];
// files where dragged directly
await dispatch(
storageThunks.uploadItemsThunk({
Expand All @@ -991,7 +995,11 @@ const uploadItems = async (props: DriveExplorerProps, rootList: IRoot[], files:
itemsDragged: items,
},
});
const unrepeatedUploadedFolders = handleRepeatedUploadingFolders(rootList, items, dispatch) as IRoot[];
const unrepeatedUploadedFolders = (await handleRepeatedUploadingFolders(
rootList,
dispatch,
currentFolderId,
)) as IRoot[];

if (unrepeatedUploadedFolders.length > 0) {
const folderDataToUpload = unrepeatedUploadedFolders.map((root) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ConnectDragSource, ConnectDropTarget, useDrag, useDrop } from 'react-dnd';
import { NativeTypes } from 'react-dnd-html5-backend';
import { SdkFactory } from '../../../../../core/factory/sdk';
import { transformDraggedItems } from '../../../../../core/services/drag-and-drop.service';
import { DragAndDropType } from '../../../../../core/types';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
Expand Down Expand Up @@ -49,7 +48,6 @@ export const useDriveItemDrop = (item: DriveItemData): DriveItemDrop => {
const dispatch = useAppDispatch();
const isSomeItemSelected = useAppSelector(storageSelectors.isSomeItemSelected);
const { selectedItems } = useAppSelector((state) => state.storage);
const workspacesCredentials = useAppSelector((state) => state.workspaces.workspaceCredentials);
const namePath = useAppSelector((state) => state.storage.namePath);
const [{ isDraggingOverThisItem, canDrop }, connectDropTarget] = useDrop<
DriveItemData | DriveItemData[],
Expand Down Expand Up @@ -88,30 +86,10 @@ export const useDriveItemDrop = (item: DriveItemData): DriveItemDrop => {
return i.isFolder;
});

const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient();

dispatch(storageActions.setMoveDestinationFolderId(item.uuid));

const [folderContentPromise] = storageClient.getFolderContentByUuid(
item.uuid,
false,
workspacesCredentials?.tokenHeader,
);
const { children: foldersInDestinationFolder, files: filesInDestinationFolder } = await folderContentPromise;
const foldersInDestinationFolderParsed = foldersInDestinationFolder.map((folder) => ({
...folder,
isFolder: true,
}));
const unrepeatedFiles = handleRepeatedUploadingFiles(
filesToMove,
filesInDestinationFolder as DriveItemData[],
dispatch,
);
const unrepeatedFolders = handleRepeatedUploadingFolders(
foldersToMove,
foldersInDestinationFolderParsed as DriveItemData[],
dispatch,
);
const unrepeatedFiles = await handleRepeatedUploadingFiles(filesToMove, dispatch, item.uuid);
const unrepeatedFolders = await handleRepeatedUploadingFolders(foldersToMove, dispatch, item.uuid);
const unrepeatedItems: DriveItemData[] = [...unrepeatedFiles, ...unrepeatedFolders] as DriveItemData[];

if (unrepeatedItems.length === itemsToMove.length)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,17 @@ const NameCollisionContainer: FC<NameCollisionContainerProps> = ({
};

const keepAndMoveItem = async (itemsToUpload: DriveItemData[]) => {
await dispatch(storageThunks.renameItemsThunk({ items: itemsToUpload, destinationFolderId: folderId }));
dispatch(
storageThunks.moveItemsThunk({
await dispatch(
storageThunks.renameItemsThunk({
items: itemsToUpload,
destinationFolderId: moveDestinationFolderId as string,
destinationFolderId: folderId,
onRenameSuccess: (itemToUpload: DriveItemData) =>
dispatch(
storageThunks.moveItemsThunk({
items: [itemToUpload],
destinationFolderId: moveDestinationFolderId as string,
}),
),
}),
);
};
Expand Down
7 changes: 7 additions & 0 deletions src/app/drive/services/file.service/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface FileToUpload {
name: string;
size: number;
type: string;
content: File;
parentFolderId: string;
}
1 change: 1 addition & 0 deletions src/app/drive/services/file.service/uploadFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import notificationsService, { ToastType } from '../../../notifications/services
import { getEnvironmentConfig } from '../network.service';
import { generateThumbnailFromFile } from '../thumbnail.service';

// TODO: REMOVE FROM HERE, DUPLICATED TO MAKE TESTS
export interface FileToUpload {
name: string;
size: number;
Expand Down
28 changes: 27 additions & 1 deletion src/app/drive/services/new-storage.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { DriveFileData, FolderAncestor, FolderMeta, FolderTreeResponse } from '@internxt/sdk/dist/drive/storage/types';
import {
CheckDuplicatedFilesResponse,
CheckDuplicatedFoldersResponse,
DriveFileData,
FileStructure,
FolderAncestor,
FolderMeta,
FolderTreeResponse,
} from '@internxt/sdk/dist/drive/storage/types';
import { SdkFactory } from '../../core/factory/sdk';

export async function searchItemsByName(name: string): Promise<DriveFileData[]> {
Expand All @@ -23,11 +31,29 @@ export async function getFolderTree(uuid: string): Promise<FolderTreeResponse> {
return storageClient.getFolderTree(uuid);
}

export async function checkDuplicatedFiles(
folderUuid: string,
filesList: FileStructure[],
): Promise<CheckDuplicatedFilesResponse> {
const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient();
return storageClient.checkDuplicatedFiles({ folderUuid, filesList });
}

export async function checkDuplicatedFolders(
folderUuid: string,
folderNamesList: string[],
): Promise<CheckDuplicatedFoldersResponse> {
const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient();
return storageClient.checkDuplicatedFolders({ folderUuid, folderNamesList });
}

const newStorageService = {
searchItemsByName,
getFolderAncestors,
getFolderMeta,
getFolderTree,
checkDuplicatedFiles,
checkDuplicatedFolders,
};

export default newStorageService;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import storageSelectors from 'app/store/slices/storage/storage.selectors';
import storageThunks from 'app/store/slices/storage/storage.thunks';
import { DropTargetMonitor, useDrop } from 'react-dnd';
import { NativeTypes } from 'react-dnd-html5-backend';
import { SdkFactory } from '../../../../core/factory/sdk';
import { storageActions } from '../../../../store/slices/storage';
import {
handleRepeatedUploadingFiles,
Expand All @@ -27,7 +26,6 @@ interface BreadcrumbsItemProps {
const BreadcrumbsItem = (props: BreadcrumbsItemProps): JSX.Element => {
const dispatch = useAppDispatch();
const namePath = useAppSelector((state) => state.storage.namePath);
const workspacesCredentials = useAppSelector((state) => state.workspaces.workspaceCredentials);
const isSomeItemSelected = useAppSelector(storageSelectors.isSomeItemSelected);
const selectedItems = useAppSelector((state) => state.storage.selectedItems);

Expand All @@ -53,31 +51,10 @@ const BreadcrumbsItem = (props: BreadcrumbsItemProps): JSX.Element => {
});

dispatch(storageActions.setMoveDestinationFolderId(props.item.uuid));
const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient();

const [folderContentPromise] = storageClient.getFolderContentByUuid(
props.item.uuid,
false,
workspacesCredentials?.tokenHeader,
);

const { children: foldersInDestinationFolder, files: filesInDestinationFolder } = await folderContentPromise;

const foldersInDestinationFolderParsed = foldersInDestinationFolder.map((folder) => ({
...folder,
isFolder: true,
}));

const unrepeatedFiles = handleRepeatedUploadingFiles(
filesToMove,
filesInDestinationFolder as DriveItemData[],
dispatch,
);
const unrepeatedFolders = handleRepeatedUploadingFolders(
foldersToMove,
foldersInDestinationFolderParsed as DriveItemData[],
dispatch,
);
const folderUuid = props.item.uuid;
const unrepeatedFiles = await handleRepeatedUploadingFiles(filesToMove, dispatch, folderUuid);
const unrepeatedFolders = await handleRepeatedUploadingFolders(foldersToMove, dispatch, folderUuid);
const unrepeatedItems: DriveItemData[] = [...unrepeatedFiles, ...unrepeatedFolders] as DriveItemData[];

if (unrepeatedItems.length === itemsToMove.length) dispatch(storageActions.setMoveDestinationFolderId(null));
Expand Down
50 changes: 50 additions & 0 deletions src/app/store/slices/storage/fileUtils/checkDuplicatedFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { items as itemUtils } from '@internxt/lib';
import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types';
import newStorageService from '../../../../drive/services/new-storage.service';

export interface DuplicatedFilesResult {
duplicatedFilesResponse: DriveFileData[];
filesWithDuplicates: (File | DriveFileData)[];
filesWithoutDuplicates: (File | DriveFileData)[];
}

export const checkDuplicatedFiles = async (
files: (File | DriveFileData)[],
parentFolderId: string,
): Promise<DuplicatedFilesResult> => {
if (files.length === 0) {
return {
duplicatedFilesResponse: [],
filesWithDuplicates: [],
filesWithoutDuplicates: files,
} as DuplicatedFilesResult;
}

const filesToUploadParsedToCheck = files.map((file) => {
const { filename, extension } = itemUtils.getFilenameAndExt(file.name);
return { plainName: filename, type: extension };
});

const checkDuplicatedFileResponse = await newStorageService.checkDuplicatedFiles(
masterprog-cmd marked this conversation as resolved.
Show resolved Hide resolved
parentFolderId,
filesToUploadParsedToCheck,
);
const duplicatedFilesResponse = checkDuplicatedFileResponse.existentFiles;
const filesWithoutDuplicates: (File | DriveFileData)[] = [];
const filesWithDuplicates: (File | DriveFileData)[] = [];

files.forEach((file) => {
const { filename, extension } = itemUtils.getFilenameAndExt(file.name);
const isDuplicated = duplicatedFilesResponse.some(
(duplicatedFile) => duplicatedFile.plainName === filename && duplicatedFile.type === extension,
);

if (isDuplicated) {
filesWithDuplicates.push(file);
} else {
filesWithoutDuplicates.push(file);
}
});

return { duplicatedFilesResponse, filesWithoutDuplicates, filesWithDuplicates };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as internxtLib from '@internxt/lib';
import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types';
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import newStorageService from '../../../../drive/services/new-storage.service';
import { getUniqueFilename } from './getUniqueFilename';

jest.mock('../../../../drive/services/new-storage.service', () => ({
checkDuplicatedFiles: jest.fn(),
}));

describe('getUniqueFilename', () => {
let renameIfNeededSpy;

beforeEach(() => {
jest.clearAllMocks();
renameIfNeededSpy = jest.spyOn(internxtLib.items, 'renameIfNeeded');
});

it('should return the original name if no duplicates exist', async () => {
const filename = 'TestFile';
const extension = 'txt';
const duplicatedFiles = [] as DriveFileData[];
const parentFolderId = 'parent123';

(newStorageService.checkDuplicatedFiles as jest.Mock).mockResolvedValue({ existentFiles: [] });

const result = await getUniqueFilename(filename, extension, duplicatedFiles, parentFolderId);

expect(result).toBe(filename);
expect(newStorageService.checkDuplicatedFiles).toHaveBeenCalledWith(parentFolderId, [
{ plainName: filename, type: extension },
]);
expect(renameIfNeededSpy).toHaveBeenCalledWith([], filename, extension);
});

it('should rename the file if duplicates exist', async () => {
const filename = 'TestFile';
const extension = 'txt';
const duplicatedFiles = [{ name: 'TestFile.txt', plainName: 'TestFile', type: 'txt' }] as DriveFileData[];
const parentFolderId = 'parent123';

(newStorageService.checkDuplicatedFiles as jest.Mock)
.mockResolvedValueOnce({ existentFiles: [{ plainName: 'TestFile', type: 'txt' }] })
.mockResolvedValueOnce({ existentFiles: [] });

const result = await getUniqueFilename(filename, extension, duplicatedFiles, parentFolderId);

expect(result).toBe('TestFile (1)');
expect(newStorageService.checkDuplicatedFiles).toHaveBeenCalledTimes(2);
expect(renameIfNeededSpy).toHaveBeenCalledTimes(2);
});

it('should handle multiple renames if necessary', async () => {
const filename = 'TestFile';
const extension = 'txt';
const duplicatedFiles = [
{ name: 'TestFile.txt', plainName: 'TestFile', type: 'txt' },
{ name: 'TestFile (1).txt', plainName: 'TestFile (1)', type: 'txt' },
] as DriveFileData[];
const parentFolderId = 'parent123';

(newStorageService.checkDuplicatedFiles as jest.Mock)
.mockResolvedValueOnce({ existentFiles: [{ plainName: 'TestFile', type: 'txt' }] })
.mockResolvedValueOnce({ existentFiles: [{ plainName: 'TestFile (1)', type: 'txt' }] })
.mockResolvedValueOnce({ existentFiles: [] });

const result = await getUniqueFilename(filename, extension, duplicatedFiles, parentFolderId);

expect(result).toBe('TestFile (2)');
expect(newStorageService.checkDuplicatedFiles).toHaveBeenCalledTimes(3);
expect(renameIfNeededSpy).toHaveBeenCalledTimes(3);
});

it('should handle files with different extensions', async () => {
const filename = 'TestFile';
const extension = 'txt';
const duplicatedFiles = [
{ name: 'TestFile.txt', plainName: 'TestFile', type: 'txt' },
{ name: 'TestFile.pdf', plainName: 'TestFile', type: 'pdf' },
] as DriveFileData[];
const parentFolderId = 'parent123';

(newStorageService.checkDuplicatedFiles as jest.Mock)
.mockResolvedValueOnce({ existentFiles: [{ plainName: 'TestFile', type: 'txt' }] })
.mockResolvedValueOnce({ existentFiles: [] });

const result = await getUniqueFilename(filename, extension, duplicatedFiles, parentFolderId);

expect(result).toBe('TestFile (1)');
expect(newStorageService.checkDuplicatedFiles).toHaveBeenCalledTimes(2);
expect(renameIfNeededSpy).toHaveBeenCalledTimes(2);
});
});
Loading
Loading