Files
jobtrackingapp/JobTrackerApi/Services/DailyExportHostedService.cs
T

142 lines
5.6 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using JobTrackerApi.Data;
namespace JobTrackerApi.Services
{
public sealed class DailyExportHostedService : BackgroundService
{
private readonly IServiceProvider _sp;
private readonly ILogger<DailyExportHostedService> _logger;
private readonly IConfiguration _cfg;
private readonly AppPaths _paths;
private readonly IStartupReadiness _startupReadiness;
public DailyExportHostedService(
IServiceProvider sp,
ILogger<DailyExportHostedService> logger,
IConfiguration cfg,
AppPaths paths,
IStartupReadiness startupReadiness)
{
_sp = sp;
_logger = logger;
_cfg = cfg;
_paths = paths;
_startupReadiness = startupReadiness;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var enabled = _cfg.GetValue("Exports:DailyEnabled", true);
await _startupReadiness.WaitUntilReadyAsync(stoppingToken);
if (!enabled)
{
_logger.LogInformation("Daily export disabled (Exports:DailyEnabled=false).");
return;
}
var hour = _cfg.GetValue("Exports:DailyHourLocal", 2);
if (hour < 0 || hour > 23) hour = 2;
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
var next = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0);
if (next <= now) next = next.AddDays(1);
var delay = next - now;
_logger.LogInformation("Next daily export scheduled at {Next}.", next);
try
{
await Task.Delay(delay, stoppingToken);
}
catch (TaskCanceledException)
{
break;
}
try
{
await RunExport(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Daily export failed.");
}
}
}
private async Task RunExport(CancellationToken ct)
{
var folder = _paths.GetExportsRoot(_cfg["Exports:DailyFolder"]);
Directory.CreateDirectory(folder);
using var scope = _sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
var companies = await db.Companies.AsNoTracking().OrderBy(c => c.Name).ToListAsync(ct);
var jobs = await db.JobApplications.AsNoTracking().OrderByDescending(j => j.DateApplied).ToListAsync(ct);
var correspondence = await db.Correspondences.AsNoTracking().OrderBy(c => c.Date).ToListAsync(ct);
var attachments = await db.Attachments.AsNoTracking().OrderBy(a => a.UploadDate).ToListAsync(ct);
var events = await db.JobEvents.AsNoTracking().OrderBy(e => e.At).ToListAsync(ct);
var rules = await db.RuleSettings.AsNoTracking().FirstOrDefaultAsync(ct);
// If multi-user ownership is present, write one export per owner.
var owners = jobs
.Select(j => j.OwnerUserId)
.Distinct()
.ToList();
if (owners.Count <= 1)
{
var export = new
{
Version = "dailyexport.v1",
CreatedAt = DateTime.Now,
Companies = companies,
JobApplications = jobs,
Correspondence = correspondence,
Attachments = attachments,
Events = events,
Rules = rules
};
var json = JsonSerializer.Serialize(export, new JsonSerializerOptions { WriteIndented = true });
var file = Path.Combine(folder, $"daily_export_{DateTime.Now:yyyyMMdd}.json");
await File.WriteAllTextAsync(file, json, ct);
_logger.LogInformation("Daily export written: {File}.", file);
return;
}
foreach (var owner in owners)
{
var ownerKey = string.IsNullOrWhiteSpace(owner) ? "_unassigned" : owner;
var ownerJobs = jobs.Where(j => j.OwnerUserId == owner).ToList();
var ownerJobIds = ownerJobs.Select(j => j.Id).ToHashSet();
var export = new
{
Version = "dailyexport.v2",
CreatedAt = DateTime.Now,
OwnerUserId = owner,
Companies = companies.Where(c => c.OwnerUserId == owner).ToList(),
JobApplications = ownerJobs,
Correspondence = correspondence.Where(c => ownerJobIds.Contains(c.JobApplicationId)).ToList(),
Attachments = attachments.Where(a => ownerJobIds.Contains(a.JobApplicationId)).ToList(),
Events = events.Where(e => ownerJobIds.Contains(e.JobApplicationId)).ToList(),
Rules = rules
};
var json = JsonSerializer.Serialize(export, new JsonSerializerOptions { WriteIndented = true });
var file = Path.Combine(folder, $"daily_export_{ownerKey}_{DateTime.Now:yyyyMMdd}.json");
await File.WriteAllTextAsync(file, json, ct);
_logger.LogInformation("Daily export written: {File}.", file);
}
}
}
}