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

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

我已经 博客上写了很多 我如何使用 Markdown 来创建博客文章的时数; 我非常喜欢这个方法, 但有一个主要的缺点, 这意味着我必须完成一个完整的 Docker 构建周期来更新一个文章 。 我在创造更多功能时,这是FFE, 然后我又在博客上写了这些功能, 但现在这已经相当有限了。 我希望能够更新我的博客文章, 所以现在我在我的博客上增加了一个新的功能, 让我可以这样做。

混合方法

我指的是这个生命周期; 又是相当简单但又很酷的(以超级怪胎的方式!) )

所以很简单

  1. 我在Markdown写了一篇新博客文章,
  2. 上传到我的网站上
  3. 文件观察器检测新文件并处理它 。
  4. 该员额被插入到数据库中
  5. 翻译被取消 。
  6. 翻译完成后,该员额将在网站上更新。

这允许我继续在当地使用骑士来创建博客文章(将来我可能会让网站本身也出现这种情况), 所有翻译都动态地出现在网站本身,

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 类可以查看新文件的目录。 当检测到一个新文件时, 它会读取文件, 处理并插入到数据库中 。 如果我删除一个英文职位,它也将删除该职位的所有译文。

整个代码都在下面 但我会把它分解一下

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 方法。 在一个 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, CreationTimeSize 变化。 这是因为我想知道文件是创建、更新还是删除。

我也有一个 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 方法。

上张的Assync

这就是"魔力"发生的地方 它所做的就是倾听改变事件。 然后用我的管道处理标记文件( 以获得 HTML 分类、 日期、 标题等) 并将其输入数据库 。 它检测文件是否用英文(所以我写了:), 见见 此员额 这是如何运作的。

   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 将文件上传到我的网站 。 我可以做一个“ 同步” 来上传任何标记文件, 然后我的服务会把它们添加到 DB 中 。 这是 FileSystemWatcher 正在观看。 未来我还会增加一个图像上传能力, 在那里,我会增加一些预处理 和处理更大的文件。

WinSCP 温西CP

在结论结论中

所以,这是一个很简单的方法, 在我的网站上添加新的博客文章, 而不必做一个完整的建设周期。 这篇文章中,我教人如何使用 FileSystemWatcher 来检测对目录的更改并处理这些更改。 我还教过如何使用 Polly 处理重试和 Serilog 来记录进程。

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.