NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Saturday, 14 September 2024
//Less than a minute
Ich habe viele Blogs mal darüber, wie ich Markdown verwenden, um meine Blog-Posts zu erstellen; Ich mag diesen Ansatz, aber es hat einen großen Nachteil - es bedeutet, dass ich einen kompletten Docker bauen Zyklus zu tun, um einen Beitrag zu aktualisieren. Das war FINE, während ich mehr Funktionen erstellte, über die ich dann bloggte, aber es ist jetzt ziemlich begrenzt. Ich möchte in der Lage sein, meine Blog-Posts zu aktualisieren, ohne einen kompletten Bauzyklus zu machen. Jetzt habe ich meinem Blog eine neue Funktionalität hinzugefügt, die es mir erlaubt, genau das zu tun.
Mit 'hybrid' Ansatz meine ich diesen Lebenszyklus; wieder ziemlich simplistisch, aber irgendwie cool (auf eine super geeky Weise!)== Einzelnachweise ==
Es ist also ziemlich einfach.
Dies ermöglicht es mir, weiterhin mit Rider vor Ort Blog-Posts zu erstellen (in Zukunft werde ich wahrscheinlich zu bewegen, um auch dies in der Website selbst geschehen), alle Übersetzungen dynamisch auf der Website selbst passieren und ich kann Beiträge aktualisieren, ohne einen vollständigen Aufbau Zyklus zu tun.
Der Code dafür ist YET ANOTHER IHostedService
, dieses Mal nutzt es die FileSystemWatcher
Klasse, um ein Verzeichnis für neue Dateien zu sehen. Wenn eine neue Datei erkannt wird, liest sie die Datei, verarbeitet sie und fügt sie in die Datenbank ein. ODER wenn ich einen englischen Post lösche, wird er auch alle Übersetzungen dieses Posts löschen.
Der ganze Code ist unten, aber ich breche ihn hier ein bisschen auf.
using Mostlylucid.Config.Markdown;
using Polly;
using Serilog.Events;
namespace Mostlylucid.Blog.WatcherService;
public class MarkdownDirectoryWatcherService(MarkdownConfig markdownConfig, IServiceScopeFactory serviceScopeFactory,
ILogger<MarkdownDirectoryWatcherService> logger)
: IHostedService
{
private Task _awaitChangeTask = Task.CompletedTask;
private FileSystemWatcher _fileSystemWatcher;
public Task StartAsync(CancellationToken cancellationToken)
{
_fileSystemWatcher = new FileSystemWatcher
{
Path = markdownConfig.MarkdownPath,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime |
NotifyFilters.Size,
Filter = "*.md", // Watch all markdown files
IncludeSubdirectories = true // Enable watching subdirectories
};
// Subscribe to events
_fileSystemWatcher.EnableRaisingEvents = true;
_awaitChangeTask = Task.Run(()=> AwaitChanges(cancellationToken));
logger.LogInformation("Started watching directory {Directory}", markdownConfig.MarkdownPath);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
// Stop watching
_fileSystemWatcher.EnableRaisingEvents = false;
_fileSystemWatcher.Dispose();
Console.WriteLine($"Stopped watching directory: {markdownConfig.MarkdownPath}");
return Task.CompletedTask;
}
private async Task AwaitChanges(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var fileEvent = _fileSystemWatcher.WaitForChanged(WatcherChangeTypes.All);
if (fileEvent.ChangeType == WatcherChangeTypes.Changed ||
fileEvent.ChangeType == WatcherChangeTypes.Created)
{
await OnChangedAsync(fileEvent);
}
else if (fileEvent.ChangeType == WatcherChangeTypes.Deleted)
{
OnDeleted(fileEvent);
}
else if (fileEvent.ChangeType == WatcherChangeTypes.Renamed)
{
}
}
}
private async Task OnChangedAsync(WaitForChangedResult e)
{
if (e.Name == null) return;
var activity = Log.Logger.StartActivity("Markdown File Changed {Name}", e.Name);
var retryPolicy = Policy
.Handle<IOException>() // Only handle IO exceptions (like file in use)
.WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromMilliseconds(500 * retryAttempt),
(exception, timeSpan, retryCount, context) =>
{
activity?.Activity?.SetTag("Retry Attempt", retryCount);
// Log the retry attempt
logger.LogWarning("File is in use, retrying attempt {RetryCount} after {TimeSpan}", retryCount, timeSpan);
});
try
{
var fileName = e.Name;
var isTranslated = Path.GetFileNameWithoutExtension(e.Name).Contains(".");
var language = MarkdownBaseService.EnglishLanguage;
var directory = markdownConfig.MarkdownPath;
if (isTranslated)
{
language = Path.GetFileNameWithoutExtension(e.Name).Split('.').Last();
fileName = Path.GetFileName(fileName);
directory = markdownConfig.MarkdownTranslatedPath;
}
var filePath = Path.Combine(directory, fileName);
var scope = serviceScopeFactory.CreateScope();
var markdownBlogService = scope.ServiceProvider.GetRequiredService<IMarkdownBlogService>();
// Use the Polly retry policy for executing the operation
await retryPolicy.ExecuteAsync(async () =>
{
var blogModel = await markdownBlogService.GetPage(filePath);
activity?.Activity?.SetTag("Page Processed", blogModel.Slug);
blogModel.Language = language;
var blogService = scope.ServiceProvider.GetRequiredService<IBlogService>();
await blogService.SavePost(blogModel);
if (language == MarkdownBaseService.EnglishLanguage)
{
var translateService = scope.ServiceProvider.GetRequiredService<BackgroundTranslateService>();
await translateService.TranslateForAllLanguages(
new PageTranslationModel(){OriginalFileName = filePath, OriginalMarkdown = blogModel.Markdown,Persist = true});
}
activity?.Activity?.SetTag("Page Saved", blogModel.Slug);
});
activity?.Complete();
}
catch (Exception exception)
{
activity?.Complete(LogEventLevel.Error, exception);
}
}
private void OnDeleted(WaitForChangedResult e)
{
if(e.Name == null) return;
var isTranslated = Path.GetFileNameWithoutExtension(e.Name).Contains(".");
var language = MarkdownBaseService.EnglishLanguage;
var slug = Path.GetFileNameWithoutExtension(e.Name);
if (isTranslated)
{
var name = Path.GetFileNameWithoutExtension(e.Name).Split('.');
language = name.Last();
slug = name.First();
}
else
{
var translatedFiles = Directory.GetFiles(markdownConfig.MarkdownTranslatedPath, $"{slug}.*.*");
_fileSystemWatcher.EnableRaisingEvents = false;
foreach (var file in translatedFiles)
{
File.Delete(file);
}
_fileSystemWatcher.EnableRaisingEvents = true;
}
var scope = serviceScopeFactory.CreateScope();
var blogService = scope.ServiceProvider.GetRequiredService<IBlogService>();
blogService.Delete(slug, language);
}
private void OnRenamed(object sender, RenamedEventArgs e)
{
Console.WriteLine($"File renamed: {e.OldFullPath} to {e.FullPath}");
}
}
Alles, was es tut, ist, eine neue Aufgabe zu starten, die FileSystemWatcher
in der es ist StartAsync
verfahren. In einem IHostedService
Dies ist der Einstiegspunkt, mit dem das ASP.NET Core Framework den Dienst startet.
private FileSystemWatcher _fileSystemWatcher;
public Task StartAsync(CancellationToken cancellationToken)
{
_fileSystemWatcher = new FileSystemWatcher
{
Path = markdownConfig.MarkdownPath,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime |
NotifyFilters.Size,
Filter = "*.md", // Watch all markdown files
IncludeSubdirectories = true // Enable watching subdirectories
};
// Subscribe to events
_fileSystemWatcher.EnableRaisingEvents = true;
_awaitChangeTask = Task.Run(()=> AwaitChanges(cancellationToken));
logger.LogInformation("Started watching directory {Directory}", markdownConfig.MarkdownPath);
return Task.CompletedTask;
}
Ich habe eine Aufgabe vor Ort, um den Dienst, die ich feuern die Änderung Schleife auf
private Task _awaitChangeTask = Task.CompletedTask;
_awaitChangeTask = Task.Run(()=> AwaitChanges(cancellationToken));
Dies ist wichtig, da sonst dies blockiert wird, müssen Sie sicherstellen, dass dies im Hintergrund läuft (Fragen Sie mich, wie ich dies weiß).
Hier haben wir eine FileSystemWatcher
um Ereignisse in meinem Markdown-Verzeichnis zu hören (aus Gründen, die wir später sehen werden, in ein Host-Verzeichnis aufgenommen!)
_fileSystemWatcher = new FileSystemWatcher
{
Path = markdownConfig.MarkdownPath,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime |
NotifyFilters.Size,
Filter = "*.md", // Watch all markdown files
IncludeSubdirectories = true // Enable watching subdirectories
};
// Subscribe to events
_fileSystemWatcher.EnableRaisingEvents = true;
Du kannst sehen, dass ich aufpasse. FileName
, LastWrite
, CreationTime
und Size
Veränderungen. Dies liegt daran, dass ich wissen möchte, ob eine Datei erstellt, aktualisiert oder gelöscht wird.
Ich habe auch eine Filter
eingestellt auf *.md
also schaue ich nur auf Markdown-Dateien und spezifizieren möchte ich auch Unterverzeichnisse sehen (für die Übersetzungen).
In diesem Code habe ich eine Änderungsschleife, die auf Änderungen am Dateisystem wartet. Beachten Sie, Sie können auch einfach in die Change-Ereignisse hier haken, aber das fühlte sich für mich sauberer an.
private async Task AwaitChanges(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var fileEvent = _fileSystemWatcher.WaitForChanged(WatcherChangeTypes.All);
if (fileEvent.ChangeType == WatcherChangeTypes.Changed ||
fileEvent.ChangeType == WatcherChangeTypes.Created)
{
await OnChangedAsync(fileEvent);
}
else if (fileEvent.ChangeType == WatcherChangeTypes.Deleted)
{
OnDeleted(fileEvent);
}
else if (fileEvent.ChangeType == WatcherChangeTypes.Renamed)
{
}
}
}
Wieder ziemlich einfach, alles, was es tut, ist auf ein Wechselereignis warten und dann ruft die OnChangedAsync
verfahren. ODER wenn die Datei gelöscht wird, ruft sie die OnDeleted
verfahren.
Hier passiert das 'Magische'. Was es tut, ist das Wechselereignis zu hören. Es verarbeitet dann die Markdown-Datei mit meiner Pipeline (um die HTML-Kategorien, Datum, Titel usw. zu erhalten) und indert dies in die Datenbank ein. Es erkennt, ob die Datei in Englisch ist (so schrieb ich sie :)) und wenn es ist, startet es einen Übersetzungsprozess. Siehe Dieser Beitrag für die Art und Weise, wie das funktioniert.
await retryPolicy.ExecuteAsync(async () =>
{
var blogModel = await markdownBlogService.GetPage(filePath);
activity?.Activity?.SetTag("Page Processed", blogModel.Slug);
blogModel.Language = language;
var blogService = scope.ServiceProvider.GetRequiredService<IBlogService>();
await blogService.SavePost(blogModel);
if (language == MarkdownBaseService.EnglishLanguage)
{
var translateService = scope.ServiceProvider.GetRequiredService<BackgroundTranslateService>();
await translateService.TranslateForAllLanguages(
new PageTranslationModel(){OriginalFileName = filePath, OriginalMarkdown = blogModel.Markdown,Persist = true});
}
activity?.Activity?.SetTag("Page Saved", blogModel.Slug);
});
Beachten Sie, dass ich Polly
hier, um die Dateiverarbeitung zu handhaben; dies stellt sicher, dass die Datei WIRKLICH hochgeladen / gespeichert wurde, bevor sie versucht, sie zu verarbeiten.
var retryPolicy = Policy
.Handle<IOException>() // Only handle IO exceptions (like file in use)
.WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromMilliseconds(500 * retryAttempt),
(exception, timeSpan, retryCount, context) =>
{
activity?.Activity?.SetTag("Retry Attempt", retryCount);
// Log the retry attempt
logger.LogWarning("File is in use, retrying attempt {RetryCount} after {TimeSpan}", retryCount, timeSpan);
});
Ich verwende auch SerilogTracing
Aktivitäten, die mir helfen, Probleme zu finden, wenn es in 'Produktion' leichter ist (I einen Artikel haben Auch darauf!)== Einzelnachweise ==
private async Task OnChangedAsync(WaitForChangedResult e)
{
if (e.Name == null) return;
var activity = Log.Logger.StartActivity("Markdown File Changed {Name}", e.Name);
var retryPolicy = Policy
.Handle<IOException>() // Only handle IO exceptions (like file in use)
.WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromMilliseconds(500 * retryAttempt),
(exception, timeSpan, retryCount, context) =>
{
activity?.Activity?.SetTag("Retry Attempt", retryCount);
// Log the retry attempt
logger.LogWarning("File is in use, retrying attempt {RetryCount} after {TimeSpan}", retryCount, timeSpan);
});
try
{
var fileName = e.Name;
var isTranslated = Path.GetFileNameWithoutExtension(e.Name).Contains(".");
var language = MarkdownBaseService.EnglishLanguage;
var directory = markdownConfig.MarkdownPath;
if (isTranslated)
{
language = Path.GetFileNameWithoutExtension(e.Name).Split('.').Last();
fileName = Path.GetFileName(fileName);
directory = markdownConfig.MarkdownTranslatedPath;
}
var filePath = Path.Combine(directory, fileName);
var scope = serviceScopeFactory.CreateScope();
var markdownBlogService = scope.ServiceProvider.GetRequiredService<IMarkdownBlogService>();
// Use the Polly retry policy for executing the operation
await retryPolicy.ExecuteAsync(async () =>
{
var blogModel = await markdownBlogService.GetPage(filePath);
activity?.Activity?.SetTag("Page Processed", blogModel.Slug);
blogModel.Language = language;
var blogService = scope.ServiceProvider.GetRequiredService<IBlogService>();
await blogService.SavePost(blogModel);
if (language == MarkdownBaseService.EnglishLanguage)
{
var translateService = scope.ServiceProvider.GetRequiredService<BackgroundTranslateService>();
await translateService.TranslateForAllLanguages(
new PageTranslationModel(){OriginalFileName = filePath, OriginalMarkdown = blogModel.Markdown,Persist = true});
}
activity?.Activity?.SetTag("Page Saved", blogModel.Slug);
});
activity?.Complete();
}
catch (Exception exception)
{
activity?.Complete(LogEventLevel.Error, exception);
}
}
Dieses ist wirklich einfach, es erkennt nur, wenn eine Datei gelöscht wird und entfernt sie aus der Datenbank; I MAINLY verwenden Sie dies als idiot-proofing Maßnahme, da ich geeignet bin, Artikel hochladen dann sofort sehen, dass sie Fehler haben. Wie Sie sehen können, testet sie, um zu sehen, ob die Datei übersetzt ist und ob sie alle Übersetzungen dieser Datei löscht.
private void OnDeleted(WaitForChangedResult e)
{
if (e.Name == null) return;
var activity = Log.Logger.StartActivity("Markdown File Deleting {Name}", e.Name);
try
{
var isTranslated = Path.GetFileNameWithoutExtension(e.Name).Contains(".");
var language = MarkdownBaseService.EnglishLanguage;
var slug = Path.GetFileNameWithoutExtension(e.Name);
if (isTranslated)
{
var name = Path.GetFileNameWithoutExtension(e.Name).Split('.');
language = name.Last();
slug = name.First();
}
else
{
var translatedFiles = Directory.GetFiles(markdownConfig.MarkdownTranslatedPath, $"{slug}.*.*");
_fileSystemWatcher.EnableRaisingEvents = false;
foreach (var file in translatedFiles)
{
File.Delete(file);
}
_fileSystemWatcher.EnableRaisingEvents = true;
}
var scope = serviceScopeFactory.CreateScope();
var blogService = scope.ServiceProvider.GetRequiredService<IBlogService>();
blogService.Delete(slug, language);
activity?.Activity?.SetTag("Page Deleted", slug);
activity?.Complete();
logger.LogInformation("Deleted blog post {Slug} in {Language}", slug, language);
}
catch (Exception exception)
{
activity?.Complete(LogEventLevel.Error, exception);
logger.LogError("Error deleting blog post {Slug}", e.Name);
}
}
Ich benutze WinSCP, um die Dateien auf meine Website hochzuladen. Ich kann einfach einen 'Sync' tun, um alle Markdown-Dateien hochzuladen, dann wird mein Dienst sie zur DB hinzufügen. Dies ist das Verzeichnis, das die FileSystemWatcher
beobachtet. In FUTURE werde ich auch dazu eine Bild-Upload-Fähigkeit hinzufügen, dort werde ich einige Vorverarbeitung und Handhabung von größeren Dateien hinzufügen.
So ist dies eine ziemlich einfache Weise, neue Blog-Posts zu meiner Website hinzuzufügen, ohne einen vollen Bauzyklus zu tun. In diesem Beitrag zeigte ich, wie man die FileSystemWatcher
um Änderungen in einem Verzeichnis zu erkennen und zu verarbeiten. Ich habe auch gezeigt, wie man Polly
für den Umgang mit Retries und Serilog
um den Prozess zu protokollieren.