feat: introduce bulkDelete method for IFileProvider (#12614)

Fixes: FRMW-2974

Currently during the product imports, we create multiple chunks that must be deleted after the import has finished (either successfully or with an error). Deleting files one by one leads to multiple network calls and slows down everything.

The `bulkDelete` method deletes multiple files (with their fileKey) in one go
This commit is contained in:
Harminder Virk
2025-05-27 12:22:11 +05:30
committed by GitHub
parent 730cac0ed2
commit 791276e80f
8 changed files with 111 additions and 38 deletions

View File

@@ -81,11 +81,11 @@ export default class FileModuleService implements FileTypes.IFileModuleService {
async deleteFiles(id: string, sharedContext?: Context): Promise<void>
async deleteFiles(ids: string[] | string): Promise<void> {
const input = Array.isArray(ids) ? ids : [ids]
await Promise.all(
input.map((id) => this.fileProviderService_.delete({ fileKey: id }))
await this.fileProviderService_.delete(
input.map((id) => {
return { fileKey: id }
})
)
return
}
async retrieveFile(id: string): Promise<FileDTO> {

View File

@@ -40,7 +40,11 @@ export default class FileProviderService {
return this.fileProvider_.upload(file)
}
delete(fileData: FileTypes.ProviderDeleteFileDTO): Promise<void> {
delete(
fileData:
| FileTypes.ProviderDeleteFileDTO
| FileTypes.ProviderDeleteFileDTO[]
): Promise<void> {
return this.fileProvider_.delete(fileData)
}

View File

@@ -66,21 +66,29 @@ export class LocalFileService extends AbstractFileProviderService {
}
}
async delete(file: FileTypes.ProviderDeleteFileDTO): Promise<void> {
const baseDir = file.fileKey.startsWith("private-")
? this.privateUploadDir_
: this.uploadDir_
async delete(
files: FileTypes.ProviderDeleteFileDTO | FileTypes.ProviderDeleteFileDTO[]
): Promise<void> {
files = Array.isArray(files) ? files : [files]
const filePath = this.getUploadFilePath(baseDir, file.fileKey)
try {
await fs.access(filePath, fs.constants.W_OK)
await fs.unlink(filePath)
} catch (e) {
// The file does not exist, we don't do anything
if (e.code !== "ENOENT") {
throw e
}
}
await Promise.all(
files.map(async (file) => {
const baseDir = file.fileKey.startsWith("private-")
? this.privateUploadDir_
: this.uploadDir_
const filePath = this.getUploadFilePath(baseDir, file.fileKey)
try {
await fs.access(filePath, fs.constants.W_OK)
await fs.unlink(filePath)
} catch (e) {
// The file does not exist, we don't do anything
if (e.code !== "ENOENT") {
throw e
}
}
})
)
return
}

View File

@@ -160,4 +160,31 @@ describe.skip("S3 File Plugin", () => {
await s3Service.delete({ fileKey: resp.key })
})
it("deletes multiple files in bulk", async () => {
const fileContent = await fs.readFile(fixtureImagePath)
const fixtureAsBinary = fileContent.toString("binary")
const cat = await s3Service.upload({
filename: "catphoto.jpg",
mimeType: "image/jpeg",
content: fixtureAsBinary,
})
const cat1 = await s3Service.upload({
filename: "catphoto-1.jpg",
mimeType: "image/jpeg",
content: fixtureAsBinary,
})
const cat2 = await s3Service.upload({
filename: "catphoto-2.jpg",
mimeType: "image/jpeg",
content: fixtureAsBinary,
})
await s3Service.delete([
{ fileKey: cat.key },
{ fileKey: cat1.key },
{ fileKey: cat2.key },
])
})
})

View File

@@ -1,5 +1,6 @@
import {
DeleteObjectCommand,
DeleteObjectsCommand,
GetObjectCommand,
ObjectCannedACL,
PutObjectCommand,
@@ -152,14 +153,33 @@ export class S3FileService extends AbstractFileProviderService {
}
}
async delete(file: FileTypes.ProviderDeleteFileDTO): Promise<void> {
const command = new DeleteObjectCommand({
Bucket: this.config_.bucket,
Key: file.fileKey,
})
async delete(
files: FileTypes.ProviderDeleteFileDTO | FileTypes.ProviderDeleteFileDTO[]
): Promise<void> {
try {
await this.client_.send(command)
/**
* Bulk delete files
*/
if (Array.isArray(files)) {
await this.client_.send(
new DeleteObjectsCommand({
Bucket: this.config_.bucket,
Delete: {
Objects: files.map((file) => ({
Key: file.fileKey,
})),
Quiet: true,
},
})
)
} else {
await this.client_.send(
new DeleteObjectCommand({
Bucket: this.config_.bucket,
Key: files.fileKey,
})
)
}
} catch (e) {
// TODO: Rethrow depending on the error (eg. a file not found error is fine, but a failed request should be rethrown)
this.logger_.error(e)