# За допомогою гібридного підходу до блогу

# Вступ

I'veve [Багато блогів](/blog/category/Markdown) час від часу я використовую Markdown для створення дописів у блогі; мені дійсно подобається цей підхід, але він має один великий недолік - це означає, що мені потрібно зробити повний цикл збирання Docker, щоб оновити допис. Це було добре, поки я створював більше можливостей, про які я згодом писав у блогі, але це досить обмежуючи зараз. Я хочу бути в змозі оновити свої блог-повідомлення без повного збирання циклу.
Тепер я додав трохи нових функціональних можливостей до мого блогу, що дозволяє мені зробити саме це.

<!--category-- ASP.NET, Markdown -->
<datetime class="hidden">2024-09-14T00:30</datetime>

[TOC]

# Гібридний підхід

Під під "гібридним" підходом я маю на увазі цей життєвий цикл, знову досить спрощений, але досить крутий (на супер фантастичний спосіб!).

Це доволі просто.

1. Я пишу новий допис блогу в Markdown, зберігаю його моєму локальному комп'ютері
2. Завантажуй на мій веб-сайт.
3. Інструмент спостереження за файлами визначає новий файл і обробляє його.
4. Допис буде вставлено у базу даних
5. Перекладів відривають.
6. Після завершення перекладу допис оновлюється на веб-сайті.

Це дозволяє мені й надалі використовувати їздця місцево для створення дописів блогу (в майбутньому я, ймовірно, також дозволю, щоб це відбувалося на самому сайті), всі переклади відбуваються динамічно на самому сайті, і я можу оновлювати дописи без повного збирання циклу.

```mermaid

graph LR;
    A[Write new .md file] --> B[Upload using WinSCP];
    B --> C[New File Detected];
    C --> D[Process Markdown];
    D --> E[Insert into Database];
    E --> F[Kick off translations];
    F-->G[Add Translations to Database];
    G-->H[Update Website];

```

# Код

Код для цього - YET ANOI `IHostedService`, цього разу вона використовує `FileSystemWatcher` клас для перегляду каталогу нових файлів. Якщо буде виявлено новий файл, програма прочитає файл, обробить його і вставить у базу даних. АБО, якщо я вилучу допис англійською мовою, він також вилучить всі переклади цього допису.

Весь код знаходиться нижче, але я його трохи розірву тут.

<details>
<summary>MarkdownDirectoryWatcherService.cs</summary>

```csharp
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}");
    }
}
```

</details>
## Починається

Все, що він робить - це викидає нове завдання, яке використовує `FileSystemWatcher` в ньомуDescription of a condition. Do not translate key words (# V1S #, # V1 #,...) `StartAsync` метод. В `IHostedService` Це точка, де ядро системи ASP.NET використовує для запуску служби.

```csharp
    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;
    }
```

У мене є завдання локальне для служби, на якій я вмикаю цикл зміни

```csharp
private Task _awaitChangeTask = Task.CompletedTask;
 _awaitChangeTask = Task.Run(()=> AwaitChanges(cancellationToken));
   
```

Це важливо для того, щоб у іншому випадку це стало блокуванням, вам слід переконатися, що це запущено у фоновому режимі (запитайте мене, звідки я це знаю).

Тут ми встановили `FileSystemWatcher` Щоб дізнатися про події з мого каталогу Markdown (наказує на каталог вузла з причин, з яких ми побачимо пізніше!)

```csharp
 _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` отже, мені слід спостерігати лише за файлами з позначенням, а також вказати, що я хочу спостерігати за підкаталогами (для перекладів).

## Змінити цикл

Всередині цього коду знаходиться змінний цикл, який очікує на зміни у файловій системі.
Зауважте, що ви також можете просто під'єднатися до подій з зміною, але для мене це було чистіше.

```csharp
    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` метод.

## OnChangedAsync

Ось де відбувається "магія."
Вона слухає змінену подію. Після цього програма обробляє файл Markdown за допомогою мого трубопроводу (щоб додати категорії HTML, дату, Заголовок і т. д.) до бази даних. Програма DON визначає, чи є файл англійською мовою (отже я написав його:) і якщо це файл, відриває процес перекладу. Див. [цей допис](/blog/backgroundtranslationspt2) як це працює.

```csharp
   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` тут для обробки файлів. За допомогою цього пункту можна переконатися, що файл дійсно було вивантажено або збережено до того, як програма спробує його обробити.

```csharp
      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` Діяльність, яка допомагає мені знайти будь-які проблеми в "продукціях" легше (Я [має статтю](/blog/selfhostingseqpt2) у цьому теж!).

```mermaid
flowchart LR
    A[Start OnChangedAsync] --> B{Is e.Name null}
    B -- Yes --> C[Return]
    B -- No --> D[Start Activity and Set File Parameters]

    D --> E[Execute retryPolicy]

    E --> F[Process and Save Markdown File]
    
    F --> G{Is language English}
    G -- Yes --> H[Kick off Translation]
    G -- No --> I[Skip Translation]

    H --> J[Complete Activity]
    I --> J

    J --> K[Handle Errors]

```

<details>
<summary>OnChangedAsync</summary>

```csharp
   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);
        }
    }
```

</details>
## OnDeleted

Цей метод дуже простий, він просто виявляє, коли файл видалено і видаляє його з бази даних; я, МАБУТЬ, використовую його, як недоумкуючий захід, який змушує мене вивантажувати статті, і відразу бачу, що вони мають помилки. Як ви можете бачити, програма перевіряє перекладені файли і вилучає всі переклади цього файла.

```mermaid
flowchart LR
A[Start OnDeleted] --> B{Is e.Name null}
B -- Yes --> C[Return]
B -- No --> D[Start Activity and Set Parameters: isTranslated, language, slug]

    D --> E{Is file translated}
    E -- Yes --> F[Set language and slug for translation]
    E -- No --> G[Delete all translated files]

    F --> H[Create DI Scope and Get IBlogService]
    G --> H

    H --> I[Delete Post from Database]

    I --> J[Complete Activity]
    J --> K[Handle Errors if any]

```

```csharp
 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 Я додам можливість вивантаження зображень у це також, там я додам деякі попередньої обробки і обробки великих файлів.

![WinSCP](winscp.png)

# Включення

Це досить простий спосіб додати нові дописи блогу до мого сайту без повного збирання циклу. У цьому дописі я показав, як використовувати `FileSystemWatcher` для виявлення змін у каталозі і обробки їх. Я також показав, як користуватися `Polly` працювати з скарбами і `Serilog` щоб увійти до журналу процесу.