From 1344199807a29211e0a9da34af077ceb0a09b22b Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Tue, 19 Aug 2025 12:24:43 +0500 Subject: [PATCH] api: add job to cleanup stale devices every 30 days --- Notesnook.API/Jobs/DeviceCleanupJob.cs | 64 ++++++++++++++++++++++++++ Notesnook.API/Notesnook.API.csproj | 2 + Notesnook.API/Startup.cs | 19 ++++++++ 3 files changed, 85 insertions(+) create mode 100644 Notesnook.API/Jobs/DeviceCleanupJob.cs diff --git a/Notesnook.API/Jobs/DeviceCleanupJob.cs b/Notesnook.API/Jobs/DeviceCleanupJob.cs new file mode 100644 index 0000000..1c3ddeb --- /dev/null +++ b/Notesnook.API/Jobs/DeviceCleanupJob.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Quartz; + +namespace Notesnook.API.Jobs +{ + public class DeviceCleanupJob : IJob + { + public async Task Execute(IJobExecutionContext context) + { + ParallelOptions parallelOptions = new() + { + MaxDegreeOfParallelism = 100, + CancellationToken = context.CancellationToken, + }; + Parallel.ForEach(Directory.EnumerateDirectories("sync"), parallelOptions, (userDir, ct) => + { + foreach (var device in Directory.EnumerateDirectories(userDir)) + { + string lastAccessFile = Path.Combine(device, "LastAccessTime"); + + try + { + if (!File.Exists(lastAccessFile)) + { + Directory.Delete(device, true); + continue; + } + + string content = File.ReadAllText(lastAccessFile); + if (!long.TryParse(content, out long lastAccessTime) || lastAccessTime <= 0) + { + Directory.Delete(device, true); + continue; + } + + DateTimeOffset accessTime; + try + { + accessTime = DateTimeOffset.FromUnixTimeMilliseconds(lastAccessTime); + } + catch (Exception) + { + Directory.Delete(device, true); + continue; + } + + // If the device hasn't been accessed for more than one month, delete it. + if (accessTime.AddMonths(1) < DateTimeOffset.UtcNow) + { + Directory.Delete(device, true); + } + } + catch (Exception ex) + { + // Log the error and continue processing other directories. + Console.Error.WriteLine($"Error processing device '{device}': {ex.Message}"); + } + } + }); + } + } +} \ No newline at end of file diff --git a/Notesnook.API/Notesnook.API.csproj b/Notesnook.API/Notesnook.API.csproj index 1888779..859cb00 100644 --- a/Notesnook.API/Notesnook.API.csproj +++ b/Notesnook.API/Notesnook.API.csproj @@ -17,6 +17,8 @@ + + diff --git a/Notesnook.API/Startup.cs b/Notesnook.API/Startup.cs index 048b9ee..b923ce4 100644 --- a/Notesnook.API/Startup.cs +++ b/Notesnook.API/Startup.cs @@ -48,11 +48,13 @@ using Notesnook.API.Authorization; using Notesnook.API.Extensions; using Notesnook.API.Hubs; using Notesnook.API.Interfaces; +using Notesnook.API.Jobs; using Notesnook.API.Models; using Notesnook.API.Repositories; using Notesnook.API.Services; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; +using Quartz; using Streetwriters.Common; using Streetwriters.Common.Extensions; using Streetwriters.Common.Messages; @@ -216,6 +218,23 @@ namespace Notesnook.API .WithMetrics((builder) => builder .AddMeter("Notesnook.API.Metrics.Sync") .AddPrometheusExporter()); + + services.AddQuartzHostedService(q => + { + q.WaitForJobsToComplete = false; + q.AwaitApplicationStarted = true; + q.StartDelay = TimeSpan.FromMinutes(1); + }).AddQuartz(q => + { + q.UseMicrosoftDependencyInjectionJobFactory(); + + var jobKey = new JobKey("DeviceCleanupJob"); + q.AddJob(opts => opts.WithIdentity(jobKey)); + q.AddTrigger(opts => opts + .ForJob(jobKey) + .WithIdentity("DeviceCleanup-trigger") + .WithSimpleSchedule((s) => s.RepeatForever().WithInterval(TimeSpan.FromDays(30)))); + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.