diff --git a/src/backend/features/sync/recovery-sync/common/get-deleted-items.test.ts b/src/backend/features/sync/recovery-sync/common/get-deleted-items.test.ts index 0589f3065..7fa17d9dd 100644 --- a/src/backend/features/sync/recovery-sync/common/get-deleted-items.test.ts +++ b/src/backend/features/sync/recovery-sync/common/get-deleted-items.test.ts @@ -1,33 +1,36 @@ -import { calls, mockProps } from '@/tests/vitest/utils.helper.test'; -import { loggerMock } from '@/tests/vitest/mocks.helper.test'; +import { mockProps, partialSpyOn } from '@/tests/vitest/utils.helper.test'; import { getDeletedItems } from './get-deleted-items'; import { FileUuid } from '@/apps/main/database/entities/DriveFile'; +import * as isItemDeletedModule from './is-item-deleted'; describe('get-deleted-items', () => { + const isItemDeletedMock = partialSpyOn(isItemDeletedModule, 'isItemDeleted'); + let props: Parameters[0]; beforeEach(() => { props = mockProps({ - remotes: [{ uuid: 'uuid' as FileUuid }], - locals: [{ uuid: 'uuid' as FileUuid }], + checkpoint: {}, + remotes: [], + locals: [{ uuid: 'uuid' as FileUuid, updatedAt: 'datetime' }], }); }); - it('should return item if not exists remotely', () => { + it('should return empty if item is not deleted', () => { // Given - props.remotes = []; + isItemDeletedMock.mockReturnValue(false); // When const res = getDeletedItems(props); // Then - expect(res).toHaveLength(1); - calls(loggerMock.error).toMatchObject([{ msg: 'Remote item does not exist' }]); + expect(res).toHaveLength(0); }); - it('should not return item if updatedAt and status are equal', () => { + it('should return local if item is deleted', () => { + // Given + isItemDeletedMock.mockReturnValue(true); // When const res = getDeletedItems(props); // Then - expect(res).toHaveLength(0); - calls(loggerMock.error).toHaveLength(0); + expect(res).toHaveLength(1); }); }); diff --git a/src/backend/features/sync/recovery-sync/common/get-deleted-items.ts b/src/backend/features/sync/recovery-sync/common/get-deleted-items.ts index d0525e277..a76e25ab7 100644 --- a/src/backend/features/sync/recovery-sync/common/get-deleted-items.ts +++ b/src/backend/features/sync/recovery-sync/common/get-deleted-items.ts @@ -1,31 +1,18 @@ import { SimpleDriveFile } from '@/apps/main/database/entities/DriveFile'; import { SimpleDriveFolder } from '@/apps/main/database/entities/DriveFolder'; import { FileProps, FolderProps } from '../recovery-sync.types'; +import { isItemDeleted } from './is-item-deleted'; type Props = FileProps | FolderProps; export function getDeletedItems(props: FolderProps): SimpleDriveFolder[]; export function getDeletedItems(props: FileProps): SimpleDriveFile[]; -export function getDeletedItems({ ctx, type, remotes, locals }: Props) { - const remotesMap = new Map(remotes.map((item) => [item.uuid, item])); - - const itemsToDelete = locals.filter((local) => { - const remote = remotesMap.get(local.uuid); - - if (!remote) { - ctx.logger.error({ - msg: 'Remote item does not exist', - type, - name: local.name, - updatedAt: local.updatedAt, - }); +export function getDeletedItems({ ctx, type, remotes, locals, checkpoint }: Props) { + const checkpointDate = new Date(checkpoint.updatedAt); - return true; - } - - return false; - }); + const remotesMap = new Map(remotes.map((item) => [item.uuid, item])); + const deletedItems = locals.filter((local) => isItemDeleted({ ctx, type, local, remotesMap, checkpointDate })); - return itemsToDelete; + return deletedItems; } diff --git a/src/backend/features/sync/recovery-sync/common/get-items-to-sync.test.ts b/src/backend/features/sync/recovery-sync/common/get-items-to-sync.test.ts index 37fffb593..829f1e2e9 100644 --- a/src/backend/features/sync/recovery-sync/common/get-items-to-sync.test.ts +++ b/src/backend/features/sync/recovery-sync/common/get-items-to-sync.test.ts @@ -2,46 +2,34 @@ import { mockProps, partialSpyOn } from '@/tests/vitest/utils.helper.test'; import { getItemsToSync } from './get-items-to-sync'; import { FileUuid } from '@/apps/main/database/entities/DriveFile'; import * as isItemToSyncModule from './is-item-to-sync'; -import { SqliteModule } from '@/infra/sqlite/sqlite.module'; describe('get-items-to-sync', () => { - const getCheckpointMock = partialSpyOn(SqliteModule.CheckpointModule, 'getCheckpoint'); const isItemToSyncMock = partialSpyOn(isItemToSyncModule, 'isItemToSync'); let props: Parameters[0]; beforeEach(() => { - getCheckpointMock.mockResolvedValue({ data: { updatedAt: 'datetime' } }); - props = mockProps({ + checkpoint: {}, remotes: [{ uuid: 'uuid' as FileUuid, updatedAt: 'datetime' }], locals: [], }); }); - it('should return empty if there is no checkpoint', async () => { - // Given - getCheckpointMock.mockResolvedValue({ data: undefined }); - // When - const res = await getItemsToSync(props); - // Then - expect(res).toHaveLength(0); - }); - - it('should return empty if it is not item to sync', async () => { + it('should return empty if item is synced', () => { // Given isItemToSyncMock.mockReturnValue(false); // When - const res = await getItemsToSync(props); + const res = getItemsToSync(props); // Then expect(res).toHaveLength(0); }); - it('should return remote if it is item to sync', async () => { + it('should return remote if item is not synced', () => { // Given isItemToSyncMock.mockReturnValue(true); // When - const res = await getItemsToSync(props); + const res = getItemsToSync(props); // Then expect(res).toHaveLength(1); }); diff --git a/src/backend/features/sync/recovery-sync/common/get-items-to-sync.ts b/src/backend/features/sync/recovery-sync/common/get-items-to-sync.ts index 8e964325a..1ac945a8a 100644 --- a/src/backend/features/sync/recovery-sync/common/get-items-to-sync.ts +++ b/src/backend/features/sync/recovery-sync/common/get-items-to-sync.ts @@ -1,22 +1,13 @@ import { ParsedFileDto, ParsedFolderDto } from '@/infra/drive-server-wip/out/dto'; import { FileProps, FolderProps } from '../recovery-sync.types'; import { isItemToSync } from './is-item-to-sync'; -import { SqliteModule } from '@/infra/sqlite/sqlite.module'; type Props = FileProps | FolderProps; -export async function getItemsToSync(props: FolderProps): Promise; -export async function getItemsToSync(props: FileProps): Promise; - -export async function getItemsToSync({ ctx, type, remotes, locals }: Props) { - const { data: checkpoint } = await SqliteModule.CheckpointModule.getCheckpoint({ - userUuid: ctx.userUuid, - workspaceId: ctx.workspaceId, - type, - }); - - if (!checkpoint) return []; +export function getItemsToSync(props: FolderProps): ParsedFolderDto[]; +export function getItemsToSync(props: FileProps): ParsedFileDto[]; +export function getItemsToSync({ ctx, type, remotes, locals, checkpoint }: Props) { const checkpointDate = new Date(checkpoint.updatedAt); const localsMap = new Map(locals.map((file) => [file.uuid, file])); diff --git a/src/backend/features/sync/recovery-sync/common/is-item-deleted.test.ts b/src/backend/features/sync/recovery-sync/common/is-item-deleted.test.ts new file mode 100644 index 000000000..0140e5bd3 --- /dev/null +++ b/src/backend/features/sync/recovery-sync/common/is-item-deleted.test.ts @@ -0,0 +1,56 @@ +import { calls, mockProps } from '@/tests/vitest/utils.helper.test'; +import { loggerMock } from '@/tests/vitest/mocks.helper.test'; +import { FileUuid } from '@/apps/main/database/entities/DriveFile'; +import { isItemDeleted } from './is-item-deleted'; + +describe('is-item-deleted', () => { + let props: Parameters[0]; + + beforeEach(() => { + props = mockProps({ + checkpointDate: new Date('2024-01-03'), + local: { uuid: 'uuid' as FileUuid }, + remotesMap: new Map([['uuid' as FileUuid, { updatedAt: '2024-01-01' }]]), + }); + }); + + it('should return false is updatedAt is equal than checkpoint', () => { + // Given + props.local.updatedAt = '2024-01-03'; + // When + const res = isItemDeleted(props); + // Then + expect(res).toBe(false); + calls(loggerMock.error).toHaveLength(0); + }); + + it('should return false is updatedAt is greater than checkpoint', () => { + // Given + props.local.updatedAt = '2024-01-04'; + // When + const res = isItemDeleted(props); + // Then + expect(res).toBe(false); + calls(loggerMock.error).toHaveLength(0); + }); + + it('should return true if remote item does not exist', () => { + // Given + props.remotesMap = new Map(); + // When + const res = isItemDeleted(props); + // Then + expect(res).toBe(true); + calls(loggerMock.error).toMatchObject([{ msg: 'Remote item does not exist' }]); + }); + + it('should return false if updatedAt is equal', () => { + // Given + props.local.updatedAt = '2024-01-01'; + // When + const res = isItemDeleted(props); + // Then + expect(res).toBe(false); + calls(loggerMock.error).toHaveLength(0); + }); +}); diff --git a/src/backend/features/sync/recovery-sync/common/is-item-deleted.ts b/src/backend/features/sync/recovery-sync/common/is-item-deleted.ts new file mode 100644 index 000000000..f64943b9b --- /dev/null +++ b/src/backend/features/sync/recovery-sync/common/is-item-deleted.ts @@ -0,0 +1,33 @@ +import { FileUuid, SimpleDriveFile } from '@/apps/main/database/entities/DriveFile'; +import { FolderUuid, SimpleDriveFolder } from '@/apps/main/database/entities/DriveFolder'; +import { SyncContext } from '@/apps/sync-engine/config'; +import { ParsedFileDto, ParsedFolderDto } from '@/infra/drive-server-wip/out/dto'; + +type Props = { + ctx: SyncContext; + type: 'file' | 'folder'; + checkpointDate: Date; + local: SimpleDriveFile | SimpleDriveFolder; + remotesMap: Map; +}; + +export function isItemDeleted({ ctx, type, local, remotesMap, checkpointDate }: Props) { + if (new Date(local.updatedAt) >= checkpointDate) { + return false; + } + + const remote = remotesMap.get(local.uuid); + + if (!remote) { + ctx.logger.error({ + msg: 'Remote item does not exist', + type, + name: local.name, + updatedAt: local.updatedAt, + }); + + return true; + } + + return false; +} diff --git a/src/backend/features/sync/recovery-sync/common/is-item-to-sync.test.ts b/src/backend/features/sync/recovery-sync/common/is-item-to-sync.test.ts index bb4db9c30..6be397d16 100644 --- a/src/backend/features/sync/recovery-sync/common/is-item-to-sync.test.ts +++ b/src/backend/features/sync/recovery-sync/common/is-item-to-sync.test.ts @@ -34,7 +34,7 @@ describe('is-item-to-sync', () => { calls(loggerMock.error).toHaveLength(0); }); - it('should return true if not exists locally', () => { + it('should return true if local item does not exists', () => { // Given props.localsMap = new Map(); // When diff --git a/src/backend/features/sync/recovery-sync/files/files-recovery-sync.test.ts b/src/backend/features/sync/recovery-sync/files/files-recovery-sync.test.ts index b9c0317af..7b550f9c1 100644 --- a/src/backend/features/sync/recovery-sync/files/files-recovery-sync.test.ts +++ b/src/backend/features/sync/recovery-sync/files/files-recovery-sync.test.ts @@ -9,6 +9,7 @@ import * as getLocalFilesModule from './get-local-files'; import { SqliteModule } from '@/infra/sqlite/sqlite.module'; describe('files-recovery-sync', () => { + const getCheckpointMock = partialSpyOn(SqliteModule.CheckpointModule, 'getCheckpoint'); const getFilesMock = partialSpyOn(DriveServerWipModule.FileModule, 'getFiles'); const getLocalFilesMock = partialSpyOn(getLocalFilesModule, 'getLocalFiles'); const getItemsToSyncMock = partialSpyOn(getItemsToSyncModule, 'getItemsToSync'); @@ -21,12 +22,22 @@ describe('files-recovery-sync', () => { }); beforeEach(() => { + getCheckpointMock.mockResolvedValue({ data: { updatedAt: 'datetime' } }); getFilesMock.mockResolvedValue({ data: [{ uuid: 'uuid' as FileUuid }] }); getLocalFilesMock.mockResolvedValue([{ uuid: 'uuid' as FileUuid }]); - getItemsToSyncMock.mockResolvedValue([{ uuid: 'create' as FileUuid }]); + getItemsToSyncMock.mockReturnValue([{ uuid: 'create' as FileUuid }]); getDeletedItemsMock.mockReturnValue([{ uuid: 'deleted' as FileUuid, parentUuid: 'parentUuid' }]); }); + it('should return empty if no checkpoint', async () => { + // Given + getCheckpointMock.mockResolvedValue({ data: undefined }); + // When + const res = await filesRecoverySync(props); + // Then + expect(res).toHaveLength(0); + }); + it('should return empty if no remote files', async () => { // Given getFilesMock.mockResolvedValue({}); diff --git a/src/backend/features/sync/recovery-sync/files/files-recovery-sync.ts b/src/backend/features/sync/recovery-sync/files/files-recovery-sync.ts index e2aeb96fc..1bb2654b5 100644 --- a/src/backend/features/sync/recovery-sync/files/files-recovery-sync.ts +++ b/src/backend/features/sync/recovery-sync/files/files-recovery-sync.ts @@ -14,6 +14,14 @@ type Props = { }; export async function filesRecoverySync({ ctx, offset }: Props) { + const { data: checkpoint } = await SqliteModule.CheckpointModule.getCheckpoint({ + userUuid: ctx.userUuid, + workspaceId: ctx.workspaceId, + type: 'file', + }); + + if (!checkpoint) return []; + const query: GetFilesQuery = { limit: FETCH_LIMIT_1000, offset, @@ -35,8 +43,8 @@ export async function filesRecoverySync({ ctx, offset }: Props) { if (!locals) return []; - const filesToSync = await getItemsToSync({ ctx, type: 'file', remotes, locals }); - const deletedFiles = getDeletedItems({ ctx, type: 'file', remotes, locals }); + const filesToSync = getItemsToSync({ ctx, type: 'file', remotes, locals, checkpoint }); + const deletedFiles = getDeletedItems({ ctx, type: 'file', remotes, locals, checkpoint }); const filesToSyncPromises = createOrUpdateFiles({ ctx, fileDtos: filesToSync }); const deletedFilesPromises = deletedFiles.map(async (file) => { diff --git a/src/backend/features/sync/recovery-sync/folders/folders-recovery-sync.test.ts b/src/backend/features/sync/recovery-sync/folders/folders-recovery-sync.test.ts index 456d61431..affcba174 100644 --- a/src/backend/features/sync/recovery-sync/folders/folders-recovery-sync.test.ts +++ b/src/backend/features/sync/recovery-sync/folders/folders-recovery-sync.test.ts @@ -10,6 +10,7 @@ import { SqliteModule } from '@/infra/sqlite/sqlite.module'; import { FileUuid } from '@/apps/main/database/entities/DriveFile'; describe('folders-recovery-sync', () => { + const getCheckpointMock = partialSpyOn(SqliteModule.CheckpointModule, 'getCheckpoint'); const getFoldersMock = partialSpyOn(DriveServerWipModule.FolderModule, 'getFolders'); const getLocalFoldersMock = partialSpyOn(getLocalFoldersModule, 'getLocalFolders'); const getItemsToSyncMock = partialSpyOn(getItemsToSyncModule, 'getItemsToSync'); @@ -22,12 +23,22 @@ describe('folders-recovery-sync', () => { }); beforeEach(() => { + getCheckpointMock.mockResolvedValue({ data: { updatedAt: 'datetime' } }); getFoldersMock.mockResolvedValue({ data: [{ uuid: 'uuid' as FolderUuid }] }); getLocalFoldersMock.mockResolvedValue([{ uuid: 'uuid' as FolderUuid }]); - getItemsToSyncMock.mockResolvedValue([{ uuid: 'create' as FileUuid }]); + getItemsToSyncMock.mockReturnValue([{ uuid: 'create' as FileUuid }]); getDeletedItemsMock.mockReturnValue([{ uuid: 'deleted' as FileUuid, parentUuid: 'parentUuid' }]); }); + it('should return empty if no checkpoint', async () => { + // Given + getCheckpointMock.mockResolvedValue({ data: undefined }); + // When + const res = await foldersRecoverySync(props); + // Then + expect(res).toHaveLength(0); + }); + it('should return empty if no remote folders', async () => { // Given getFoldersMock.mockResolvedValue({}); diff --git a/src/backend/features/sync/recovery-sync/folders/folders-recovery-sync.ts b/src/backend/features/sync/recovery-sync/folders/folders-recovery-sync.ts index 2ec4d5aab..6b38544e4 100644 --- a/src/backend/features/sync/recovery-sync/folders/folders-recovery-sync.ts +++ b/src/backend/features/sync/recovery-sync/folders/folders-recovery-sync.ts @@ -14,6 +14,14 @@ type Props = { }; export async function foldersRecoverySync({ ctx, offset }: Props) { + const { data: checkpoint } = await SqliteModule.CheckpointModule.getCheckpoint({ + userUuid: ctx.userUuid, + workspaceId: ctx.workspaceId, + type: 'folder', + }); + + if (!checkpoint) return []; + const query: GetFoldersQuery = { limit: FETCH_LIMIT_1000, offset, @@ -35,8 +43,8 @@ export async function foldersRecoverySync({ ctx, offset }: Props) { if (!locals) return []; - const foldersToSync = await getItemsToSync({ ctx, type: 'folder', remotes, locals }); - const deletedFolders = getDeletedItems({ ctx, type: 'folder', remotes, locals }); + const foldersToSync = getItemsToSync({ ctx, type: 'folder', remotes, locals, checkpoint }); + const deletedFolders = getDeletedItems({ ctx, type: 'folder', remotes, locals, checkpoint }); const foldersToSyncPromises = createOrUpdateFolders({ ctx, folderDtos: foldersToSync }); const deletedFoldersPromises = deletedFolders.map(async (folder) => { diff --git a/src/backend/features/sync/recovery-sync/recovery-sync.types.ts b/src/backend/features/sync/recovery-sync/recovery-sync.types.ts index 5125ae00e..69671a8c7 100644 --- a/src/backend/features/sync/recovery-sync/recovery-sync.types.ts +++ b/src/backend/features/sync/recovery-sync/recovery-sync.types.ts @@ -1,7 +1,21 @@ +import { Checkpoint } from '@/apps/main/database/entities/checkpoint'; import { SimpleDriveFile } from '@/apps/main/database/entities/DriveFile'; import { SimpleDriveFolder } from '@/apps/main/database/entities/DriveFolder'; import { SyncContext } from '@/apps/sync-engine/config'; import { ParsedFileDto, ParsedFolderDto } from '@/infra/drive-server-wip/out/dto'; -export type FileProps = { ctx: SyncContext; type: 'file'; remotes: ParsedFileDto[]; locals: SimpleDriveFile[] }; -export type FolderProps = { ctx: SyncContext; type: 'folder'; remotes: ParsedFolderDto[]; locals: SimpleDriveFolder[] }; +export type FileProps = { + ctx: SyncContext; + type: 'file'; + remotes: ParsedFileDto[]; + locals: SimpleDriveFile[]; + checkpoint: Checkpoint; +}; + +export type FolderProps = { + ctx: SyncContext; + type: 'folder'; + remotes: ParsedFolderDto[]; + locals: SimpleDriveFolder[]; + checkpoint: Checkpoint; +};