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<AuthMethod> AuthMethods { get; } = []; | ||||
|     public List<DataExport> DataExports { get; } = []; | ||||
|     public UserSettings Settings { get; set; } = new(); | ||||
| 
 | ||||
|     public required Instant LastActive { get; set; } | ||||
|  | @ -53,6 +54,12 @@ public class User : BaseModel | |||
|         [JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] | ||||
|         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 | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ public class CreateDataExportInvocable( | |||
|             .Users.Include(u => u.AuthMethods) | ||||
|             .Include(u => u.Flags) | ||||
|             .Include(u => u.ProfileFlags) | ||||
|             .AsSplitQuery() | ||||
|             .FirstOrDefaultAsync(u => u.Id == Payload.UserId); | ||||
|         if (user == null) | ||||
|         { | ||||
|  |  | |||
|  | @ -1,7 +1,10 @@ | |||
| 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; | ||||
| 
 | ||||
|  | @ -16,10 +19,76 @@ public class DataCleanupService( | |||
| 
 | ||||
|     public async Task InvokeAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         _logger.Information("Cleaning up expired users"); | ||||
|         await CleanUsersAsync(ct); | ||||
| 
 | ||||
|         _logger.Information("Cleaning up expired data exports"); | ||||
|         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) | ||||
|     { | ||||
|         var minExpiredId = Snowflake.FromInstant(clock.GetCurrentInstant() - DataExport.Expiration); | ||||
|  | @ -27,7 +96,7 @@ public class DataCleanupService( | |||
|         if (exports.Count == 0) | ||||
|             return; | ||||
| 
 | ||||
|         _logger.Debug("There are {Count} expired exports", exports.Count); | ||||
|         _logger.Debug("Deleting {Count} expired exports", exports.Count); | ||||
| 
 | ||||
|         foreach (var export in exports) | ||||
|         { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue