s3: add bulk delete api

This commit is contained in:
01zulfi
2026-02-04 15:37:56 +05:00
parent 014c4e3b32
commit 449867ab44
4 changed files with 111 additions and 12 deletions

View File

@@ -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<IActionResult> 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." });
}
}
}
}

View File

@@ -17,18 +17,16 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<long> GetObjectSizeAsync(string userId, string name);
Task<string?> GetUploadObjectUrlAsync(string userId, string name);

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
namespace Notesnook.API.Models;
public class DeleteBulkObjectsRequest
{
public required string[] Names { get; set; }
}

View File

@@ -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<KeyVersion>();
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<DeleteError>();
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<string>();
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