feat(backend): clean deleted users
This commit is contained in:
parent
903be2709c
commit
18bdbc0745
3 changed files with 78 additions and 1 deletions
|
@ -30,6 +30,7 @@ public class User : BaseModel
|
||||||
|
|
||||||
public List<Member> Members { get; } = [];
|
public List<Member> Members { get; } = [];
|
||||||
public List<AuthMethod> AuthMethods { get; } = [];
|
public List<AuthMethod> AuthMethods { get; } = [];
|
||||||
|
public List<DataExport> DataExports { get; } = [];
|
||||||
public UserSettings Settings { get; set; } = new();
|
public UserSettings Settings { get; set; } = new();
|
||||||
|
|
||||||
public required Instant LastActive { get; set; }
|
public required Instant LastActive { get; set; }
|
||||||
|
@ -53,6 +54,12 @@ public class User : BaseModel
|
||||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||||
public PreferenceSize Size { get; set; }
|
public PreferenceSize Size { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public static readonly Duration DeleteAfter = Duration.FromDays(30);
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public static readonly Duration DeleteSuspendedAfter = Duration.FromDays(180);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum UserRole
|
public enum UserRole
|
||||||
|
|
|
@ -44,6 +44,7 @@ public class CreateDataExportInvocable(
|
||||||
.Users.Include(u => u.AuthMethods)
|
.Users.Include(u => u.AuthMethods)
|
||||||
.Include(u => u.Flags)
|
.Include(u => u.Flags)
|
||||||
.Include(u => u.ProfileFlags)
|
.Include(u => u.ProfileFlags)
|
||||||
|
.AsSplitQuery()
|
||||||
.FirstOrDefaultAsync(u => u.Id == Payload.UserId);
|
.FirstOrDefaultAsync(u => u.Id == Payload.UserId);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
using System.Diagnostics;
|
||||||
using Foxnouns.Backend.Database;
|
using Foxnouns.Backend.Database;
|
||||||
using Foxnouns.Backend.Database.Models;
|
using Foxnouns.Backend.Database.Models;
|
||||||
|
using Foxnouns.Backend.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using NodaTime.Extensions;
|
||||||
|
|
||||||
namespace Foxnouns.Backend.Services;
|
namespace Foxnouns.Backend.Services;
|
||||||
|
|
||||||
|
@ -16,10 +19,76 @@ public class DataCleanupService(
|
||||||
|
|
||||||
public async Task InvokeAsync(CancellationToken ct = default)
|
public async Task InvokeAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
_logger.Information("Cleaning up expired users");
|
||||||
|
await CleanUsersAsync(ct);
|
||||||
|
|
||||||
_logger.Information("Cleaning up expired data exports");
|
_logger.Information("Cleaning up expired data exports");
|
||||||
await CleanExportsAsync(ct);
|
await CleanExportsAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CleanUsersAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter;
|
||||||
|
var suspendExpires = clock.GetCurrentInstant() - User.DeleteSuspendedAfter;
|
||||||
|
var 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)
|
private async Task CleanExportsAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var minExpiredId = Snowflake.FromInstant(clock.GetCurrentInstant() - DataExport.Expiration);
|
var minExpiredId = Snowflake.FromInstant(clock.GetCurrentInstant() - DataExport.Expiration);
|
||||||
|
@ -27,7 +96,7 @@ public class DataCleanupService(
|
||||||
if (exports.Count == 0)
|
if (exports.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.Debug("There are {Count} expired exports", exports.Count);
|
_logger.Debug("Deleting {Count} expired exports", exports.Count);
|
||||||
|
|
||||||
foreach (var export in exports)
|
foreach (var export in exports)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue