# ब्लॉगिंग के लिए हाइब्रिड दृष्टिकोण का उपयोग करना

# परिचय

I've [कई ब्लॉग किया](/blog/category/Markdown) मैं अपने ब्लॉग पोस्ट बनाने के लिए मार्कडाउन का प्रयोग कैसे करता हूँ के बारे में कई बार।
तो अब मैं अपने ब्लॉग में एक नया प्रकार्य जोड़ा है जो मुझे सिर्फ यह करने के लिए अनुमति देता है

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

[TOC]

# हाइब्रिड दृष्टिकोण

दृष्टिकोण से मेरा मतलब है यह जीवन चक्र

तो यह काफी सरल है

1. मैं मार्कडाउन में एक नया ब्लॉग पोस्ट लिखता हूँ
2. इसे मेरी वेबसाइट पर अपलोड करें
3. एक फ़ाइलWatcher नए फ़ाइल को पता लगाता है और उसे संसाधित करता है
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 ANOTHER है `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` में `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` मेरे मार्कडाउन निर्देशिका में घटनाओं के लिए सुनने के लिए

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

आप देख सकते हैं I'm के लिए देख रहे `FileName`, `LastWrite`, `CreationTime` और `Size` परिवर्तन. यह इसलिए है क्योंकि मैं जानना चाहता हूँ कि क्या एक फ़ाइल बनाया गया है

मेरे पास भी एक है `Filter` सेट करें `*.md` तो मैं सिर्फ मार्क डाउन फ़ाइलों के लिए देखता हूँ और निर्दिष्ट करता है कि मैं subdirectories भी देखना चाहता हूँ

## परिवर्तन लूप

इस कोड के अंदर मैं एक परिवर्तन लूप है जो फ़ाइल सिस्टम में परिवर्तनों की प्रतीक्षा करता है
नोट आप भी सिर्फ यहाँ परिवर्तन घटनाओं में जुड़ सकते हैं लेकिन यह मेरे लिए साफ महसूस किया

```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

यह है जहाँ 'magic' होता है
यह क्या करता है कि परिवर्तन घटना के लिए सुनता है। [इस पोस्ट](/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>
## मिटाया गया

यह एक सचमुच सरल है। <s> यह सिर्फ पता लगाता है जब कोई फ़ाइल मिटाया जाता है और डाटाबेस से इसे हटा देता है।

```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 का उपयोग अपने वेबसाइट में फ़ाइलों को अपलोड करने के लिए कर सकता है . मैं सिर्फ एक 'sync' किसी भी मार्क डाउन फ़ाइलें अपलोड करने पर कर सकते हैं तब मेरी सेवा DB में उन्हें जोड़ेगा `FileSystemWatcher` देख रहा है. भविष्य में मैं'इस पर भी एक छवि अपलोड करने की क्षमता जोड़ूँगा।

![WinSCP](winscp.png)

# अंत में

तो यह एक बहुत सरल तरीका है मेरे साइट में नए ब्लॉग पोस्ट जोड़ने के लिए बिना एक पूरा निर्माण चक्र करने के लिए है `FileSystemWatcher` डाइरेक्टरी में परिवर्तनों को पहचानने और उन्हें प्रक्रिया करने के लिए `Polly` पुनः प्रयासों को संभालने के लिए और `Serilog` प्रक्रिया को लॉग करने के लिए