diff --git a/src/plugins/files/server/file_service/file_action_types.ts b/src/plugins/files/server/file_service/file_action_types.ts index 4247f567802ed..fb8432147326f 100644 --- a/src/plugins/files/server/file_service/file_action_types.ts +++ b/src/plugins/files/server/file_service/file_action_types.ts @@ -62,6 +62,16 @@ export interface DeleteFileArgs { id: string; } +/** + * Arguments to delete files in a bulk request. + */ +export interface BulkDeleteFilesArgs { + /** + * File IDs. + */ + ids: string[]; +} + /** * Arguments to get a file by ID. */ diff --git a/src/plugins/files/server/file_service/file_service.ts b/src/plugins/files/server/file_service/file_service.ts index 9dc1b0769cedf..0828d03ada7fd 100644 --- a/src/plugins/files/server/file_service/file_service.ts +++ b/src/plugins/files/server/file_service/file_service.ts @@ -12,6 +12,7 @@ import type { CreateFileArgs, UpdateFileArgs, DeleteFileArgs, + BulkDeleteFilesArgs, GetByIdArgs, FindFileArgs, } from './file_action_types'; @@ -43,6 +44,13 @@ export interface FileServiceStart { */ delete(args: DeleteFileArgs): Promise; + /** + * Delete multiple files at once. + * + * @param args - delete files args + */ + bulkDelete(args: BulkDeleteFilesArgs): Promise>>; + /** * Get a file by ID. Will throw if file cannot be found. * diff --git a/src/plugins/files/server/file_service/file_service_factory.ts b/src/plugins/files/server/file_service/file_service_factory.ts index 50ceafb6fc249..bebb5b72c2b3d 100644 --- a/src/plugins/files/server/file_service/file_service_factory.ts +++ b/src/plugins/files/server/file_service/file_service_factory.ts @@ -94,7 +94,10 @@ export class FileServiceFactoryImpl implements FileServiceFactory { await internalFileService.updateFile(args); }, async delete(args) { - return internalFileService.deleteFile(args); + return await internalFileService.deleteFile(args); + }, + async bulkDelete(args) { + return await internalFileService.bulkDeleteFiles(args); }, async getById(args: GetByIdArgs) { return internalFileService.getById(args) as Promise>; diff --git a/src/plugins/files/server/file_service/internal_file_service.ts b/src/plugins/files/server/file_service/internal_file_service.ts index cd7a45740de4e..653929f7b1236 100644 --- a/src/plugins/files/server/file_service/internal_file_service.ts +++ b/src/plugins/files/server/file_service/internal_file_service.ts @@ -8,6 +8,7 @@ import { Logger, SavedObjectsErrorHelpers } from '@kbn/core/server'; import { AuditEvent, AuditLogger } from '@kbn/security-plugin/server'; +import pLimit from 'p-limit'; import { BlobStorageService } from '../blob_storage_service'; import { InternalFileShareService } from '../file_share_service'; @@ -20,10 +21,14 @@ import type { CreateFileArgs, UpdateFileArgs, DeleteFileArgs, + BulkDeleteFilesArgs, FindFileArgs, GetByIdArgs, } from './file_action_types'; import { createFileClient, FileClientImpl } from '../file_client/file_client'; + +const bulkDeleteConcurrency = pLimit(10); + /** * Service containing methods for working with files. * @@ -64,6 +69,14 @@ export class InternalFileService { await file.delete(); } + public async bulkDeleteFiles({ + ids, + }: BulkDeleteFilesArgs): Promise>> { + const promises = ids.map((id) => bulkDeleteConcurrency(() => this.deleteFile({ id }))); + const result = await Promise.allSettled(promises); + return result; + } + private async get(id: string) { try { const { metadata } = await this.metadataClient.get({ id }); diff --git a/src/plugins/files/server/integration_tests/file_service.test.ts b/src/plugins/files/server/integration_tests/file_service.test.ts index 25d7f463de03a..adb551438973c 100644 --- a/src/plugins/files/server/integration_tests/file_service.test.ts +++ b/src/plugins/files/server/integration_tests/file_service.test.ts @@ -99,7 +99,7 @@ describe('FileService', () => { return file; } afterEach(async () => { - await Promise.all(disposables.map((file) => file.delete())); + await fileService.bulkDelete({ ids: disposables.map((d) => d.id) }); const { files } = await fileService.find({ kind: [fileKind] }); expect(files.length).toBe(0); disposables = []; @@ -233,7 +233,7 @@ describe('FileService', () => { expect(result3.files.length).toBe(2); }); - it('deletes files', async () => { + it('deletes a single file', async () => { const file = await fileService.create({ fileKind, name: 'test' }); const result = await fileService.find({ kind: [fileKind] }); expect(result.files.length).toBe(1); @@ -241,6 +241,25 @@ describe('FileService', () => { expect(await fileService.find({ kind: [fileKind] })).toEqual({ files: [], total: 0 }); }); + it('deletes a single file using the bulk method', async () => { + const file = await fileService.create({ fileKind, name: 'test' }); + const result = await fileService.find({ kind: [fileKind] }); + expect(result.files.length).toBe(1); + await fileService.bulkDelete({ ids: [file.id] }); + expect(await fileService.find({ kind: [fileKind] })).toEqual({ files: [], total: 0 }); + }); + + it('deletes multiple files using the bulk method', async () => { + const promises = Array.from({ length: 15 }, (v, i) => + fileService.create({ fileKind, name: 'test ' + i }) + ); + const files = await Promise.all(promises); + const result = await fileService.find({ kind: [fileKind] }); + expect(result.files.length).toBe(15); + await fileService.bulkDelete({ ids: files.map((file) => file.id) }); + expect(await fileService.find({ kind: [fileKind] })).toEqual({ files: [], total: 0 }); + }); + interface CustomMeta { some: string; } diff --git a/src/plugins/files/server/mocks.ts b/src/plugins/files/server/mocks.ts index 8472717b544a1..da94d95b273c4 100644 --- a/src/plugins/files/server/mocks.ts +++ b/src/plugins/files/server/mocks.ts @@ -15,6 +15,7 @@ import { FileClient, FileServiceFactory, FileServiceStart, FilesSetup } from '.' export const createFileServiceMock = (): DeeplyMockedKeys => ({ create: jest.fn(), delete: jest.fn(), + bulkDelete: jest.fn(), deleteShareObject: jest.fn(), find: jest.fn(), getById: jest.fn(),