203 lines
8.8 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|