Files
MilkyShots/Lactose/Jobs/IntegrityCheckJob.cs

203 lines
8.8 KiB
C#

using Butter.Settings;
using Lactose.Context;
using Lactose.Models;
using Lactose.Utils;
namespace Lactose.Jobs;
/// <summary>
/// Performs a comprehensive integrity check of the system — database, assets, albums, people, thumbnails, and previews.
/// </summary>
public class IntegrityCheckJob (ILogger<IntegrityCheckJob> logger, IServiceProvider serviceProvider): Job{
const float totalSteps = 7;
/// <inheritdoc />
public override string Name => "Integrity Check Job";
void IncrementProgress(float f) => JobStatus.UpdateProgress(f/totalSteps, "Database connection successful.");
/// <inheritdoc />
protected override async Task TaskJob(CancellationToken token) {
try {
float progress = 0;
logger.LogInformation("Starting integrity check job...");
using var scope = serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<LactoseDbContext>();
//Step 1
// Check database connection
if (!dbContext.Database.CanConnect()) {
logger.LogError("Cannot connect to the database.");
JobStatus.Fail("Cannot connect to the database.");
return;
}
IncrementProgress(++progress);
token.ThrowIfCancellationRequested();
// Step 2
// Check for pending migrations
var pendingMigrations = dbContext.Database.GetPendingMigrations().ToList();
if (pendingMigrations.Any()) {
logger.LogWarning("There are pending migrations:");
foreach (var migration in pendingMigrations) logger.LogWarning($"- {migration}");
// Apply migrations
try {
dbContext.Database.Migrate();
logger.LogInformation("Applied pending migrations successfully.");
} catch (Exception ex) {
logger.LogError(ex, "Error applying migrations.");
JobStatus.Fail("Error applying migrations.");
return;
}
}
IncrementProgress(++progress);
token.ThrowIfCancellationRequested();
// A bit of context: Load() will load all entities from the table into the DbContext.
// Essentially it will ask the DB for all rows in the table and create entities for them in the DbContext.
// This will basically warm up the cache of the DbContext so that when we later access all the rows in the table,
// it won't lazy load them one by one, one call at the time.
// This is important because lazy loading one by one would be a huge performance hit,
// especially if there are many rows in the table.
// While for folders it might not be a big deal, this is just for consistency with all the other steps,
// especially the asset one.
// Step 3
// Check for folders existence
dbContext.Folders.Load();
foreach (Folder folder in dbContext.Folders.ToList()) {
token.ThrowIfCancellationRequested();
if (!Directory.Exists(folder.BasePath)) {
logger.LogWarning($"Folder {folder.Id} does not exist. Deactivating.");
folder.Active = false;
dbContext.Folders.Update(folder);
var affectedAssets = dbContext.Assets.Where(a => a.FolderId == folder.Id && a.DeletedAt == null)
.ToList();
foreach (var asset in affectedAssets) {
asset.DeletedAt = DateTime.UtcNow;
dbContext.Assets.Update(asset);
}
dbContext.SaveChanges();
logger.LogWarning("Soft-deleted {Count} assets associated with missing folder {FolderId}.", affectedAssets.Count, folder.Id);
}
}
IncrementProgress(++progress);
token.ThrowIfCancellationRequested();
// Step 4
// Check for assets existence
dbContext.Assets.Load();
foreach (Asset asset in dbContext.Assets.Where(a => a.DeletedAt == null)) {
token.ThrowIfCancellationRequested();
if (File.Exists(asset.OriginalPath)) continue;
asset.DeletedAt = DateTime.UtcNow;
dbContext.Assets.Update(asset);
logger.LogWarning($"Asset {asset.Id} does not exist. Soft-deleting.");
}
int changes = dbContext.SaveChanges();
logger.LogInformation("Soft-deleted {Changes} non-existent assets.", changes);
IncrementProgress(++progress);
token.ThrowIfCancellationRequested();
// Step 5
// Check for empty albums
dbContext.Albums.Load();
dbContext.Albums.Include(a => a.Assets)
.Where(a => a.Assets!.Count == 0)
.ToList().ForEach(a => {
logger.LogWarning($"Album {a.Id} is empty. Removing from database.");
dbContext.Albums.Remove(a);
});
changes = dbContext.SaveChanges();
logger.LogInformation("Removed {Changes} empty albums from the database.", changes);
IncrementProgress(++progress);
token.ThrowIfCancellationRequested();
// Step 6
// Check for orphaned thumbnails
dbContext.Settings.Load();
dbContext.Assets.Load();
var thumbnailPath = dbContext.Settings.First(s => s.Name == Settings.ThumbnailPath.AsString()).Value;
if (!Directory.Exists(thumbnailPath)) {
logger.LogWarning("Thumbnail path does not exist. Creating...");
try {
if (thumbnailPath != null)
Directory.CreateDirectory(thumbnailPath);
} catch (Exception e) {
logger.LogError(e, "Could not create thumbnail directory.");
}
logger.LogWarning("Marking all assets as missing thumbnails.");
dbContext.Assets.ForEach(a => a.ThumbnailPath = "");
changes = dbContext.SaveChanges();
logger.LogInformation("Marked {Changes} assets as missing thumbnails.", changes);
} else {
dbContext.Assets.Where(a => !String.IsNullOrEmpty(a.ThumbnailPath)).AsEnumerable()
.Where(a => !File.Exists(a.ThumbnailPath)).AsEnumerable()
.ForEach(a => {
logger.LogWarning($"Asset {a.Id} has a missing thumbnail. Marking as missing.");
a.ThumbnailPath = "";
dbContext.Assets.Update(a);
});
changes = dbContext.SaveChanges();
logger.LogInformation("Marked {Changes} assets as missing thumbnails.", changes);
}
IncrementProgress(++progress);
token.ThrowIfCancellationRequested();
// Step 7
// Check for orphaned previews
var previewPath = dbContext.Settings.First(s => s.Name == Settings.PreviewPath.AsString()).Value;
if (!Directory.Exists(previewPath)) {
logger.LogWarning("Preview path does not exist. Creating...");
try {
if (previewPath != null)
Directory.CreateDirectory(previewPath);
} catch (Exception e) {
logger.LogError(e, "Could not create preview directory.");
}
logger.LogWarning("Marking all assets as missing previews.");
dbContext.Assets.ForEach(a => a.PreviewPath = "");
changes = dbContext.SaveChanges();
logger.LogInformation("Marked {Changes} assets as missing previews.", changes);
} else {
dbContext.Assets.Where(a => !String.IsNullOrEmpty(a.PreviewPath)).AsEnumerable()
.Where(a => !File.Exists(a.PreviewPath)).AsEnumerable()
.ForEach(a => {
logger.LogWarning($"Asset {a.Id} has a missing preview. Marking as missing.");
a.PreviewPath = "";
dbContext.Assets.Update(a);
});
}
changes = dbContext.SaveChanges();
logger.LogInformation("Marked {Changes} assets as missing previews.", changes);
IncrementProgress(++progress);
// Complete
logger.LogInformation("Integrity check completed successfully.");
JobStatus.Complete("Integrity check completed successfully.");
} catch (OperationCanceledException) {
JobStatus.Cancel();
}
}
}