| 
									
										
										
										
											2024-12-03 14:55:19 +01:00
										 |  |  | using System.Diagnostics; | 
					
						
							| 
									
										
										
										
											2024-12-02 18:06:19 +01:00
										 |  |  | using Foxnouns.Backend.Database; | 
					
						
							|  |  |  | using Foxnouns.Backend.Database.Models; | 
					
						
							| 
									
										
										
										
											2024-12-03 14:55:19 +01:00
										 |  |  | using Foxnouns.Backend.Extensions; | 
					
						
							| 
									
										
										
										
											2024-12-02 18:06:19 +01:00
										 |  |  | using Microsoft.EntityFrameworkCore; | 
					
						
							|  |  |  | using NodaTime; | 
					
						
							| 
									
										
										
										
											2024-12-03 14:55:19 +01:00
										 |  |  | using NodaTime.Extensions; | 
					
						
							| 
									
										
										
										
											2024-12-02 18:06:19 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-12-03 14:55:19 +01:00
										 |  |  |         _logger.Information("Cleaning up expired users"); | 
					
						
							|  |  |  |         await CleanUsersAsync(ct); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-02 18:06:19 +01:00
										 |  |  |         _logger.Information("Cleaning up expired data exports"); | 
					
						
							|  |  |  |         await CleanExportsAsync(ct); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-03 14:55:19 +01:00
										 |  |  |     private async Task CleanUsersAsync(CancellationToken ct = default) | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-12-08 15:07:25 +01:00
										 |  |  |         Instant selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter; | 
					
						
							|  |  |  |         Instant suspendExpires = clock.GetCurrentInstant() - User.DeleteSuspendedAfter; | 
					
						
							|  |  |  |         List<User> users = await db | 
					
						
							| 
									
										
										
										
											2024-12-03 14:55:19 +01:00
										 |  |  |             .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); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-02 18:06:19 +01:00
										 |  |  |     private async Task CleanExportsAsync(CancellationToken ct = default) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         var minExpiredId = Snowflake.FromInstant(clock.GetCurrentInstant() - DataExport.Expiration); | 
					
						
							| 
									
										
										
										
											2024-12-08 15:07:25 +01:00
										 |  |  |         List<DataExport> exports = await db | 
					
						
							|  |  |  |             .DataExports.Where(d => d.Id < minExpiredId) | 
					
						
							|  |  |  |             .ToListAsync(ct); | 
					
						
							| 
									
										
										
										
											2024-12-02 18:06:19 +01:00
										 |  |  |         if (exports.Count == 0) | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-03 14:55:19 +01:00
										 |  |  |         _logger.Debug("Deleting {Count} expired exports", exports.Count); | 
					
						
							| 
									
										
										
										
											2024-12-02 18:06:19 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-08 15:07:25 +01:00
										 |  |  |         foreach (DataExport? export in exports) | 
					
						
							| 
									
										
										
										
											2024-12-02 18:06:19 +01:00
										 |  |  |         { | 
					
						
							|  |  |  |             _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}.zip"; | 
					
						
							|  |  |  | } |