From 449867ab442a2f46196d571c8db99c224a5a3ab4 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:37:56 +0500 Subject: [PATCH] s3: add bulk delete api --- Notesnook.API/Controllers/S3Controller.cs | 24 +++++++- Notesnook.API/Interfaces/IS3Service.cs | 4 +- Notesnook.API/Models/DeleteBulkRequest.cs | 25 ++++++++ Notesnook.API/Services/S3Service.cs | 70 +++++++++++++++++++++-- 4 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 Notesnook.API/Models/DeleteBulkRequest.cs diff --git a/Notesnook.API/Controllers/S3Controller.cs b/Notesnook.API/Controllers/S3Controller.cs index 8292a04..a999640 100644 --- a/Notesnook.API/Controllers/S3Controller.cs +++ b/Notesnook.API/Controllers/S3Controller.cs @@ -21,20 +21,17 @@ using System; using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; -using Amazon.S3.Model; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using MongoDB.Driver; -using Notesnook.API.Accessors; using Notesnook.API.Helpers; using Notesnook.API.Interfaces; using Notesnook.API.Models; using Streetwriters.Common; using Streetwriters.Common.Accessors; using Streetwriters.Common.Extensions; -using Streetwriters.Common.Interfaces; using Streetwriters.Common.Models; namespace Notesnook.API.Controllers @@ -212,5 +209,26 @@ namespace Notesnook.API.Controllers return BadRequest(new { error = "Failed to delete attachment." }); } } + + [HttpPost("bulk-delete")] + public async Task DeleteBulkAsync([FromBody] DeleteBulkObjectsRequest request) + { + try + { + if (request.Names == null || request.Names.Length == 0) + { + return BadRequest(new { error = "No files specified for deletion." }); + } + + var userId = this.User.GetUserId(); + await s3Service.DeleteObjectsAsync(userId, request.Names); + return Ok(); + } + catch (Exception ex) + { + logger.LogError(ex, "Error deleting objects for user."); + return BadRequest(new { error = "Failed to delete attachments." }); + } + } } } diff --git a/Notesnook.API/Interfaces/IS3Service.cs b/Notesnook.API/Interfaces/IS3Service.cs index 5d5f2bb..4327230 100644 --- a/Notesnook.API/Interfaces/IS3Service.cs +++ b/Notesnook.API/Interfaces/IS3Service.cs @@ -17,18 +17,16 @@ You should have received a copy of the Affero GNU General Public License along with this program. If not, see . */ -using System.Threading; using System.Threading.Tasks; using Amazon.S3.Model; using Notesnook.API.Models; -using Notesnook.API.Models.Responses; -using Streetwriters.Common.Interfaces; namespace Notesnook.API.Interfaces { public interface IS3Service { Task DeleteObjectAsync(string userId, string name); + Task DeleteObjectsAsync(string userId, string[] names); Task DeleteDirectoryAsync(string userId); Task GetObjectSizeAsync(string userId, string name); Task GetUploadObjectUrlAsync(string userId, string name); diff --git a/Notesnook.API/Models/DeleteBulkRequest.cs b/Notesnook.API/Models/DeleteBulkRequest.cs new file mode 100644 index 0000000..013cbd0 --- /dev/null +++ b/Notesnook.API/Models/DeleteBulkRequest.cs @@ -0,0 +1,25 @@ +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +namespace Notesnook.API.Models; + +public class DeleteBulkObjectsRequest +{ + public required string[] Names { get; set; } +} diff --git a/Notesnook.API/Services/S3Service.cs b/Notesnook.API/Services/S3Service.cs index 240c951..b541942 100644 --- a/Notesnook.API/Services/S3Service.cs +++ b/Notesnook.API/Services/S3Service.cs @@ -24,21 +24,15 @@ using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using Amazon; -using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using MongoDB.Driver; -using Notesnook.API.Accessors; using Notesnook.API.Helpers; using Notesnook.API.Interfaces; using Notesnook.API.Models; using Streetwriters.Common; using Streetwriters.Common.Accessors; -using Streetwriters.Common.Enums; -using Streetwriters.Common.Interfaces; -using Streetwriters.Common.Models; namespace Notesnook.API.Services { @@ -110,6 +104,70 @@ namespace Notesnook.API.Services throw new Exception("Could not delete object."); } + public async Task DeleteObjectsAsync(string userId, string[] names) + { + var objectsToDelete = new List(); + + foreach (var name in names) + { + var objectName = GetFullObjectName(userId, name); + if (objectName == null) continue; + + objectsToDelete.Add(new KeyVersion { Key = objectName }); + } + + if (objectsToDelete.Count == 0) + { + return; + } + + // S3 DeleteObjectsRequest supports max 1000 keys per request + var batchSize = 1000; + var deleteErrors = new List(); + var failedBatches = 0; + + for (int i = 0; i < objectsToDelete.Count; i += batchSize) + { + var batch = objectsToDelete.Skip(i).Take(batchSize).ToList(); + var deleteObjectsResponse = await S3InternalClient.ExecuteWithFailoverAsync( + (client) => client.DeleteObjectsAsync(new DeleteObjectsRequest + { + BucketName = INTERNAL_BUCKET_NAME, + Objects = batch, + }), + operationName: "DeleteObjects", + isWriteOperation: true + ); + + if (!IsSuccessStatusCode((int)deleteObjectsResponse.HttpStatusCode)) + { + failedBatches++; + } + + if (deleteObjectsResponse.DeleteErrors.Count > 0) + { + deleteErrors.AddRange(deleteObjectsResponse.DeleteErrors); + } + } + + if (failedBatches > 0 || deleteErrors.Count > 0) + { + var errorParts = new List(); + + if (failedBatches > 0) + { + errorParts.Add($"{failedBatches} batch(es) failed with unsuccessful status code"); + } + + if (deleteErrors.Count > 0) + { + errorParts.Add(string.Join(", ", deleteErrors.Select(e => $"{e.Key}: {e.Message}"))); + } + + throw new Exception(string.Join("; ", errorParts)); + } + } + public async Task DeleteDirectoryAsync(string userId) { var request = new ListObjectsV2Request