From 71e134e78c251d06cec2d25e4eb671a95d96658f Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Wed, 18 Sep 2024 14:15:34 +0200 Subject: [PATCH 01/11] Added new endpoint for check duplicated files and folders and changes logic when upload or move (dragging) files or folders to use the name collision modal --- package.json | 2 +- .../DriveExplorer/DriveExplorer.tsx | 16 ++- .../hooks/useDriveItemDragAndDrop.tsx | 26 +---- .../NameCollisionContainer.tsx | 14 ++- src/app/drive/services/new-storage.service.ts | 28 ++++- .../BreadcrumbsItem/BreadcrumbsItem.tsx | 29 +---- .../fileNameUtils/checkDuplicatedFiles.ts | 50 ++++++++ .../fileNameUtils/getUniqueFilename.ts | 38 +++++++ .../slices/storage/fileNameUtils/index.ts | 1 + .../fileNameUtils/prepareFilesToUpload.ts | 50 ++++++++ .../fileNameUtils/processDuplicateFiles.ts | 48 ++++++++ .../folderNameUtils/checkFolderDuplicated.ts | 47 ++++++++ .../folderNameUtils/getUniqueFolderName.ts | 32 ++++++ .../storage.thunks/renameItemsThunk.ts | 107 +++++++----------- .../storage.thunks/uploadFolderThunk.ts | 27 +++-- .../storage.thunks/uploadItemsThunk.ts | 58 +--------- src/use_cases/trash/move-items-to-trash.ts | 10 +- yarn.lock | 10 +- 18 files changed, 388 insertions(+), 205 deletions(-) create mode 100644 src/app/store/slices/storage/fileNameUtils/checkDuplicatedFiles.ts create mode 100644 src/app/store/slices/storage/fileNameUtils/getUniqueFilename.ts create mode 100644 src/app/store/slices/storage/fileNameUtils/index.ts create mode 100644 src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts create mode 100644 src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts create mode 100644 src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts create mode 100644 src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts diff --git a/package.json b/package.json index f1716abe7..c216f441d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/drive/components/DriveExplorer/DriveExplorer.tsx b/src/app/drive/components/DriveExplorer/DriveExplorer.tsx index 6629b4a93..f91348450 100644 --- a/src/app/drive/components/DriveExplorer/DriveExplorer.tsx +++ b/src/app/drive/components/DriveExplorer/DriveExplorer.tsx @@ -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), @@ -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({ @@ -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) => ({ diff --git a/src/app/drive/components/DriveExplorer/DriveExplorerItem/hooks/useDriveItemDragAndDrop.tsx b/src/app/drive/components/DriveExplorer/DriveExplorerItem/hooks/useDriveItemDragAndDrop.tsx index 0fc0d0cb1..78677f861 100644 --- a/src/app/drive/components/DriveExplorer/DriveExplorerItem/hooks/useDriveItemDragAndDrop.tsx +++ b/src/app/drive/components/DriveExplorer/DriveExplorerItem/hooks/useDriveItemDragAndDrop.tsx @@ -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'; @@ -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[], @@ -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) diff --git a/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx b/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx index 9982ddaea..6318fe6b6 100644 --- a/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx +++ b/src/app/drive/components/NameCollisionDialog/NameCollisionContainer.tsx @@ -105,11 +105,17 @@ const NameCollisionContainer: FC = ({ }; 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, + }), + ), }), ); }; diff --git a/src/app/drive/services/new-storage.service.ts b/src/app/drive/services/new-storage.service.ts index 8b335d367..ac8c4e2e8 100644 --- a/src/app/drive/services/new-storage.service.ts +++ b/src/app/drive/services/new-storage.service.ts @@ -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 { @@ -23,11 +31,29 @@ export async function getFolderTree(uuid: string): Promise { return storageClient.getFolderTree(uuid); } +export async function checkDuplicatedFiles( + folderUuid: string, + filesList: FileStructure[], +): Promise { + const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); + return storageClient.checkDuplicatedFiles({ folderUuid, filesList }); +} + +export async function checkDuplicatedFolders( + folderUuid: string, + folderNamesList: string[], +): Promise { + const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); + return storageClient.checkDuplicatedFolders({ folderUuid, folderNamesList }); +} + const newStorageService = { searchItemsByName, getFolderAncestors, getFolderMeta, getFolderTree, + checkDuplicatedFiles, + checkDuplicatedFolders, }; export default newStorageService; diff --git a/src/app/shared/components/Breadcrumbs/BreadcrumbsItem/BreadcrumbsItem.tsx b/src/app/shared/components/Breadcrumbs/BreadcrumbsItem/BreadcrumbsItem.tsx index 279a20767..171a6041c 100644 --- a/src/app/shared/components/Breadcrumbs/BreadcrumbsItem/BreadcrumbsItem.tsx +++ b/src/app/shared/components/Breadcrumbs/BreadcrumbsItem/BreadcrumbsItem.tsx @@ -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, @@ -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); @@ -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)); diff --git a/src/app/store/slices/storage/fileNameUtils/checkDuplicatedFiles.ts b/src/app/store/slices/storage/fileNameUtils/checkDuplicatedFiles.ts new file mode 100644 index 000000000..b6597e17e --- /dev/null +++ b/src/app/store/slices/storage/fileNameUtils/checkDuplicatedFiles.ts @@ -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 => { + 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( + 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 }; +}; diff --git a/src/app/store/slices/storage/fileNameUtils/getUniqueFilename.ts b/src/app/store/slices/storage/fileNameUtils/getUniqueFilename.ts new file mode 100644 index 000000000..85eddd2db --- /dev/null +++ b/src/app/store/slices/storage/fileNameUtils/getUniqueFilename.ts @@ -0,0 +1,38 @@ +import { items as itemsUtils } from '@internxt/lib'; + +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; +import newStorageService from '../../../../drive/services/new-storage.service'; + +export const getUniqueFilename = async ( + filename: string, + extension: string, + duplicatedFiles: DriveFileData[], + parentFolderId: string, +): Promise => { + let isFileNewNameDuplicated = true; + let finalFilename = filename; + let currentDuplicatedFiles = duplicatedFiles; + + do { + const currentFolderFilesToCheckDuplicates = currentDuplicatedFiles.map((file) => ({ + name: file.plainName ?? file.name, + type: file.type, + })); + + const [, , renamedFilename] = itemsUtils.renameIfNeeded( + currentFolderFilesToCheckDuplicates, + finalFilename, + extension, + ); + + finalFilename = renamedFilename; + + const duplicatedFilesResponse = await newStorageService.checkDuplicatedFiles(parentFolderId, [ + { plainName: renamedFilename, type: extension }, + ]); + currentDuplicatedFiles = duplicatedFilesResponse.existentFiles; + isFileNewNameDuplicated = currentDuplicatedFiles.length > 0; + } while (isFileNewNameDuplicated); + + return finalFilename; +}; diff --git a/src/app/store/slices/storage/fileNameUtils/index.ts b/src/app/store/slices/storage/fileNameUtils/index.ts new file mode 100644 index 000000000..8c93fdf05 --- /dev/null +++ b/src/app/store/slices/storage/fileNameUtils/index.ts @@ -0,0 +1 @@ +export { prepareFilesToUpload } from './prepareFilesToUpload'; diff --git a/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts b/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts new file mode 100644 index 000000000..97ec1937b --- /dev/null +++ b/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts @@ -0,0 +1,50 @@ +import { FileToUpload } from '../../../../drive/services/file.service/uploadFile'; +import { checkDuplicatedFiles } from './checkDuplicatedFiles'; +import { processDuplicateFiles } from './processDuplicateFiles'; + +const BATCH_SIZE = 200; + +export const prepareFilesToUpload = async ({ + files, + parentFolderId, + disableDuplicatedNamesCheck, + fileType, +}: { + files: File[]; + parentFolderId: string; + disableDuplicatedNamesCheck?: boolean; + fileType?: string; +}): Promise<{ filesToUpload: FileToUpload[]; zeroLengthFilesNumber: number }> => { + const filesToUpload: FileToUpload[] = []; + let zeroLengthFilesNumber = 0; + + const processFilesBatch = async (filesBatch: File[]) => { + const { duplicatedFilesResponse, filesWithoutDuplicates, filesWithDuplicates } = await checkDuplicatedFiles( + filesBatch, + parentFolderId, + ); + + zeroLengthFilesNumber += await processDuplicateFiles({ + filesWithDuplicates: filesWithoutDuplicates as File[], + filesToUpload, + fileType, + parentFolderId, + disableDuplicatedNamesCheck: true, + }); + zeroLengthFilesNumber += await processDuplicateFiles({ + filesWithDuplicates: filesWithDuplicates as File[], + filesToUpload, + fileType, + parentFolderId, + disableDuplicatedNamesCheck, + duplicatedFiles: duplicatedFilesResponse, + }); + }; + + for (let i = 0; i < files.length; i += BATCH_SIZE) { + const batch = files.slice(i, i + BATCH_SIZE); + await processFilesBatch(batch); + } + + return { filesToUpload, zeroLengthFilesNumber }; +}; diff --git a/src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts b/src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts new file mode 100644 index 000000000..38fb7cc8d --- /dev/null +++ b/src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts @@ -0,0 +1,48 @@ +import { items as itemUtils } from '@internxt/lib'; + +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; +import { renameFile } from '../../../../crypto/services/utils'; +import { FileToUpload } from '../../../../drive/services/file.service/uploadFile'; +import { getUniqueFilename } from './getUniqueFilename'; + +export const processDuplicateFiles = async ({ + filesWithDuplicates, + filesToUpload, + fileType, + parentFolderId, + disableDuplicatedNamesCheck, + duplicatedFiles, +}: { + filesWithDuplicates: File[]; + filesToUpload: FileToUpload[]; + fileType: string | undefined; + parentFolderId: string; + disableDuplicatedNamesCheck: boolean | undefined; + duplicatedFiles?: DriveFileData[]; +}): Promise => { + let zeroLengthFiles = 0; + + for (const file of filesWithDuplicates) { + if (file.size === 0) { + zeroLengthFiles++; + continue; + } + const { filename, extension } = itemUtils.getFilenameAndExt(file.name); + let finalFilename = filename; + + if (!disableDuplicatedNamesCheck && duplicatedFiles) { + finalFilename = await getUniqueFilename(filename, extension, duplicatedFiles, parentFolderId); + } + + const fileContent = renameFile(file, finalFilename); + + filesToUpload.push({ + name: finalFilename, + size: file.size, + type: extension ?? fileType, + content: fileContent, + parentFolderId, + }); + } + return zeroLengthFiles; +}; diff --git a/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts b/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts new file mode 100644 index 000000000..2cab540b9 --- /dev/null +++ b/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts @@ -0,0 +1,47 @@ +import { DriveFolderData } from '@internxt/sdk/dist/drive/storage/types'; +import newStorageService from '../../../../drive/services/new-storage.service'; +// import { DriveFolderData } from '../../../../drive/types'; +import { IRoot } from '../storage.thunks/uploadFolderThunk'; + +interface DuplicatedFoldersResult { + duplicatedFoldersResponse: DriveFolderData[]; + foldersWithDuplicates: (IRoot | DriveFolderData)[]; + foldersWithoutDuplicates: (IRoot | DriveFolderData)[]; +} + +export const checkFolderDuplicated = async ( + folders: (IRoot | DriveFolderData)[], + parentFolderId: string, +): Promise => { + if (folders.length === 0) { + return { + duplicatedFoldersResponse: [], + foldersWithDuplicates: [], + foldersWithoutDuplicates: folders, + }; + } + const foldersNamesToUpload = folders.map((folder) => folder.name); + + const checkDuplicatedFolderResponse = await newStorageService.checkDuplicatedFolders( + parentFolderId, + foldersNamesToUpload, + ); + const duplicatedFoldersResponse = checkDuplicatedFolderResponse.existentFolders; + + const foldersWithDuplicates: (IRoot | DriveFolderData)[] = []; + const foldersWithoutDuplicates: (IRoot | DriveFolderData)[] = []; + + folders.forEach((folder) => { + const isDuplicate = duplicatedFoldersResponse.some( + (duplicatedFolder) => (duplicatedFolder as DriveFolderData & { plainName: string }).plainName === folder.name, + ); + + if (isDuplicate) { + foldersWithDuplicates.push(folder); + } else { + foldersWithoutDuplicates.push(folder); + } + }); + + return { duplicatedFoldersResponse, foldersWithoutDuplicates, foldersWithDuplicates }; +}; diff --git a/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts b/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts new file mode 100644 index 000000000..cc05b02c6 --- /dev/null +++ b/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts @@ -0,0 +1,32 @@ +import newStorageService from '../../../../drive/services/new-storage.service'; +import { DriveFolderData } from '../../../../drive/types'; +import renameFolderIfNeeded from '../storage.thunks/uploadFolderThunk'; + +export const getUniqueFolderName = async ( + folderName: string, + duplicatedFolders: DriveFolderData[], + parentFolderId: string, +): Promise => { + let isFolderNewNameDuplicated = true; + let finalFolderName = folderName; + let currentDuplicatedFolders = duplicatedFolders; + do { + const currentFolderFoldersToCheckDuplicates = currentDuplicatedFolders.map((folder) => ({ + ...folder, + name: folder?.plainName ?? folder.name, + })); + + const [, , renamedFoldername] = renameFolderIfNeeded(currentFolderFoldersToCheckDuplicates, finalFolderName); + + finalFolderName = renamedFoldername; + + const duplicatedFoldersResponse = await newStorageService.checkDuplicatedFolders(parentFolderId, [ + renamedFoldername, + ]); + + currentDuplicatedFolders = duplicatedFoldersResponse.existentFolders as DriveFolderData[]; + isFolderNewNameDuplicated = currentDuplicatedFolders.length > 0; + } while (isFolderNewNameDuplicated); + + return finalFolderName; +}; diff --git a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts index 10ca14ca3..3703bc9f0 100644 --- a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts @@ -1,6 +1,5 @@ import { ActionReducerMapBuilder, createAsyncThunk, Dispatch } from '@reduxjs/toolkit'; -import renameIfNeeded from '@internxt/lib/dist/src/items/renameIfNeeded'; import { DriveItemData } from 'app/drive/types'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import tasksService from 'app/tasks/services/tasks.service'; @@ -13,98 +12,62 @@ import { SdkFactory } from '../../../../core/factory/sdk'; import errorService from '../../../../core/services/error.service'; import { uiActions } from '../../ui'; import workspacesSelectors from '../../workspaces/workspaces.selectors'; +import { checkDuplicatedFiles } from '../fileNameUtils/checkDuplicatedFiles'; +import { getUniqueFilename } from '../fileNameUtils/getUniqueFilename'; +import { checkFolderDuplicated } from '../folderNameUtils/checkFolderDuplicated'; import { StorageState } from '../storage.model'; import storageSelectors from '../storage.selectors'; import renameFolderIfNeeded, { IRoot } from './uploadFolderThunk'; -const checkRepeatedNameFiles = (destinationFolderFiles: DriveItemData[], files: (DriveItemData | File)[]) => { - const repeatedFilesInDrive: DriveItemData[] = []; - const unrepeatedFiles: (DriveItemData | File)[] = []; - const filesRepeated = files.reduce((acc, file) => { - const exists = destinationFolderFiles.some((folderFile) => { - const fullFolderFileName = folderFile.name + '.' + folderFile.type; - - const fileName = (file as DriveItemData)?.fileId ? file.name + '.' + file.type : file.name; - if (fullFolderFileName === fileName) { - repeatedFilesInDrive.push(folderFile); - return true; - } - return false; - }); - - if (exists) { - return [...acc, file]; - } - unrepeatedFiles.push(file); - - return acc; - }, [] as (DriveItemData | File)[]); - - return { filesRepeated, repeatedFilesInDrive, unrepeatedFiles }; -}; - -const checkRepeatedNameFolders = (destinationFolderFolders: DriveItemData[], folders: (DriveItemData | IRoot)[]) => { - const repeatedFoldersInDrive: DriveItemData[] = []; - const unrepeatedFolders: (DriveItemData | IRoot)[] = []; - const foldersRepeated = folders.reduce((acc, file) => { - const exists = destinationFolderFolders.some((folderFile) => { - if (folderFile.name === file.name) { - repeatedFoldersInDrive.push(folderFile); - return true; - } - return false; - }); - - if (exists) { - return [...acc, file]; - } - unrepeatedFolders.push(file); - - return acc; - }, [] as (DriveItemData | IRoot)[]); - - return { foldersRepeated, repeatedFoldersInDrive, unrepeatedFolders }; -}; - -export const handleRepeatedUploadingFiles = ( +export const handleRepeatedUploadingFiles = async ( files: (DriveItemData | File)[], - items: DriveItemData[], dispatch: Dispatch, -): (DriveItemData | File)[] => { - const { filesRepeated, repeatedFilesInDrive, unrepeatedFiles } = checkRepeatedNameFiles(items, files); + destinationFolderUuid: string, +): Promise<(DriveItemData | File)[]> => { + const { + filesWithDuplicates: filesRepeated, + duplicatedFilesResponse, + filesWithoutDuplicates: unrepeatedFiles, + } = await checkDuplicatedFiles(files as File[], destinationFolderUuid); const hasRepeatedNameFiles = !!filesRepeated.length; if (hasRepeatedNameFiles) { - dispatch(storageActions.setFilesToRename(filesRepeated)); - dispatch(storageActions.setDriveFilesToRename(repeatedFilesInDrive)); + dispatch(storageActions.setFilesToRename(filesRepeated as DriveItemData[])); + dispatch(storageActions.setDriveFilesToRename(duplicatedFilesResponse as DriveItemData[])); dispatch(uiActions.setIsNameCollisionDialogOpen(true)); } - return unrepeatedFiles; + return unrepeatedFiles as DriveItemData[]; }; -export const handleRepeatedUploadingFolders = ( +export const handleRepeatedUploadingFolders = async ( folders: (DriveItemData | IRoot)[], - items: DriveItemData[], dispatch: Dispatch, -): (DriveItemData | IRoot)[] => { - const { foldersRepeated, repeatedFoldersInDrive, unrepeatedFolders } = checkRepeatedNameFolders(items, folders); + destinationFolderUuid: string, +): Promise<(DriveItemData | IRoot)[]> => { + const { + foldersWithDuplicates: foldersRepeated, + duplicatedFoldersResponse, + foldersWithoutDuplicates: unrepeatedFolders, + } = await checkFolderDuplicated(folders, destinationFolderUuid); + const hasRepeatedNameFiles = !!foldersRepeated.length; if (hasRepeatedNameFiles) { - dispatch(storageActions.setFoldersToRename(foldersRepeated)); - dispatch(storageActions.setDriveFoldersToRename(repeatedFoldersInDrive)); + dispatch(storageActions.setFoldersToRename(foldersRepeated as DriveItemData[])); + dispatch(storageActions.setDriveFoldersToRename(duplicatedFoldersResponse as DriveItemData[])); dispatch(uiActions.setIsNameCollisionDialogOpen(true)); } - return unrepeatedFolders; + return unrepeatedFolders as DriveItemData[]; }; export interface RenameItemsPayload { items: DriveItemData[]; destinationFolderId: string; + onRenameSuccess?: (newItemName: DriveItemData) => void; } export const renameItemsThunk = createAsyncThunk( 'storage/renameItems', - async ({ items, destinationFolderId }: RenameItemsPayload, { getState, dispatch }) => { + async ({ items, destinationFolderId, onRenameSuccess }: RenameItemsPayload, { getState, dispatch }) => { const promises: Promise[] = []; const state = getState(); const workspaceCredentials = workspacesSelectors.getWorkspaceCredentials(state); @@ -116,7 +79,7 @@ export const renameItemsThunk = createAsyncThunk !item?.isFolder); - const allFilesToCheckNames = [...parentFolderContent.files, ...currentFolderFiles]; - const [, , finalFilename] = renameIfNeeded(allFilesToCheckNames, item.name, item.type); + const { duplicatedFilesResponse } = await checkDuplicatedFiles([item], destinationFolderId); + + const finalFilename = await getUniqueFilename( + item.name, + item.type, + duplicatedFilesResponse, + destinationFolderId, + ); itemParsed = { ...item, name: finalFilename, plain_name: finalFilename }; } @@ -168,6 +136,7 @@ export const renameItemsThunk = createAsyncThunk onRenameSuccess?.(itemParsed), 1000); }) .catch((e) => { errorService.reportError(e); diff --git a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts index fed082bb7..81a5e41b9 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts @@ -11,6 +11,9 @@ import tasksService from '../../../../tasks/services/tasks.service'; import { TaskStatus, TaskType, UploadFolderTask } from '../../../../tasks/types'; import { planThunks } from '../../plan'; import workspacesSelectors from '../../workspaces/workspaces.selectors'; + +import { checkFolderDuplicated } from '../folderNameUtils/checkFolderDuplicated'; +import { getUniqueFolderName } from '../folderNameUtils/getUniqueFolderName'; import { StorageState } from '../storage.model'; import { deleteItemsThunk } from './deleteItemsThunk'; import { uploadItemsParallelThunk } from './uploadItemsThunk'; @@ -33,14 +36,20 @@ interface UploadFolderThunkPayload { }; } -const handleFoldersRename = async (root: IRoot, currentFolderId: string, tokenHeader?: string) => { - const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); - const [parentFolderContentPromise] = storageClient.getFolderContentByUuid(currentFolderId, false, tokenHeader); - const parentFolderContent = await parentFolderContentPromise; - const [, , finalFilename] = renameFolderIfNeeded(parentFolderContent.children, root.name); - const fileContent: IRoot = { ...root, name: finalFilename }; +const handleFoldersRename = async (root: IRoot, currentFolderId: string) => { + const { duplicatedFoldersResponse } = await checkFolderDuplicated([root], currentFolderId); + + let finalFilename = root.name; + if (duplicatedFoldersResponse.length > 0) + finalFilename = await getUniqueFolderName( + root.name, + duplicatedFoldersResponse as DriveFolderData[], + currentFolderId, + ); - return fileContent; + const folder: IRoot = { ...root, name: finalFilename }; + + return folder; }; const wait = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -73,8 +82,6 @@ export const uploadFolderThunk = createAsyncThunk { const state = getState(); - const workspaceCredentials = workspacesSelectors.getWorkspaceCredentials(state); - const workspaceSelected = workspacesSelectors.getSelectedWorkspace(state); const memberId = workspaceSelected?.workspaceUser?.memberId; @@ -85,7 +92,7 @@ export const uploadFolderThunk = createAsyncThunk => { - const filesToUpload: FileToUpload[] = []; - const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); - - let parentFolderContent; - - if (!disableDuplicatedNamesCheck) { - const [parentFolderContentPromise] = storageClient.getFolderContentByUuid(parentFolderId, false, workspaceToken); - parentFolderContent = await parentFolderContentPromise; - } - - let zeroLengthFilesNumber = 0; - - for (const file of files) { - if (file.size === 0) { - zeroLengthFilesNumber = zeroLengthFilesNumber + 1; - continue; - } - const { filename, extension } = itemUtils.getFilenameAndExt(file.name); - let fileContent; - let finalFilename = filename; - - if (!disableDuplicatedNamesCheck) { - const [, , renamedFilename] = itemUtils.renameIfNeeded(parentFolderContent.files, filename, extension); - finalFilename = renamedFilename; - fileContent = renameFile(file, renamedFilename); - } else { - fileContent = renameFile(file, filename); - } - - filesToUpload.push({ - name: finalFilename, - size: file.size, - type: extension ?? fileType, - content: fileContent, - parentFolderId, - }); - } - - return { filesToUpload, zeroLengthFilesNumber }; -}; - /** * @description * 1. Prepare files to upload @@ -199,7 +145,6 @@ export const uploadItemsThunk = createAsyncThunk item?.type === 'folder' || item?.isFolder; + const moveItemsToTrash = async (itemsToTrash: DriveItemData[], onSuccess?: () => void): Promise => { const items: Array<{ uuid: string; type: string }> = itemsToTrash.map((item) => { return { uuid: item.uuid, - type: item.isFolder ? 'folder' : 'file', + type: isFolder(item) ? 'folder' : 'file', }; }); let movingItemsToastId; @@ -81,10 +83,10 @@ const moveItemsToTrash = async (itemsToTrash: DriveItemData[], onSuccess?: () => item: itemsToTrash.length > 1 ? t('general.files') - : itemsToTrash[0].isFolder + : isFolder(itemsToTrash[0]) ? t('general.folder') : t('general.file'), - s: itemsToTrash.length > 1 ? 'os' : itemsToTrash[0].isFolder ? 'a' : 'o', + s: itemsToTrash.length > 1 ? 'os' : isFolder(itemsToTrash[0]) ? 'a' : 'o', }), action: { @@ -92,7 +94,7 @@ const moveItemsToTrash = async (itemsToTrash: DriveItemData[], onSuccess?: () => onClick: async () => { notificationsService.dismiss(id); if (itemsToTrash.length > 0) { - const destinationId = itemsToTrash[0].isFolder ? itemsToTrash[0].parentUuid : itemsToTrash[0].folderUuid; + const destinationId = isFolder(itemsToTrash[0]) ? itemsToTrash[0].parentUuid : itemsToTrash[0].folderUuid; store.dispatch( storageActions.pushItems({ updateRecents: true, items: itemsToTrash, folderIds: [destinationId] }), diff --git a/yarn.lock b/yarn.lock index 8eb9a8e9c..5352a6c11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1612,10 +1612,10 @@ resolved "https://npm.pkg.github.com/download/@internxt/prettier-config/1.0.2/5bd220b8de76734448db5475b3e0c01f9d22c19b#5bd220b8de76734448db5475b3e0c01f9d22c19b" integrity sha512-t4HiqvCbC7XgQepwWlIaFJe3iwW7HCf6xOSU9nKTV0tiGqOPz7xMtIgLEloQrDA34Cx4PkOYBXrvFPV6RxSFAA== -"@internxt/sdk@^1.5.15": - version "1.5.15" - resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.5.15/8b8febc6f5d37f66db525f66d85fbfdba36b7357#8b8febc6f5d37f66db525f66d85fbfdba36b7357" - integrity sha512-LO2VpJsZKmPzAkz7Lu4JVubjpv27XwwunHKT3XTZ4BntGS7JxAVljOQ0LtY9GN5pdV3Id5YzQwB6SAzdm03z3A== +"@internxt/sdk@^1.5.16": + version "1.5.16" + resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.5.16/6449a68049a83e9820cc59791d3fc239e32aa715#6449a68049a83e9820cc59791d3fc239e32aa715" + integrity sha512-e6anhV6PL04M+kEshBvHgjNMFUz2qylLwvhWzk4csEA/iBZHEJ5FeKN7RKcnP6XDuAjPKIYB2Vz3mnNdq3w/iA== dependencies: axios "^0.24.0" query-string "^7.1.0" @@ -1970,7 +1970,7 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@phosphor-icons/react@^2.0.10": +"@phosphor-icons/react@^2.1.7": version "2.1.7" resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.1.7.tgz#b11a4b25849b7e3849970b688d9fe91e5d4fd8d7" integrity sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ== From 54a627a60b64686f89ae07af6a59544d9e1bb730 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Wed, 18 Sep 2024 16:48:07 +0200 Subject: [PATCH 02/11] Added tests for checkFolderDuplicated --- .../checkFolderDuplicated.test.ts | 83 +++++++++++++++++++ .../folderNameUtils/checkFolderDuplicated.ts | 3 +- .../storage.thunks/uploadFolderThunk.ts | 9 +- src/app/store/slices/storage/types.ts | 7 ++ 4 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts create mode 100644 src/app/store/slices/storage/types.ts diff --git a/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts b/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts new file mode 100644 index 000000000..b6114c2d9 --- /dev/null +++ b/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts @@ -0,0 +1,83 @@ +import newStorageService from '../../../../drive/services/new-storage.service'; +import { DriveFolderData } from '../../../../drive/types'; + +import { checkFolderDuplicated } from './checkFolderDuplicated'; + +jest.mock('../../../../drive/services/new-storage.service', () => ({ + checkDuplicatedFolders: jest.fn(), +})); + +describe('checkFolderDuplicated', () => { + const parentFolderId = 'parent-folder-id'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return empty results when there are no folders', async () => { + const folders = []; + const result = await checkFolderDuplicated(folders, parentFolderId); + + expect(result).toEqual({ + duplicatedFoldersResponse: [], + foldersWithDuplicates: [], + foldersWithoutDuplicates: [], + }); + }); + + it('should return all folders as duplicated when all are duplicated', async () => { + const folders = [{ name: 'Folder1' }, { name: 'Folder2' }] as unknown as DriveFolderData[]; + + (newStorageService.checkDuplicatedFolders as jest.Mock).mockResolvedValue({ + existentFolders: [{ plainName: 'Folder1' }, { plainName: 'Folder2' }], + }); + + const result = await checkFolderDuplicated(folders, parentFolderId); + + expect(newStorageService.checkDuplicatedFolders).toHaveBeenCalledWith(parentFolderId, ['Folder1', 'Folder2']); + expect(result).toEqual({ + duplicatedFoldersResponse: [{ plainName: 'Folder1' }, { plainName: 'Folder2' }], + foldersWithDuplicates: folders, + foldersWithoutDuplicates: [], + }); + }); + + it('should return some folders as duplicated and others without duplicates', async () => { + const folders = [{ name: 'Folder1' }, { name: 'Folder2' }, { name: 'Folder3' }] as unknown as DriveFolderData[]; + const parentFolderId = 'someParentId'; + + (newStorageService.checkDuplicatedFolders as jest.Mock).mockResolvedValue({ + existentFolders: [{ plainName: 'Folder1' }, { plainName: 'Folder3' }], + }); + + const result = await checkFolderDuplicated(folders, parentFolderId); + + expect(newStorageService.checkDuplicatedFolders).toHaveBeenCalledWith(parentFolderId, [ + 'Folder1', + 'Folder2', + 'Folder3', + ]); + expect(result).toEqual({ + duplicatedFoldersResponse: [{ plainName: 'Folder1' }, { plainName: 'Folder3' }], + foldersWithDuplicates: [{ name: 'Folder1' }, { name: 'Folder3' }], + foldersWithoutDuplicates: [{ name: 'Folder2' }], + }); + }); + + it('should return all folders without duplicates when none are duplicated', async () => { + const folders = [{ name: 'Folder1' }, { name: 'Folder2' }] as unknown as DriveFolderData[]; + + (newStorageService.checkDuplicatedFolders as jest.Mock).mockResolvedValue({ + existentFolders: [], + }); + + const result = await checkFolderDuplicated(folders, parentFolderId); + + expect(newStorageService.checkDuplicatedFolders).toHaveBeenCalledWith(parentFolderId, ['Folder1', 'Folder2']); + expect(result).toEqual({ + duplicatedFoldersResponse: [], + foldersWithDuplicates: [], + foldersWithoutDuplicates: folders, + }); + }); +}); diff --git a/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts b/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts index 2cab540b9..57f1aae11 100644 --- a/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts +++ b/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts @@ -1,7 +1,6 @@ import { DriveFolderData } from '@internxt/sdk/dist/drive/storage/types'; import newStorageService from '../../../../drive/services/new-storage.service'; -// import { DriveFolderData } from '../../../../drive/types'; -import { IRoot } from '../storage.thunks/uploadFolderThunk'; +import { IRoot } from '../types'; interface DuplicatedFoldersResult { duplicatedFoldersResponse: DriveFolderData[]; diff --git a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts index 81a5e41b9..78fceb43f 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts @@ -15,17 +15,10 @@ import workspacesSelectors from '../../workspaces/workspaces.selectors'; import { checkFolderDuplicated } from '../folderNameUtils/checkFolderDuplicated'; import { getUniqueFolderName } from '../folderNameUtils/getUniqueFolderName'; import { StorageState } from '../storage.model'; +import { IRoot } from '../types'; import { deleteItemsThunk } from './deleteItemsThunk'; import { uploadItemsParallelThunk } from './uploadItemsThunk'; -export interface IRoot { - name: string; - folderId: string | null; - childrenFiles: File[]; - childrenFolders: IRoot[]; - fullPathEdited: string; -} - interface UploadFolderThunkPayload { root: IRoot; currentFolderId: string; diff --git a/src/app/store/slices/storage/types.ts b/src/app/store/slices/storage/types.ts new file mode 100644 index 000000000..9bb300c2f --- /dev/null +++ b/src/app/store/slices/storage/types.ts @@ -0,0 +1,7 @@ +export interface IRoot { + name: string; + folderId: string | null; + childrenFiles: File[]; + childrenFolders: IRoot[]; + fullPathEdited: string; +} From c7311cd4842c65454c4bd98ec902f5fce96b04f4 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Wed, 18 Sep 2024 16:55:37 +0200 Subject: [PATCH 03/11] Added TODO to remove IRoot type from uploadFolderThunk.ts file --- .../slices/storage/storage.thunks/uploadFolderThunk.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts index 78fceb43f..2437a10ac 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts @@ -15,10 +15,18 @@ import workspacesSelectors from '../../workspaces/workspaces.selectors'; import { checkFolderDuplicated } from '../folderNameUtils/checkFolderDuplicated'; import { getUniqueFolderName } from '../folderNameUtils/getUniqueFolderName'; import { StorageState } from '../storage.model'; -import { IRoot } from '../types'; import { deleteItemsThunk } from './deleteItemsThunk'; import { uploadItemsParallelThunk } from './uploadItemsThunk'; +// TODO: REMOVE IROOT from this file, it is in types.ts +export interface IRoot { + name: string; + folderId: string | null; + childrenFiles: File[]; + childrenFolders: IRoot[]; + fullPathEdited: string; +} + interface UploadFolderThunkPayload { root: IRoot; currentFolderId: string; From c1a6a6c7aa2ac1b8ab7832c48d052ffe7930553c Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Wed, 18 Sep 2024 17:50:01 +0200 Subject: [PATCH 04/11] Added tests to renameFolderIfNeeded and getUniqueFolderName functions --- .../getUniqueFolderName.test.ts | 76 +++++++++++++++++++ .../folderNameUtils/getUniqueFolderName.ts | 2 +- .../renameFolderIfNeeded.test.ts | 70 +++++++++++++++++ .../folderNameUtils/renameFolderIfNeeded.ts | 35 +++++++++ .../storage.thunks/renameItemsThunk.ts | 3 +- .../storage.thunks/uploadFolderThunk.ts | 37 --------- 6 files changed, 184 insertions(+), 39 deletions(-) create mode 100644 src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts create mode 100644 src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.test.ts create mode 100644 src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.ts diff --git a/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts b/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts new file mode 100644 index 000000000..051a4fed0 --- /dev/null +++ b/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it, jest } from '@jest/globals'; + +import newStorageService from '../../../../drive/services/new-storage.service'; +import { DriveFolderData } from '../../../../drive/types'; + +import { getUniqueFolderName } from './getUniqueFolderName'; +import * as renameFolderModule from './renameFolderIfNeeded'; + +jest.mock('../../../../drive/services/new-storage.service', () => ({ + checkDuplicatedFolders: jest.fn(), +})); + +jest.mock('../storage.thunks/uploadFolderThunk', () => jest.fn()); + +describe('getUniqueFolderName', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the original name if no duplicates exist', async () => { + const folderName = 'TestFolder'; + const duplicatedFolders: [] = []; + const parentFolderId = 'parent123'; + + (newStorageService.checkDuplicatedFolders as jest.Mock).mockResolvedValue({ existentFolders: [] }); + + const renameFolderIfNeeded = jest.spyOn(renameFolderModule, 'default'); + + const result = await getUniqueFolderName(folderName, duplicatedFolders, parentFolderId); + + expect(result).toBe(folderName); + expect(newStorageService.checkDuplicatedFolders).toHaveBeenCalledWith(parentFolderId, [folderName]); + expect(renameFolderIfNeeded).toHaveBeenCalledWith([], folderName); + }); + + it('should rename the folder if duplicates exist', async () => { + const folderName = 'TestFolder'; + const duplicatedFolders = [{ name: 'TestFolder', plainName: 'TestFolder' }] as unknown as DriveFolderData[]; + const parentFolderId = 'parent123'; + + (newStorageService.checkDuplicatedFolders as jest.Mock) + .mockResolvedValueOnce({ existentFolders: [{ plainName: 'TestFolder' }] }) + .mockResolvedValueOnce({ existentFolders: [] }); + const renameFolderIfNeeded = jest.spyOn(renameFolderModule, 'default'); + + const result = await getUniqueFolderName(folderName, duplicatedFolders, parentFolderId); + + expect(result).toBe('TestFolder (1)'); + expect(newStorageService.checkDuplicatedFolders).toHaveBeenCalledTimes(2); + expect(newStorageService.checkDuplicatedFolders).toHaveBeenCalledWith(parentFolderId, ['TestFolder (1)']); + expect(renameFolderIfNeeded).toHaveBeenCalledTimes(2); + }); + + it('should handle multiple renames if necessary', async () => { + const folderName = 'TestFolder'; + const duplicatedFolders = [ + { name: 'TestFolder', plainName: 'TestFolder' }, + { name: 'TestFolder (1)', plainName: 'TestFolder (1)' }, + ] as unknown as DriveFolderData[]; + const parentFolderId = 'parent123'; + + (newStorageService.checkDuplicatedFolders as jest.Mock) + .mockResolvedValueOnce({ existentFolders: [{ plainName: 'TestFolder' }] }) + .mockResolvedValueOnce({ existentFolders: [{ plainName: 'TestFolder (1)' }] }) + .mockResolvedValueOnce({ existentFolders: [] }); + + const renameFolderIfNeeded = jest.spyOn(renameFolderModule, 'default'); + + const result = await getUniqueFolderName(folderName, duplicatedFolders, parentFolderId); + + expect(result).toBe('TestFolder (2)'); + expect(newStorageService.checkDuplicatedFolders).toHaveBeenCalledTimes(3); + expect(newStorageService.checkDuplicatedFolders).toHaveBeenLastCalledWith(parentFolderId, ['TestFolder (2)']); + expect(renameFolderIfNeeded).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts b/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts index cc05b02c6..6ec7594bd 100644 --- a/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts +++ b/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts @@ -1,6 +1,6 @@ import newStorageService from '../../../../drive/services/new-storage.service'; import { DriveFolderData } from '../../../../drive/types'; -import renameFolderIfNeeded from '../storage.thunks/uploadFolderThunk'; +import renameFolderIfNeeded from './renameFolderIfNeeded'; export const getUniqueFolderName = async ( folderName: string, diff --git a/src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.test.ts b/src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.test.ts new file mode 100644 index 000000000..e844984ee --- /dev/null +++ b/src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from '@jest/globals'; +import renameFolderIfNeeded from './renameFolderIfNeeded'; + +describe('renameFolderIfNeeded', () => { + it('should return the original name when there are no conflicts', () => { + const items = [{ name: 'Folder1' }, { name: 'Folder2' }]; + const folderName = 'NewFolder'; + + const [needsRename, index, newName] = renameFolderIfNeeded(items, folderName); + + expect(needsRename).toBe(false); + expect(index).toBe(0); + expect(newName).toBe('NewFolder'); + }); + + it('should rename the folder when there is a conflict', () => { + const items = [{ name: 'NewFolder' }, { name: 'Folder2' }]; + const folderName = 'NewFolder'; + + const [needsRename, index, newName] = renameFolderIfNeeded(items, folderName); + + expect(needsRename).toBe(true); + expect(index).toBe(1); + expect(newName).toBe('NewFolder (1)'); + }); + + it('should handle multiple conflicts and increment correctly', () => { + const items = [{ name: 'NewFolder' }, { name: 'NewFolder (1)' }, { name: 'NewFolder (2)' }, { name: 'Folder2' }]; + const folderName = 'NewFolder'; + + const [needsRename, index, newName] = renameFolderIfNeeded(items, folderName); + + expect(needsRename).toBe(true); + expect(index).toBe(3); + expect(newName).toBe('NewFolder (3)'); + }); + + it('should handle non-sequential numbering', () => { + const items = [{ name: 'NewFolder' }, { name: 'NewFolder (1)' }, { name: 'NewFolder (3)' }, { name: 'Folder2' }]; + const folderName = 'NewFolder'; + + const [needsRename, index, newName] = renameFolderIfNeeded(items, folderName); + + expect(needsRename).toBe(true); + expect(index).toBe(4); + expect(newName).toBe('NewFolder (4)'); + }); + + it('should handle folder names with existing parentheses', () => { + const items = [{ name: 'NewFolder (test)' }, { name: 'Folder2' }]; + const folderName = 'NewFolder (test)'; + + const [needsRename, index, newName] = renameFolderIfNeeded(items, folderName); + + expect(needsRename).toBe(true); + expect(index).toBe(1); + expect(newName).toBe('NewFolder (test) (1)'); + }); + + it('should handle folder names with existing numbers', () => { + const items = [{ name: 'NewFolder 123' }, { name: 'Folder2' }]; + const folderName = 'NewFolder 123'; + + const [needsRename, index, newName] = renameFolderIfNeeded(items, folderName); + + expect(needsRename).toBe(true); + expect(index).toBe(1); + expect(newName).toBe('NewFolder 123 (1)'); + }); +}); diff --git a/src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.ts b/src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.ts new file mode 100644 index 000000000..5e19994fc --- /dev/null +++ b/src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.ts @@ -0,0 +1,35 @@ +function getNextNewName(filename: string, i: number): string { + return `${filename} (${i})`; +} + +export default function renameFolderIfNeeded(items: { name: string }[], folderName: string): [boolean, number, string] { + const FOLDER_INCREMENT_REGEX = /( \([0-9]+\))$/i; + const INCREMENT_INDEX_REGEX = /\(([^)]+)\)/; + + const cleanFilename = folderName.replace(FOLDER_INCREMENT_REGEX, ''); + + const infoFoldernames: { name: string; cleanName: string; incrementIndex: number }[] = items + .map((item) => { + const cleanName = item.name.replace(FOLDER_INCREMENT_REGEX, ''); + const incrementString = item.name.match(FOLDER_INCREMENT_REGEX)?.pop()?.match(INCREMENT_INDEX_REGEX)?.pop(); + const incrementIndex = parseInt(incrementString || '0'); + + return { + name: item.name, + cleanName, + incrementIndex, + }; + }) + .filter((item) => item.cleanName === cleanFilename) + .sort((a, b) => b.incrementIndex - a.incrementIndex); + + const filenameExists = !!infoFoldernames.length; + + if (filenameExists) { + const index = infoFoldernames[0].incrementIndex + 1; + + return [true, index, getNextNewName(cleanFilename, index)]; + } else { + return [false, 0, folderName]; + } +} diff --git a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts index 3703bc9f0..8990e8474 100644 --- a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts @@ -15,9 +15,10 @@ import workspacesSelectors from '../../workspaces/workspaces.selectors'; import { checkDuplicatedFiles } from '../fileNameUtils/checkDuplicatedFiles'; import { getUniqueFilename } from '../fileNameUtils/getUniqueFilename'; import { checkFolderDuplicated } from '../folderNameUtils/checkFolderDuplicated'; +import renameFolderIfNeeded from '../folderNameUtils/renameFolderIfNeeded'; import { StorageState } from '../storage.model'; import storageSelectors from '../storage.selectors'; -import renameFolderIfNeeded, { IRoot } from './uploadFolderThunk'; +import { IRoot } from './uploadFolderThunk'; export const handleRepeatedUploadingFiles = async ( files: (DriveItemData | File)[], diff --git a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts index 2437a10ac..806a0f22d 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts @@ -234,43 +234,6 @@ export const uploadFolderThunk = createAsyncThunk { - const cleanName = item.name.replace(FOLDER_INCREMENT_REGEX, ''); - const incrementString = item.name.match(FOLDER_INCREMENT_REGEX)?.pop()?.match(INCREMENT_INDEX_REGEX)?.pop(); - const incrementIndex = parseInt(incrementString || '0'); - - return { - name: item.name, - cleanName, - incrementIndex, - }; - }) - .filter((item) => item.cleanName === cleanFilename) - .sort((a, b) => b.incrementIndex - a.incrementIndex); - - const filenameExists = !!infoFoldernames.length; - - if (filenameExists) { - const index = infoFoldernames[0].incrementIndex + 1; - - return [true, index, getNextNewName(cleanFilename, index)]; - } else { - return [false, 0, folderName]; - } -} - -function getNextNewName(filename: string, i: number): string { - return `${filename} (${i})`; -} - const generateTaskIdForFolders = (foldersPayload: UploadFolderThunkPayload[]) => { return foldersPayload.map(({ root, currentFolderId, options: payloadOptions }) => { const options = { withNotification: true, ...payloadOptions }; From 665048e5273aec2a660f2fc4f65b6695edefe187 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Thu, 19 Sep 2024 09:28:57 +0200 Subject: [PATCH 05/11] Added tests to getUniqueFileName function and remove innecessary files --- .../fileNameUtils/getUniqueFileName.test.ts | 93 +++++++++++++++++++ .../slices/storage/fileNameUtils/index.ts | 1 - .../checkFolderDuplicated.test.ts | 6 +- .../getUniqueFolderName.test.ts | 4 +- .../storage.thunks/uploadItemsThunk.ts | 3 +- 5 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 src/app/store/slices/storage/fileNameUtils/getUniqueFileName.test.ts delete mode 100644 src/app/store/slices/storage/fileNameUtils/index.ts diff --git a/src/app/store/slices/storage/fileNameUtils/getUniqueFileName.test.ts b/src/app/store/slices/storage/fileNameUtils/getUniqueFileName.test.ts new file mode 100644 index 000000000..210e72d77 --- /dev/null +++ b/src/app/store/slices/storage/fileNameUtils/getUniqueFileName.test.ts @@ -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); + }); +}); diff --git a/src/app/store/slices/storage/fileNameUtils/index.ts b/src/app/store/slices/storage/fileNameUtils/index.ts deleted file mode 100644 index 8c93fdf05..000000000 --- a/src/app/store/slices/storage/fileNameUtils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { prepareFilesToUpload } from './prepareFilesToUpload'; diff --git a/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts b/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts index b6114c2d9..684344e17 100644 --- a/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts +++ b/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts @@ -26,7 +26,7 @@ describe('checkFolderDuplicated', () => { }); it('should return all folders as duplicated when all are duplicated', async () => { - const folders = [{ name: 'Folder1' }, { name: 'Folder2' }] as unknown as DriveFolderData[]; + const folders = [{ name: 'Folder1' }, { name: 'Folder2' }] as DriveFolderData[]; (newStorageService.checkDuplicatedFolders as jest.Mock).mockResolvedValue({ existentFolders: [{ plainName: 'Folder1' }, { plainName: 'Folder2' }], @@ -43,7 +43,7 @@ describe('checkFolderDuplicated', () => { }); it('should return some folders as duplicated and others without duplicates', async () => { - const folders = [{ name: 'Folder1' }, { name: 'Folder2' }, { name: 'Folder3' }] as unknown as DriveFolderData[]; + const folders = [{ name: 'Folder1' }, { name: 'Folder2' }, { name: 'Folder3' }] as DriveFolderData[]; const parentFolderId = 'someParentId'; (newStorageService.checkDuplicatedFolders as jest.Mock).mockResolvedValue({ @@ -65,7 +65,7 @@ describe('checkFolderDuplicated', () => { }); it('should return all folders without duplicates when none are duplicated', async () => { - const folders = [{ name: 'Folder1' }, { name: 'Folder2' }] as unknown as DriveFolderData[]; + const folders = [{ name: 'Folder1' }, { name: 'Folder2' }] as DriveFolderData[]; (newStorageService.checkDuplicatedFolders as jest.Mock).mockResolvedValue({ existentFolders: [], diff --git a/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts b/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts index 051a4fed0..c011fae2f 100644 --- a/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts +++ b/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts @@ -35,7 +35,7 @@ describe('getUniqueFolderName', () => { it('should rename the folder if duplicates exist', async () => { const folderName = 'TestFolder'; - const duplicatedFolders = [{ name: 'TestFolder', plainName: 'TestFolder' }] as unknown as DriveFolderData[]; + const duplicatedFolders = [{ name: 'TestFolder', plainName: 'TestFolder' }] as DriveFolderData[]; const parentFolderId = 'parent123'; (newStorageService.checkDuplicatedFolders as jest.Mock) @@ -56,7 +56,7 @@ describe('getUniqueFolderName', () => { const duplicatedFolders = [ { name: 'TestFolder', plainName: 'TestFolder' }, { name: 'TestFolder (1)', plainName: 'TestFolder (1)' }, - ] as unknown as DriveFolderData[]; + ] as DriveFolderData[]; const parentFolderId = 'parent123'; (newStorageService.checkDuplicatedFolders as jest.Mock) diff --git a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts index b51cd6ee8..5f332592d 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts @@ -22,7 +22,8 @@ import shareService from '../../../../share/services/share.service'; import { planThunks } from '../../plan'; import { uiActions } from '../../ui'; import workspacesSelectors from '../../workspaces/workspaces.selectors'; -import { prepareFilesToUpload } from '../fileNameUtils'; + +import { prepareFilesToUpload } from '../fileNameUtils/prepareFilesToUpload'; import { StorageState } from '../storage.model'; interface UploadItemsThunkOptions { From cc4b055444518c708a49947735ee7a16eda6ff19 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Thu, 19 Sep 2024 16:24:21 +0200 Subject: [PATCH 06/11] Added prepareFileToUpload test and minor refactor --- src/app/drive/services/file.service/types.ts | 7 ++ .../drive/services/file.service/uploadFile.ts | 1 + .../prepareFilesToUpload.test.ts | 116 ++++++++++++++++++ .../fileNameUtils/prepareFilesToUpload.ts | 31 +++-- .../fileNameUtils/processDuplicateFiles.ts | 45 +++---- 5 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 src/app/drive/services/file.service/types.ts create mode 100644 src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.test.ts diff --git a/src/app/drive/services/file.service/types.ts b/src/app/drive/services/file.service/types.ts new file mode 100644 index 000000000..2a567d598 --- /dev/null +++ b/src/app/drive/services/file.service/types.ts @@ -0,0 +1,7 @@ +export interface FileToUpload { + name: string; + size: number; + type: string; + content: File; + parentFolderId: string; +} diff --git a/src/app/drive/services/file.service/uploadFile.ts b/src/app/drive/services/file.service/uploadFile.ts index 903b77f15..261ce8c5b 100644 --- a/src/app/drive/services/file.service/uploadFile.ts +++ b/src/app/drive/services/file.service/uploadFile.ts @@ -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; diff --git a/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.test.ts b/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.test.ts new file mode 100644 index 000000000..e7c93f722 --- /dev/null +++ b/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.test.ts @@ -0,0 +1,116 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import newStorageService from '../../../../drive/services/new-storage.service'; +import * as checkDuplicatedFilesModule from './checkDuplicatedFiles'; +import { prepareFilesToUpload } from './prepareFilesToUpload'; +import * as processDuplicateFilesModule from './processDuplicateFiles'; + +jest.mock('../../../../drive/services/new-storage.service', () => ({ + checkDuplicatedFiles: jest.fn(), +})); + +// MOCK FILE NECESSARY BECAUSE IN NODE, THE CLASS FILE NOT EXISTS +class MockFile { + name: string; + size: number; + type: string; + + constructor(parts: [], filename: string, properties?: { type?: string; size?: number }) { + this.name = filename; + this.size = properties?.size || 0; + this.type = properties?.type || ''; + } +} + +global.File = MockFile as any; + +describe('prepareFilesToUpload', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should process files in batches', async () => { + const TOTAL_FILES = 800; + const mockFiles = Array(TOTAL_FILES) + .fill(null) + .map((_, i) => new MockFile([], `file${i}.txt`, { type: 'text/plain', size: 13 })); + const parentFolderId = 'parent123'; + (newStorageService.checkDuplicatedFiles as jest.Mock).mockResolvedValue({ + existentFiles: [], + }); + + const checkDuplicatedFilesSpy = jest.spyOn(checkDuplicatedFilesModule, 'checkDuplicatedFiles'); + const processDuplicateFiles = jest.spyOn(processDuplicateFilesModule, 'processDuplicateFiles'); + const result = await prepareFilesToUpload({ files: mockFiles as File[], parentFolderId }); + + expect(checkDuplicatedFilesSpy).toHaveBeenCalledTimes(4); + expect(processDuplicateFiles).toHaveBeenCalledTimes(8); + expect(result.zeroLengthFilesNumber).toBe(0); + expect(result.filesToUpload.length).toBe(TOTAL_FILES); + }); + + it('should handle duplicates and non-duplicates', async () => { + const files = Array(10) + .fill(null) + .map((_, i) => new MockFile([], `file${i}.txt`, { type: 'text/plain', size: i === 0 ? 0 : 1 })); + const parentFolderId = 'parent123'; + + (newStorageService.checkDuplicatedFiles as jest.Mock) + .mockResolvedValueOnce({ + existentFiles: [{ plainName: 'file2', type: 'txt' }], + }) + .mockResolvedValueOnce({ existentFiles: [] }); + + const checkDuplicatedFilesSpy = jest.spyOn(checkDuplicatedFilesModule, 'checkDuplicatedFiles'); + const processDuplicateFiles = jest.spyOn(processDuplicateFilesModule, 'processDuplicateFiles'); + + const result = await prepareFilesToUpload({ files: files as File[], parentFolderId }); + + expect(checkDuplicatedFilesSpy).toHaveBeenCalledTimes(1); + expect(processDuplicateFiles).toHaveBeenCalledTimes(2); + expect(result.zeroLengthFilesNumber).toBe(1); + }); + + it('should respect the disableDuplicatedNamesCheck flag', async () => { + const files = [new File([], 'file.txt')]; + const parentFolderId = 'parent123'; + + (checkDuplicatedFilesModule.checkDuplicatedFiles as jest.Mock).mockResolvedValue({ + duplicatedFilesResponse: [{ name: 'file.txt' }], + filesWithoutDuplicates: [], + filesWithDuplicates: files, + }); + + const processDuplicateFiles = jest.spyOn(processDuplicateFilesModule, 'processDuplicateFiles'); + + await prepareFilesToUpload({ files, parentFolderId, disableDuplicatedNamesCheck: true }); + + expect(processDuplicateFiles).toHaveBeenCalledWith( + expect.objectContaining({ + disableDuplicatedNamesCheck: true, + }), + ); + }); + + it('should handle fileType parameter', async () => { + const files = [new File([], 'file.txt')]; + const parentFolderId = 'parent123'; + const fileType = 'text/plain'; + + (checkDuplicatedFilesModule.checkDuplicatedFiles as jest.Mock).mockResolvedValue({ + duplicatedFilesResponse: [], + filesWithoutDuplicates: files, + filesWithDuplicates: [], + }); + + const processDuplicateFiles = jest.spyOn(processDuplicateFilesModule, 'processDuplicateFiles'); + + await prepareFilesToUpload({ files, parentFolderId, fileType }); + + expect(processDuplicateFiles).toHaveBeenCalledWith( + expect.objectContaining({ + fileType: 'text/plain', + }), + ); + }); +}); diff --git a/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts b/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts index 97ec1937b..618989544 100644 --- a/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts +++ b/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts @@ -15,7 +15,7 @@ export const prepareFilesToUpload = async ({ disableDuplicatedNamesCheck?: boolean; fileType?: string; }): Promise<{ filesToUpload: FileToUpload[]; zeroLengthFilesNumber: number }> => { - const filesToUpload: FileToUpload[] = []; + let filesToUpload: FileToUpload[] = []; let zeroLengthFilesNumber = 0; const processFilesBatch = async (filesBatch: File[]) => { @@ -24,21 +24,28 @@ export const prepareFilesToUpload = async ({ parentFolderId, ); - zeroLengthFilesNumber += await processDuplicateFiles({ - filesWithDuplicates: filesWithoutDuplicates as File[], - filesToUpload, + const { zeroLengthFiles, newFilesToUpload: filesToUploadReturned } = await processDuplicateFiles({ + files: filesWithoutDuplicates as File[], + existingFilesToUpload: filesToUpload, fileType, parentFolderId, disableDuplicatedNamesCheck: true, }); - zeroLengthFilesNumber += await processDuplicateFiles({ - filesWithDuplicates: filesWithDuplicates as File[], - filesToUpload, - fileType, - parentFolderId, - disableDuplicatedNamesCheck, - duplicatedFiles: duplicatedFilesResponse, - }); + filesToUpload = filesToUploadReturned; + zeroLengthFilesNumber += zeroLengthFiles; + + const { zeroLengthFiles: zeroLengthFilesReturned, newFilesToUpload: filesToUploadReturned2 } = + await processDuplicateFiles({ + files: filesWithDuplicates as File[], + existingFilesToUpload: filesToUpload, + fileType, + parentFolderId, + disableDuplicatedNamesCheck, + duplicatedFiles: duplicatedFilesResponse, + }); + + filesToUpload = filesToUploadReturned2; + zeroLengthFilesNumber += zeroLengthFilesReturned; }; for (let i = 0; i < files.length; i += BATCH_SIZE) { diff --git a/src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts b/src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts index 38fb7cc8d..2c190f144 100644 --- a/src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts +++ b/src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts @@ -1,32 +1,32 @@ import { items as itemUtils } from '@internxt/lib'; - import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; import { renameFile } from '../../../../crypto/services/utils'; -import { FileToUpload } from '../../../../drive/services/file.service/uploadFile'; +import { FileToUpload } from '../../../../drive/services/file.service/types'; import { getUniqueFilename } from './getUniqueFilename'; +interface ProcessDuplicateFilesParams { + files: File[]; + existingFilesToUpload: FileToUpload[]; + fileType?: string; + parentFolderId: string; + disableDuplicatedNamesCheck?: boolean; + duplicatedFiles?: DriveFileData[]; +} + export const processDuplicateFiles = async ({ - filesWithDuplicates, - filesToUpload, + files, + existingFilesToUpload, fileType, parentFolderId, disableDuplicatedNamesCheck, duplicatedFiles, -}: { - filesWithDuplicates: File[]; - filesToUpload: FileToUpload[]; - fileType: string | undefined; - parentFolderId: string; - disableDuplicatedNamesCheck: boolean | undefined; - duplicatedFiles?: DriveFileData[]; -}): Promise => { - let zeroLengthFiles = 0; +}: ProcessDuplicateFilesParams): Promise<{ newFilesToUpload: FileToUpload[]; zeroLengthFiles: number }> => { + const zeroLengthFiles = files.filter((file) => file.size === 0).length; + const newFilesToUpload: FileToUpload[] = [...existingFilesToUpload]; + + const processFile = async (file: File): Promise => { + if (file.size === 0) return; - for (const file of filesWithDuplicates) { - if (file.size === 0) { - zeroLengthFiles++; - continue; - } const { filename, extension } = itemUtils.getFilenameAndExt(file.name); let finalFilename = filename; @@ -36,13 +36,16 @@ export const processDuplicateFiles = async ({ const fileContent = renameFile(file, finalFilename); - filesToUpload.push({ + newFilesToUpload.push({ name: finalFilename, size: file.size, type: extension ?? fileType, content: fileContent, parentFolderId, }); - } - return zeroLengthFiles; + }; + + await Promise.all(files.filter((file) => file.size > 0).map(processFile)); + + return { newFilesToUpload, zeroLengthFiles }; }; From 047abbb82427809238d5884a2481ab58adb0e420 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Thu, 19 Sep 2024 16:34:07 +0200 Subject: [PATCH 07/11] Renamed directory --- .../{fileNameUtils => fileUtils}/checkDuplicatedFiles.ts | 0 .../getUniqueFileName.test.ts | 0 .../{fileNameUtils => fileUtils}/getUniqueFilename.ts | 0 .../prepareFilesToUpload.test.ts | 0 .../{fileNameUtils => fileUtils}/prepareFilesToUpload.ts | 0 .../{fileNameUtils => fileUtils}/processDuplicateFiles.ts | 0 .../checkFolderDuplicated.test.ts | 0 .../checkFolderDuplicated.ts | 0 .../getUniqueFolderName.test.ts | 0 .../getUniqueFolderName.ts | 0 .../renameFolderIfNeeded.test.ts | 0 .../renameFolderIfNeeded.ts | 0 .../slices/storage/storage.thunks/renameItemsThunk.ts | 8 ++++---- .../slices/storage/storage.thunks/uploadFolderThunk.ts | 4 ++-- .../slices/storage/storage.thunks/uploadItemsThunk.ts | 2 +- 15 files changed, 7 insertions(+), 7 deletions(-) rename src/app/store/slices/storage/{fileNameUtils => fileUtils}/checkDuplicatedFiles.ts (100%) rename src/app/store/slices/storage/{fileNameUtils => fileUtils}/getUniqueFileName.test.ts (100%) rename src/app/store/slices/storage/{fileNameUtils => fileUtils}/getUniqueFilename.ts (100%) rename src/app/store/slices/storage/{fileNameUtils => fileUtils}/prepareFilesToUpload.test.ts (100%) rename src/app/store/slices/storage/{fileNameUtils => fileUtils}/prepareFilesToUpload.ts (100%) rename src/app/store/slices/storage/{fileNameUtils => fileUtils}/processDuplicateFiles.ts (100%) rename src/app/store/slices/storage/{folderNameUtils => folderUtils}/checkFolderDuplicated.test.ts (100%) rename src/app/store/slices/storage/{folderNameUtils => folderUtils}/checkFolderDuplicated.ts (100%) rename src/app/store/slices/storage/{folderNameUtils => folderUtils}/getUniqueFolderName.test.ts (100%) rename src/app/store/slices/storage/{folderNameUtils => folderUtils}/getUniqueFolderName.ts (100%) rename src/app/store/slices/storage/{folderNameUtils => folderUtils}/renameFolderIfNeeded.test.ts (100%) rename src/app/store/slices/storage/{folderNameUtils => folderUtils}/renameFolderIfNeeded.ts (100%) diff --git a/src/app/store/slices/storage/fileNameUtils/checkDuplicatedFiles.ts b/src/app/store/slices/storage/fileUtils/checkDuplicatedFiles.ts similarity index 100% rename from src/app/store/slices/storage/fileNameUtils/checkDuplicatedFiles.ts rename to src/app/store/slices/storage/fileUtils/checkDuplicatedFiles.ts diff --git a/src/app/store/slices/storage/fileNameUtils/getUniqueFileName.test.ts b/src/app/store/slices/storage/fileUtils/getUniqueFileName.test.ts similarity index 100% rename from src/app/store/slices/storage/fileNameUtils/getUniqueFileName.test.ts rename to src/app/store/slices/storage/fileUtils/getUniqueFileName.test.ts diff --git a/src/app/store/slices/storage/fileNameUtils/getUniqueFilename.ts b/src/app/store/slices/storage/fileUtils/getUniqueFilename.ts similarity index 100% rename from src/app/store/slices/storage/fileNameUtils/getUniqueFilename.ts rename to src/app/store/slices/storage/fileUtils/getUniqueFilename.ts diff --git a/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.test.ts b/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.test.ts similarity index 100% rename from src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.test.ts rename to src/app/store/slices/storage/fileUtils/prepareFilesToUpload.test.ts diff --git a/src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts b/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts similarity index 100% rename from src/app/store/slices/storage/fileNameUtils/prepareFilesToUpload.ts rename to src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts diff --git a/src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.ts similarity index 100% rename from src/app/store/slices/storage/fileNameUtils/processDuplicateFiles.ts rename to src/app/store/slices/storage/fileUtils/processDuplicateFiles.ts diff --git a/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts b/src/app/store/slices/storage/folderUtils/checkFolderDuplicated.test.ts similarity index 100% rename from src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.test.ts rename to src/app/store/slices/storage/folderUtils/checkFolderDuplicated.test.ts diff --git a/src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts b/src/app/store/slices/storage/folderUtils/checkFolderDuplicated.ts similarity index 100% rename from src/app/store/slices/storage/folderNameUtils/checkFolderDuplicated.ts rename to src/app/store/slices/storage/folderUtils/checkFolderDuplicated.ts diff --git a/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts b/src/app/store/slices/storage/folderUtils/getUniqueFolderName.test.ts similarity index 100% rename from src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.test.ts rename to src/app/store/slices/storage/folderUtils/getUniqueFolderName.test.ts diff --git a/src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts b/src/app/store/slices/storage/folderUtils/getUniqueFolderName.ts similarity index 100% rename from src/app/store/slices/storage/folderNameUtils/getUniqueFolderName.ts rename to src/app/store/slices/storage/folderUtils/getUniqueFolderName.ts diff --git a/src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.test.ts b/src/app/store/slices/storage/folderUtils/renameFolderIfNeeded.test.ts similarity index 100% rename from src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.test.ts rename to src/app/store/slices/storage/folderUtils/renameFolderIfNeeded.test.ts diff --git a/src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.ts b/src/app/store/slices/storage/folderUtils/renameFolderIfNeeded.ts similarity index 100% rename from src/app/store/slices/storage/folderNameUtils/renameFolderIfNeeded.ts rename to src/app/store/slices/storage/folderUtils/renameFolderIfNeeded.ts diff --git a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts index 8990e8474..fa078ce37 100644 --- a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts @@ -12,10 +12,10 @@ import { SdkFactory } from '../../../../core/factory/sdk'; import errorService from '../../../../core/services/error.service'; import { uiActions } from '../../ui'; import workspacesSelectors from '../../workspaces/workspaces.selectors'; -import { checkDuplicatedFiles } from '../fileNameUtils/checkDuplicatedFiles'; -import { getUniqueFilename } from '../fileNameUtils/getUniqueFilename'; -import { checkFolderDuplicated } from '../folderNameUtils/checkFolderDuplicated'; -import renameFolderIfNeeded from '../folderNameUtils/renameFolderIfNeeded'; +import { checkDuplicatedFiles } from '../fileUtils/checkDuplicatedFiles'; +import { getUniqueFilename } from '../fileUtils/getUniqueFilename'; +import { checkFolderDuplicated } from '../folderUtils/checkFolderDuplicated'; +import renameFolderIfNeeded from '../folderUtils/renameFolderIfNeeded'; import { StorageState } from '../storage.model'; import storageSelectors from '../storage.selectors'; import { IRoot } from './uploadFolderThunk'; diff --git a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts index 806a0f22d..aa40837de 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadFolderThunk.ts @@ -12,8 +12,8 @@ import { TaskStatus, TaskType, UploadFolderTask } from '../../../../tasks/types' import { planThunks } from '../../plan'; import workspacesSelectors from '../../workspaces/workspaces.selectors'; -import { checkFolderDuplicated } from '../folderNameUtils/checkFolderDuplicated'; -import { getUniqueFolderName } from '../folderNameUtils/getUniqueFolderName'; +import { checkFolderDuplicated } from '../folderUtils/checkFolderDuplicated'; +import { getUniqueFolderName } from '../folderUtils/getUniqueFolderName'; import { StorageState } from '../storage.model'; import { deleteItemsThunk } from './deleteItemsThunk'; import { uploadItemsParallelThunk } from './uploadItemsThunk'; diff --git a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts index 5f332592d..c7b9502f5 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts @@ -23,7 +23,7 @@ import { planThunks } from '../../plan'; import { uiActions } from '../../ui'; import workspacesSelectors from '../../workspaces/workspaces.selectors'; -import { prepareFilesToUpload } from '../fileNameUtils/prepareFilesToUpload'; +import { prepareFilesToUpload } from '../fileUtils/prepareFilesToUpload'; import { StorageState } from '../storage.model'; interface UploadItemsThunkOptions { From 09bcc711a434c78ca0c6b1b2da81e1fdad8b798d Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Thu, 19 Sep 2024 17:15:43 +0200 Subject: [PATCH 08/11] Refactored prepareFileToUpload function --- .../storage/fileUtils/prepareFilesToUpload.ts | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts b/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts index 618989544..adfd7d8d6 100644 --- a/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts +++ b/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts @@ -1,3 +1,4 @@ +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; import { FileToUpload } from '../../../../drive/services/file.service/uploadFile'; import { checkDuplicatedFiles } from './checkDuplicatedFiles'; import { processDuplicateFiles } from './processDuplicateFiles'; @@ -7,7 +8,7 @@ const BATCH_SIZE = 200; export const prepareFilesToUpload = async ({ files, parentFolderId, - disableDuplicatedNamesCheck, + disableDuplicatedNamesCheck = false, fileType, }: { files: File[]; @@ -18,34 +19,32 @@ export const prepareFilesToUpload = async ({ let filesToUpload: FileToUpload[] = []; let zeroLengthFilesNumber = 0; - const processFilesBatch = async (filesBatch: File[]) => { - const { duplicatedFilesResponse, filesWithoutDuplicates, filesWithDuplicates } = await checkDuplicatedFiles( - filesBatch, - parentFolderId, - ); - - const { zeroLengthFiles, newFilesToUpload: filesToUploadReturned } = await processDuplicateFiles({ - files: filesWithoutDuplicates as File[], + const processFiles = async ( + filesBatch: File[], + disableDuplicatedNamesCheckOverride: boolean, + duplicatedFiles?: DriveFileData[], + ) => { + const { zeroLengthFiles, newFilesToUpload } = await processDuplicateFiles({ + files: filesBatch, existingFilesToUpload: filesToUpload, fileType, parentFolderId, - disableDuplicatedNamesCheck: true, + disableDuplicatedNamesCheck: disableDuplicatedNamesCheckOverride, + duplicatedFiles, }); - filesToUpload = filesToUploadReturned; + + filesToUpload = newFilesToUpload; zeroLengthFilesNumber += zeroLengthFiles; + }; + + const processFilesBatch = async (filesBatch: File[]) => { + const { duplicatedFilesResponse, filesWithoutDuplicates, filesWithDuplicates } = await checkDuplicatedFiles( + filesBatch, + parentFolderId, + ); - const { zeroLengthFiles: zeroLengthFilesReturned, newFilesToUpload: filesToUploadReturned2 } = - await processDuplicateFiles({ - files: filesWithDuplicates as File[], - existingFilesToUpload: filesToUpload, - fileType, - parentFolderId, - disableDuplicatedNamesCheck, - duplicatedFiles: duplicatedFilesResponse, - }); - - filesToUpload = filesToUploadReturned2; - zeroLengthFilesNumber += zeroLengthFilesReturned; + await processFiles(filesWithoutDuplicates as File[], true); + await processFiles(filesWithDuplicates as File[], disableDuplicatedNamesCheck, duplicatedFilesResponse); }; for (let i = 0; i < files.length; i += BATCH_SIZE) { From 8ed7fc12395a59c9be0180cce60b4ceb609951b6 Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Mon, 23 Sep 2024 16:54:59 +0200 Subject: [PATCH 09/11] Added new folder names check to rename items thunk --- .../storage.thunks/renameItemsThunk.ts | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts index fa078ce37..a5161db34 100644 --- a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts @@ -1,6 +1,6 @@ import { ActionReducerMapBuilder, createAsyncThunk, Dispatch } from '@reduxjs/toolkit'; -import { DriveItemData } from 'app/drive/types'; +import { DriveFolderData, DriveItemData } from 'app/drive/types'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import tasksService from 'app/tasks/services/tasks.service'; import { RenameFileTask, RenameFolderTask, TaskStatus, TaskType } from 'app/tasks/types'; @@ -8,16 +8,13 @@ import { t } from 'i18next'; import storageThunks from '.'; import { storageActions } from '..'; import { RootState } from '../../..'; -import { SdkFactory } from '../../../../core/factory/sdk'; import errorService from '../../../../core/services/error.service'; import { uiActions } from '../../ui'; -import workspacesSelectors from '../../workspaces/workspaces.selectors'; import { checkDuplicatedFiles } from '../fileUtils/checkDuplicatedFiles'; import { getUniqueFilename } from '../fileUtils/getUniqueFilename'; import { checkFolderDuplicated } from '../folderUtils/checkFolderDuplicated'; -import renameFolderIfNeeded from '../folderUtils/renameFolderIfNeeded'; +import { getUniqueFolderName } from '../folderUtils/getUniqueFolderName'; import { StorageState } from '../storage.model'; -import storageSelectors from '../storage.selectors'; import { IRoot } from './uploadFolderThunk'; export const handleRepeatedUploadingFiles = async ( @@ -70,32 +67,23 @@ export const renameItemsThunk = createAsyncThunk { const promises: Promise[] = []; - const state = getState(); - const workspaceCredentials = workspacesSelectors.getWorkspaceCredentials(state); if (items.some((item) => item.isFolder && item.uuid === destinationFolderId)) { return void notificationsService.show({ text: t('error.movingItemInsideItself'), type: ToastType.Error }); } - const currentFolderItems = storageSelectors.currentFolderItems(state); - for (const [index, item] of items.entries()) { let itemParsed: DriveItemData; - const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); - - const [parentFolderContentPromise] = storageClient.getFolderContentByUuid( - destinationFolderId, - false, - workspaceCredentials?.tokenHeader, - ); - const parentFolderContent = await parentFolderContentPromise; - if (item.isFolder) { - const currentFolderFolders = currentFolderItems.filter((item) => item?.isFolder); - const allFolderToCheckNames = [...parentFolderContent.children, ...currentFolderFolders]; - const [, , finalFilename] = renameFolderIfNeeded(allFolderToCheckNames, item.name); - itemParsed = { ...item, name: finalFilename, plain_name: finalFilename }; + const { duplicatedFoldersResponse } = await checkFolderDuplicated([item], destinationFolderId); + + const finalFolderName = await getUniqueFolderName( + item.plainName ?? item.name, + duplicatedFoldersResponse as DriveFolderData[], + destinationFolderId, + ); + itemParsed = { ...item, name: finalFolderName, plain_name: finalFolderName }; } else { const { duplicatedFilesResponse } = await checkDuplicatedFiles([item], destinationFolderId); From c2ae5eb680a837a0a042f1526eecf0c780c275ab Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Thu, 26 Sep 2024 13:12:33 +0200 Subject: [PATCH 10/11] Added handle repeated items feature to move items dialog --- .../MoveItemsDialog/MoveItemsDialog.tsx | 32 ++++++++--- .../storage/fileUtils/checkDuplicatedFiles.ts | 57 ++++++++++++------- .../storage.thunks/renameItemsThunk.ts | 9 +-- 3 files changed, 65 insertions(+), 33 deletions(-) diff --git a/src/app/drive/components/MoveItemsDialog/MoveItemsDialog.tsx b/src/app/drive/components/MoveItemsDialog/MoveItemsDialog.tsx index eee2038bc..1dfe42c76 100644 --- a/src/app/drive/components/MoveItemsDialog/MoveItemsDialog.tsx +++ b/src/app/drive/components/MoveItemsDialog/MoveItemsDialog.tsx @@ -20,7 +20,12 @@ import { uiActions } from 'app/store/slices/ui'; import folderImage from 'assets/icons/light/folder.svg'; import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { DriveItemData, FolderPathDialog } from '../../types'; +import { + handleRepeatedUploadingFiles, + handleRepeatedUploadingFolders, +} from '../../../store/slices/storage/storage.thunks/renameItemsThunk'; +import { IRoot } from '../../../store/slices/storage/types'; +import { DriveFileData, DriveFolderData, DriveItemData, FolderPathDialog } from '../../types'; import CreateFolderDialog from '../CreateFolderDialog/CreateFolderDialog'; interface MoveItemsDialogProps { @@ -152,6 +157,8 @@ const MoveItemsDialog = (props: MoveItemsDialogProps): JSX.Element => { const onAccept = async (destinationFolderId, name, namePaths): Promise => { try { + dispatch(storageActions.setMoveDestinationFolderId(destinationFolderId)); + setIsLoading(true); if (itemsToMove.length > 0) { if (destinationFolderId != currentFolderId) { @@ -162,14 +169,23 @@ const MoveItemsDialog = (props: MoveItemsDialogProps): JSX.Element => { destinationFolderId = currentFolderId; } - await dispatch( - storageThunks.moveItemsThunk({ - items: itemsToMove, - destinationFolderId: destinationFolderId, - }), - ); - } + const files = itemsToMove.filter((item) => item.type !== 'folder') as DriveFileData[]; + const folders = itemsToMove.filter((item) => item.type === 'folder') as (IRoot | DriveFolderData)[]; + + const filesWithoutDuplicates = await handleRepeatedUploadingFiles(files, dispatch, destinationFolderId); + const foldersWithoutDuplicates = await handleRepeatedUploadingFolders(folders, dispatch, destinationFolderId); + const itemsToMoveWithoutDuplicates = [...filesWithoutDuplicates, ...foldersWithoutDuplicates]; + + if (itemsToMoveWithoutDuplicates.length > 0) { + await dispatch( + storageThunks.moveItemsThunk({ + items: itemsToMoveWithoutDuplicates as DriveItemData[], + destinationFolderId: destinationFolderId, + }), + ); + } + } props.onItemsMoved?.(); setIsLoading(false); diff --git a/src/app/store/slices/storage/fileUtils/checkDuplicatedFiles.ts b/src/app/store/slices/storage/fileUtils/checkDuplicatedFiles.ts index b6597e17e..8a5c4c46a 100644 --- a/src/app/store/slices/storage/fileUtils/checkDuplicatedFiles.ts +++ b/src/app/store/slices/storage/fileUtils/checkDuplicatedFiles.ts @@ -20,31 +20,46 @@ export const checkDuplicatedFiles = async ( } as DuplicatedFilesResult; } - const filesToUploadParsedToCheck = files.map((file) => { - const { filename, extension } = itemUtils.getFilenameAndExt(file.name); - return { plainName: filename, type: extension }; - }); + const parsedFiles = files.map(parseFile); + const checkDuplicatedFileResponse = await newStorageService.checkDuplicatedFiles(parentFolderId, parsedFiles); - const checkDuplicatedFileResponse = await newStorageService.checkDuplicatedFiles( - 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, - ); + const { filesWithDuplicates, filesWithoutDuplicates } = parsedFiles.reduce( + (acc, parsedFile) => { + const isDuplicated = duplicatedFilesResponse.some( + (duplicatedFile) => + duplicatedFile.plainName === parsedFile.plainName && duplicatedFile.type === parsedFile.type, + ); - if (isDuplicated) { - filesWithDuplicates.push(file); - } else { - filesWithoutDuplicates.push(file); - } - }); + if (isDuplicated) { + acc.filesWithDuplicates.push(parsedFile.originalFile); + } else { + acc.filesWithoutDuplicates.push(parsedFile.originalFile); + } + + return acc; + }, + { filesWithDuplicates: [], filesWithoutDuplicates: [] } as { + filesWithDuplicates: (File | DriveFileData)[]; + filesWithoutDuplicates: (File | DriveFileData)[]; + }, + ); return { duplicatedFilesResponse, filesWithoutDuplicates, filesWithDuplicates }; }; + +interface ParsedFile { + plainName: string; + type: string; + originalFile: File | DriveFileData; +} + +const parseFile = (file: File | DriveFileData): ParsedFile => { + if (file instanceof File) { + const { filename, extension } = itemUtils.getFilenameAndExt(file.name); + return { plainName: filename, type: extension, originalFile: file }; + } else { + return { plainName: file.name, type: file.type, originalFile: file }; + } +}; diff --git a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts index a5161db34..79fbc3444 100644 --- a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts @@ -1,5 +1,6 @@ import { ActionReducerMapBuilder, createAsyncThunk, Dispatch } from '@reduxjs/toolkit'; +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; import { DriveFolderData, DriveItemData } from 'app/drive/types'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import tasksService from 'app/tasks/services/tasks.service'; @@ -18,10 +19,10 @@ import { StorageState } from '../storage.model'; import { IRoot } from './uploadFolderThunk'; export const handleRepeatedUploadingFiles = async ( - files: (DriveItemData | File)[], + files: (DriveFileData | File)[], dispatch: Dispatch, destinationFolderUuid: string, -): Promise<(DriveItemData | File)[]> => { +): Promise<(DriveFileData | File)[]> => { const { filesWithDuplicates: filesRepeated, duplicatedFilesResponse, @@ -38,10 +39,10 @@ export const handleRepeatedUploadingFiles = async ( }; export const handleRepeatedUploadingFolders = async ( - folders: (DriveItemData | IRoot)[], + folders: (DriveFolderData | IRoot)[], dispatch: Dispatch, destinationFolderUuid: string, -): Promise<(DriveItemData | IRoot)[]> => { +): Promise<(DriveFolderData | IRoot)[]> => { const { foldersWithDuplicates: foldersRepeated, duplicatedFoldersResponse, From f4e0f384bc05725ba45a0070959dfb8fa90e4bbc Mon Sep 17 00:00:00 2001 From: Ramon Candel Date: Tue, 1 Oct 2024 16:57:02 +0200 Subject: [PATCH 11/11] Added task subtitle to rename or move tasks --- src/app/drive/types/index.ts | 1 + .../storage/storage.thunks/moveItemsThunk.ts | 4 +- .../storage.thunks/renameItemsThunk.ts | 64 ++++++++++--------- src/app/tasks/services/tasks.service/index.ts | 14 +++- 4 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/app/drive/types/index.ts b/src/app/drive/types/index.ts index 6e4ac4b7b..6d53299d5 100644 --- a/src/app/drive/types/index.ts +++ b/src/app/drive/types/index.ts @@ -27,6 +27,7 @@ export interface DriveFolderData { shares?: Array; sharings?: { type: string; id: string }[]; uuid: string; + type?: string; } export interface DriveFolderMetadataPayload { diff --git a/src/app/store/slices/storage/storage.thunks/moveItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/moveItemsThunk.ts index 270468f8e..c363b0b58 100644 --- a/src/app/store/slices/storage/storage.thunks/moveItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/moveItemsThunk.ts @@ -110,9 +110,9 @@ export const moveItemsThunkExtraReducers = (builder: ActionReducerMapBuilder undefined) .addCase(moveItemsThunk.fulfilled, () => undefined) - .addCase(moveItemsThunk.rejected, (state, action) => { + .addCase(moveItemsThunk.rejected, (_, action) => { notificationsService.show({ - text: 'error', + text: action.error.message ?? t('error.movingItem'), type: ToastType.Error, }); }); diff --git a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts index 79fbc3444..27b4bb2fa 100644 --- a/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/renameItemsThunk.ts @@ -9,7 +9,6 @@ import { t } from 'i18next'; import storageThunks from '.'; import { storageActions } from '..'; import { RootState } from '../../..'; -import errorService from '../../../../core/services/error.service'; import { uiActions } from '../../ui'; import { checkDuplicatedFiles } from '../fileUtils/checkDuplicatedFiles'; import { getUniqueFilename } from '../fileUtils/getUniqueFilename'; @@ -66,7 +65,7 @@ export interface RenameItemsPayload { export const renameItemsThunk = createAsyncThunk( 'storage/renameItems', - async ({ items, destinationFolderId, onRenameSuccess }: RenameItemsPayload, { getState, dispatch }) => { + async ({ items, destinationFolderId, onRenameSuccess }: RenameItemsPayload, { dispatch }) => { const promises: Promise[] = []; if (items.some((item) => item.isFolder && item.uuid === destinationFolderId)) { @@ -97,39 +96,44 @@ export const renameItemsThunk = createAsyncThunk({ - action: TaskType.RenameFolder, - showNotification: true, - folder: itemParsed, - destinationFolderId, - cancellable: true, - }); - } else { - taskId = tasksService.create({ - action: TaskType.RenameFile, - showNotification: true, - file: itemParsed, - destinationFolderId, - cancellable: true, - }); - } + const taskId: string = itemParsed.isFolder + ? tasksService.create({ + action: TaskType.RenameFolder, + showNotification: true, + folder: itemParsed, + destinationFolderId, + cancellable: true, + }) + : tasksService.create({ + action: TaskType.RenameFile, + showNotification: true, + file: itemParsed, + destinationFolderId, + cancellable: true, + }); promises.push(dispatch(storageThunks.updateItemMetadataThunk({ item, metadata: { itemName: itemParsed.name } }))); promises[index] - .then(async () => { - tasksService.updateTask({ - taskId, - merge: { - status: TaskStatus.Success, - }, - }); - setTimeout(() => onRenameSuccess?.(itemParsed), 1000); + .then(async (result) => { + if (!result.error) { + tasksService.updateTask({ + taskId, + merge: { + status: TaskStatus.Success, + }, + }); + setTimeout(() => onRenameSuccess?.(itemParsed), 1000); + } else { + tasksService.updateTask({ + taskId, + merge: { + status: TaskStatus.Error, + }, + }); + } }) - .catch((e) => { - errorService.reportError(e); + .catch(() => { tasksService.updateTask({ taskId, merge: { diff --git a/src/app/tasks/services/tasks.service/index.ts b/src/app/tasks/services/tasks.service/index.ts index 48c003244..8df7b4d3c 100644 --- a/src/app/tasks/services/tasks.service/index.ts +++ b/src/app/tasks/services/tasks.service/index.ts @@ -4,6 +4,7 @@ import { uniqueId } from 'lodash'; import { FunctionComponent, SVGProps } from 'react'; import iconService from 'app/drive/services/icon.service'; +import { t } from 'i18next'; import { BaseTask, DownloadFilesData, @@ -203,7 +204,9 @@ class TaskManagerService { break; } case TaskType.RenameFolder: { - title = itemsLib.getItemDisplayName(task.folder); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { type, ...driveFolderWithoutType } = task.folder; + title = itemsLib.getItemDisplayName(driveFolderWithoutType); break; } } @@ -212,9 +215,18 @@ class TaskManagerService { } private getTaskNotificationSubtitle(task: TaskData): string { + const isRenameOrMoveTask = + task.action === TaskType.RenameFolder || + task.action === TaskType.RenameFile || + task.action === TaskType.MoveFile || + task.action === TaskType.MoveFolder; + if (task.status === TaskStatus.Error && task.subtitle) { return task.subtitle; } + + if (isRenameOrMoveTask) return t(`tasks.${task.action}.status.${task.status}`); + return ''; }