ब्लॉगिंग के लिए हाइब्रिड दृष्टिकोण का उपयोग करना
परिचय
I've कई ब्लॉग किया मैं अपने ब्लॉग पोस्ट बनाने के लिए मार्कडाउन का प्रयोग कैसे करता हूँ के बारे में कई बार। तो अब मैं अपने ब्लॉग में एक नया प्रकार्य जोड़ा है जो मुझे सिर्फ यह करने के लिए अनुमति देता है
हाइब्रिड दृष्टिकोण
दृष्टिकोण से मेरा मतलब है यह जीवन चक्र
तो यह काफी सरल है
- मैं मार्कडाउन में एक नया ब्लॉग पोस्ट लिखता हूँ
- इसे मेरी वेबसाइट पर अपलोड करें
- एक फ़ाइलWatcher नए फ़ाइल को पता लगाता है और उसे संसाधित करता है
- पद डाटाबेस में घुसाया जाता है
- अनुवाद आरंभ कर दिए गए हैं
- जब अनुवाद पूर्ण हो जाते हैं तो पोस्ट वेबसाइट पर अद्यतन किया जाता है
यह मुझे ब्लॉग पोस्ट बनाने के लिए राइडर स्थानीय रूप से उपयोग जारी रखने की अनुमति देता है ( भविष्य में मैं'\मैं संभवतः यह भी करने के लिए स्थानांतरित हो जाएगा कि यह स्वयं साइट पर हो जाए),\ सभी अनुवाद स्वचालित रूप से साइट पर होते हैं और मैं बिना एक पूरा निर्माण चक्र करने के पोस्टों को अद्यतन कर सकते हैं
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 नई फ़ाइलों के लिए निर्देशिका देखने के लिए वर्ग. जब कोई नया फ़ाइल पता चला जाता है तो वह फ़ाइल को पढ़ता है, उसे प्रक्रमित करता है और डाटाबेस में डाल देता है।
पूरी कोड नीचे है लेकिन मैं इसे यहाँ थोड़ा विभाजित करेंगे
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 मेरे मार्कडाउन निर्देशिका में घटनाओं के लिए सुनने के लिए
_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 भी देखना चाहता हूँ
परिवर्तन लूप
इस कोड के अंदर मैं एक परिवर्तन लूप है जो फ़ाइल सिस्टम में परिवर्तनों की प्रतीक्षा करता है नोट आप भी सिर्फ यहाँ परिवर्तन घटनाओं में जुड़ सकते हैं लेकिन यह मेरे लिए साफ महसूस किया
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' होता है यह क्या करता है कि परिवर्तन घटना के लिए सुनता है। इस पोस्ट यह कैसे काम करता है के लिए
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 मुझे किसी भी समस्या को ढूंढने में मदद करने के लिए क्रियाकलाप जब यह अधिक आसान है एक लेख रखना पर भी
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 का उपयोग अपने वेबसाइट में फ़ाइलों को अपलोड करने के लिए कर सकता है . मैं सिर्फ एक 'sync' किसी भी मार्क डाउन फ़ाइलें अपलोड करने पर कर सकते हैं तब मेरी सेवा DB में उन्हें जोड़ेगा FileSystemWatcher देख रहा है. भविष्य में मैं'इस पर भी एक छवि अपलोड करने की क्षमता जोड़ूँगा।

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