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.