This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
I'veve Багато блогів час від часу я використовую Markdown для створення дописів у блогі; мені дійсно подобається цей підхід, але він має один великий недолік - це означає, що мені потрібно зробити повний цикл збирання Docker, щоб оновити допис. Це було добре, поки я створював більше можливостей, про які я згодом писав у блогі, але це досить обмежуючи зараз. Я хочу бути в змозі оновити свої блог-повідомлення без повного збирання циклу. Тепер я додав трохи нових функціональних можливостей до мого блогу, що дозволяє мені зробити саме це.
Під під "гібридним" підходом я маю на увазі цей життєвий цикл, знову досить спрощений, але досить крутий (на супер фантастичний спосіб!).
Це доволі просто.
Це дозволяє мені й надалі використовувати їздця місцево для створення дописів блогу (в майбутньому я, ймовірно, також дозволю, щоб це відбувалося на самому сайті), всі переклади відбуваються динамічно на самому сайті, і я можу оновлювати дописи без повного збирання циклу.
Код для цього - YET ANOI IHostedService
, цього разу вона використовує FileSystemWatcher
клас для перегляду каталогу нових файлів. Якщо буде виявлено новий файл, програма прочитає файл, обробить його і вставить у базу даних. АБО, якщо я вилучу допис англійською мовою, він також вилучить всі переклади цього допису.
Весь код знаходиться нижче, але я його трохи розірву тут.
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}");
}
}
Все, що він робить - це викидає нове завдання, яке використовує FileSystemWatcher
в ньомуDescription of a condition. Do not translate key words (# V1S #, # V1 #,...) StartAsync
метод. В IHostedService
Це точка, де ядро системи ASP.NET використовує для запуску служби.
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;
}
У мене є завдання локальне для служби, на якій я вмикаю цикл зміни
private Task _awaitChangeTask = Task.CompletedTask;
_awaitChangeTask = Task.Run(()=> AwaitChanges(cancellationToken));
Це важливо для того, щоб у іншому випадку це стало блокуванням, вам слід переконатися, що це запущено у фоновому режимі (запитайте мене, звідки я це знаю).
Тут ми встановили FileSystemWatcher
Щоб дізнатися про події з мого каталогу Markdown (наказує на каталог вузла з причин, з яких ми побачимо пізніше!)
_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;
Ви бачите, що я спостерігаю за FileName
, LastWrite
, CreationTime
і Size
Зміниться. Це тому, що я хочу знати, чи створюється, оновлюється або вилучається файл.
У мене також є Filter
встановити до *.md
отже, мені слід спостерігати лише за файлами з позначенням, а також вказати, що я хочу спостерігати за підкаталогами (для перекладів).
Всередині цього коду знаходиться змінний цикл, який очікує на зміни у файловій системі. Зауважте, що ви також можете просто під'єднатися до подій з зміною, але для мене це було чистіше.
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)
{
}
}
}
Знову ж таки, досить просто, все що він робить це чекає на подію зміни, а потім кличе OnChangedAsync
метод. АБО, якщо файл буде вилучено, він викликає OnDeleted
метод.
Ось де відбувається "магія." Вона слухає змінену подію. Після цього програма обробляє файл Markdown за допомогою мого трубопроводу (щоб додати категорії HTML, дату, Заголовок і т. д.) до бази даних. Програма DON визначає, чи є файл англійською мовою (отже я написав його:) і якщо це файл, відриває процес перекладу. Див. цей допис як це працює.
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);
});
Зауважте, що я використовую Polly
тут для обробки файлів. За допомогою цього пункту можна переконатися, що файл дійсно було вивантажено або збережено до того, як програма спробує його обробити.
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);
});
Я також використовую SerilogTracing
Діяльність, яка допомагає мені знайти будь-які проблеми в "продукціях" легше (Я має статтю у цьому теж!).
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 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);
}
}
Я використовую WinSCP, щоб завантажити файли на мій сайт. Я можу просто виконати "синхронізацію," щоб вивантажити всі файли markdown, тоді моя служба додасть ці файли до DB. Це каталог, який FileSystemWatcher
переглядає. У FUTURE Я додам можливість вивантаження зображень у це також, там я додам деякі попередньої обробки і обробки великих файлів.
Це досить простий спосіб додати нові дописи блогу до мого сайту без повного збирання циклу. У цьому дописі я показав, як використовувати FileSystemWatcher
для виявлення змін у каталозі і обробки їх. Я також показав, як користуватися Polly
працювати з скарбами і Serilog
щоб увійти до журналу процесу.