Back to "استعمال نهج مُهَجَل في التدوِّج"

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

ASP.NET Markdown

استعمال نهج مُهَجَل في التدوِّج

Saturday, 14 September 2024

أولاً

                          • العديد من المدونات عن كيفية استخدامي لماركداون لإنشاء مقالات مدونتي، أنا حقاً أحب هذا النهج ولكن لديه عيب رئيسي واحد، يعني أن علي القيام بدورة كاملة في بناء Doker لتحديث تدوينة. كان هذا جيداً بينما كنت أصنع المزيد من الميزات التي قمت بكتابتها بعد ذلك لكنها محدودة جداً الآن. أريد أن أكون قادرة على تحديث مقالات مدونتي دون الحاجة إلى القيام بدورة بناء كاملة. لذا فقد قمت الآن بإضافة جزء جديد من الوظائف إلى مدونتي مما يسمح لي بالقيام بذلك.

رابعاً - النهج المُدَلَّل

عن طريق نهج "Hybridd" أعني دورة الحياة هذه؛ مرة أخرى مبسطة جدا جدا ولكن كيندا بارد (بطريقة مهوسة جدا!)ع(

إذاً هو بسيط جداً.

  1. أكتب تدوينة جديدة في "ماركداون" وأحفظها لآلتي المحلية
  2. تحميله إلى موقعي الإلكتروني.
  3. أمين ملف يكشف الملف الجديد ويعالجه.
  4. أُدخلت الوظيفة في قاعدة البيانات
  5. الترجمات تم طردها.
  6. وبمجرد اكتمال الترجمات، يُحدَّث الموقع الجديد على الموقع الشبكي.

هذا يسمح لي بالاستمرار في استخدام Rider محلياً لإنشاء مواقع مدونية (في المستقبل سأنتقل على الأرجح للسماح بحدوث هذا أيضاً في الموقع نفسه)، كل الترجمات تحدث بشكل ديناميكي على الموقع نفسه ويمكنني تحديث المقالات دون الحاجة إلى القيام بدورة بناء كاملة.

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

ألف - القانون

الرمز لهذا هو حقاً آخر IHostedServiceهذه المرة تستخدم FileSystemWatcher إلى a دليل لـ جديد ملفات. عند اكتشاف ملف جديد فإنه يقرأ الملف ويعالجه ويدرجه في قاعدة البيانات. وإذا قمت بحذف وظيفة من وظائف اللغة الانكليزية، فستحذف جميع ترجمات تلك الوظيفة أيضا.

الشفرة كلها تحت لكني سأكسرها قليلاً هنا

MarkdownDirectoryWatcherService.cs
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 في داخله StartAsync من الناحية العملية. في 1 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 للاستماع إلى الأحداث في دليلي الخاص بالماركداون (مرتبة إلى دليل مضيف لأسباب سنرى لاحقاً!)

 _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 من الناحية العملية. IF IF الـ ملفّ هو مُحححّف، فإنّه يدعو OnDeleted من الناحية العملية.

& مُتغيّر AsSync

هنا حيث "السحر" يحدث. ما يفعله هو الإستماع لحدث التغيير ثم يقوم بعد ذلك بتجهيز ملف الماركداون باستخدام خط أنابيبي (للحصول على فئات HTML، التاريخ، العنوان وما إلى ذلك) ويُدخل هذا في قاعدة البيانات. ثم يكشف إذا كان الملف باللغة الإنجليزية (لذا كتبته:) وإذا كان كذلك فإنه يبدأ من عملية الترجمة. انظر S انظر هذه الوظيفة لكيفية عمل هذا.

   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 الأنشطة لمساعدتي في العثور على أي مشاكل عندما يكون في "الإنتاج" بسهولة أكبر (I لـه مادة على ذلك أيضاً!)ع(

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]
OnChangedAsync
   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);
        }
    }
## جاري مُحذف

هذا بسيط جداً، إنه فقط يكشف عندما يحذف الملف ويزيله من قاعدة البيانات؛ أنا أستعمل هذا كمقياس ضد البلهاء كما أنا قادر على رفع المقالات ثم على الفور أرى أن لديهم أخطاء. كما يمكنك أن ترى أنها تختبر لترى ما إذا كان الملف مترجمة و إذا كان هو يحذف كل ترجمات ذلك الملف.

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]
 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 لتحميل الملفات إلى موقعي الإلكتروني. يمكنني فقط القيام بـ 'ysync' لتحميل أي علامة إلى أسفل الملفات ثم تقوم خدمتي بإضافتها إلى DB. هذا هو الدليل الذي FileSystemWatcher يُراقبُ. في المستقبل سأضيف قدرة رفع صورة إلى هذا أيضاً، هناك سأضيف بعض التجهيز الأولي و معالجة ملفات أكبر.

& سنسسس

في الإستنتاج

إذاً هذه طريقة بسيطة جداً لإضافة مدونات جديدة لموقعي دون الحاجة إلى القيام بدورة بناء كاملة. في هذا المنصب عرضت كيفية استخدام FileSystemWatcher (ب) الكشف عن التغييرات التي تطرأ على دليل وتجهيزها. كما بينت كيفية استخدام Polly إلى مـن أجـل مـن أجـل مـن مـن Serilog إلى سجل العملية.

logo

©2024 Scott Galloway