mirror of
https://github.com/streetwriters/notesnook-sync-server.git
synced 2026-02-12 11:12:44 +00:00
s3: add bulk delete api
This commit is contained in:
@@ -21,20 +21,17 @@ using System;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Amazon.S3.Model;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Notesnook.API.Accessors;
|
|
||||||
using Notesnook.API.Helpers;
|
using Notesnook.API.Helpers;
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Streetwriters.Common;
|
using Streetwriters.Common;
|
||||||
using Streetwriters.Common.Accessors;
|
using Streetwriters.Common.Accessors;
|
||||||
using Streetwriters.Common.Extensions;
|
using Streetwriters.Common.Extensions;
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
using Streetwriters.Common.Models;
|
using Streetwriters.Common.Models;
|
||||||
|
|
||||||
namespace Notesnook.API.Controllers
|
namespace Notesnook.API.Controllers
|
||||||
@@ -212,5 +209,26 @@ namespace Notesnook.API.Controllers
|
|||||||
return BadRequest(new { error = "Failed to delete attachment." });
|
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." });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Amazon.S3.Model;
|
using Amazon.S3.Model;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Notesnook.API.Models.Responses;
|
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Interfaces
|
namespace Notesnook.API.Interfaces
|
||||||
{
|
{
|
||||||
public interface IS3Service
|
public interface IS3Service
|
||||||
{
|
{
|
||||||
Task DeleteObjectAsync(string userId, string name);
|
Task DeleteObjectAsync(string userId, string name);
|
||||||
|
Task DeleteObjectsAsync(string userId, string[] names);
|
||||||
Task DeleteDirectoryAsync(string userId);
|
Task DeleteDirectoryAsync(string userId);
|
||||||
Task<long> GetObjectSizeAsync(string userId, string name);
|
Task<long> GetObjectSizeAsync(string userId, string name);
|
||||||
Task<string?> GetUploadObjectUrlAsync(string userId, string name);
|
Task<string?> GetUploadObjectUrlAsync(string userId, string name);
|
||||||
|
|||||||
25
Notesnook.API/Models/DeleteBulkRequest.cs
Normal file
25
Notesnook.API/Models/DeleteBulkRequest.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -24,21 +24,15 @@ using System.Net.Http;
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Amazon;
|
using Amazon;
|
||||||
using Amazon.Runtime;
|
|
||||||
using Amazon.S3;
|
using Amazon.S3;
|
||||||
using Amazon.S3.Model;
|
using Amazon.S3.Model;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Notesnook.API.Accessors;
|
|
||||||
using Notesnook.API.Helpers;
|
using Notesnook.API.Helpers;
|
||||||
using Notesnook.API.Interfaces;
|
using Notesnook.API.Interfaces;
|
||||||
using Notesnook.API.Models;
|
using Notesnook.API.Models;
|
||||||
using Streetwriters.Common;
|
using Streetwriters.Common;
|
||||||
using Streetwriters.Common.Accessors;
|
using Streetwriters.Common.Accessors;
|
||||||
using Streetwriters.Common.Enums;
|
|
||||||
using Streetwriters.Common.Interfaces;
|
|
||||||
using Streetwriters.Common.Models;
|
|
||||||
|
|
||||||
namespace Notesnook.API.Services
|
namespace Notesnook.API.Services
|
||||||
{
|
{
|
||||||
@@ -110,6 +104,70 @@ namespace Notesnook.API.Services
|
|||||||
throw new Exception("Could not delete object.");
|
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)
|
public async Task DeleteDirectoryAsync(string userId)
|
||||||
{
|
{
|
||||||
var request = new ListObjectsV2Request
|
var request = new ListObjectsV2Request
|
||||||
|
|||||||
Reference in New Issue
Block a user