sam
661c3eab0f
change the random base 64 to a directory rather than part of the filename, so that users downloading their exports aren't greeted with a completely incomprehensible file in their downloads folder
132 lines
4.3 KiB
C#
132 lines
4.3 KiB
C#
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero 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
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
using System.Diagnostics;
|
|
using Foxnouns.Backend.Database;
|
|
using Foxnouns.Backend.Database.Models;
|
|
using Foxnouns.Backend.Extensions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NodaTime;
|
|
using NodaTime.Extensions;
|
|
|
|
namespace Foxnouns.Backend.Services;
|
|
|
|
public class DataCleanupService(
|
|
DatabaseContext db,
|
|
IClock clock,
|
|
ILogger logger,
|
|
ObjectStorageService objectStorageService
|
|
)
|
|
{
|
|
private readonly ILogger _logger = logger.ForContext<DataCleanupService>();
|
|
|
|
public async Task InvokeAsync(CancellationToken ct = default)
|
|
{
|
|
_logger.Debug("Cleaning up expired users");
|
|
await CleanUsersAsync(ct);
|
|
|
|
_logger.Debug("Cleaning up expired data exports");
|
|
await CleanExportsAsync(ct);
|
|
}
|
|
|
|
private async Task CleanUsersAsync(CancellationToken ct = default)
|
|
{
|
|
Instant selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter;
|
|
Instant suspendExpires = clock.GetCurrentInstant() - User.DeleteSuspendedAfter;
|
|
List<User> users = await db
|
|
.Users.Include(u => u.Members)
|
|
.Include(u => u.DataExports)
|
|
.Where(u =>
|
|
u.Deleted
|
|
&& (
|
|
(u.DeletedBy != null && u.DeletedAt < suspendExpires)
|
|
|| (u.DeletedBy == null && u.DeletedAt < selfDeleteExpires)
|
|
)
|
|
)
|
|
.OrderBy(u => u.Id)
|
|
.AsSplitQuery()
|
|
.ToListAsync(ct);
|
|
if (users.Count == 0)
|
|
return;
|
|
|
|
_logger.Debug(
|
|
"Deleting {Count} users that have been deleted for over 30 days or suspended for over 180 days",
|
|
users.Count
|
|
);
|
|
|
|
var sw = new Stopwatch();
|
|
|
|
await Task.WhenAll(users.Select(u => CleanUserAsync(u, ct)));
|
|
|
|
await db.SaveChangesAsync(ct);
|
|
_logger.Information(
|
|
"Deleted {Count} users, their members, and their exports in {Time}",
|
|
users.Count,
|
|
sw.ElapsedDuration()
|
|
);
|
|
}
|
|
|
|
private Task CleanUserAsync(User user, CancellationToken ct = default)
|
|
{
|
|
var tasks = new List<Task>();
|
|
|
|
if (user.Avatar != null)
|
|
tasks.Add(objectStorageService.DeleteUserAvatarAsync(user.Id, user.Avatar, ct));
|
|
|
|
tasks.AddRange(
|
|
user.Members.Select(member =>
|
|
objectStorageService.DeleteMemberAvatarAsync(member.Id, member.Avatar!, ct)
|
|
)
|
|
);
|
|
|
|
tasks.AddRange(
|
|
user.DataExports.Select(export =>
|
|
objectStorageService.RemoveObjectAsync(
|
|
ExportPath(export.UserId, export.Filename),
|
|
ct
|
|
)
|
|
)
|
|
);
|
|
|
|
db.Remove(user);
|
|
return Task.WhenAll(tasks);
|
|
}
|
|
|
|
private async Task CleanExportsAsync(CancellationToken ct = default)
|
|
{
|
|
var minExpiredId = Snowflake.FromInstant(clock.GetCurrentInstant() - DataExport.Expiration);
|
|
List<DataExport> exports = await db
|
|
.DataExports.Where(d => d.Id < minExpiredId)
|
|
.ToListAsync(ct);
|
|
if (exports.Count == 0)
|
|
return;
|
|
|
|
_logger.Debug("Deleting {Count} expired exports", exports.Count);
|
|
|
|
foreach (DataExport? export in exports)
|
|
{
|
|
_logger.Debug("Deleting export {ExportId}", export.Id);
|
|
await objectStorageService.RemoveObjectAsync(
|
|
ExportPath(export.UserId, export.Filename),
|
|
ct
|
|
);
|
|
db.Remove(export);
|
|
}
|
|
|
|
await db.SaveChangesAsync(ct);
|
|
}
|
|
|
|
private static string ExportPath(Snowflake userId, string b64) =>
|
|
$"data-exports/{userId}/{b64}/data-export.zip";
|
|
}
|