From d8e4931d634da6823e47fe4a5320c70eb17931cb Mon Sep 17 00:00:00 2001 From: jackfiled Date: Sat, 14 Mar 2026 19:48:44 +0800 Subject: [PATCH] refact: Let host to handle command arguments. Signed-off-by: jackfiled --- .gitea/workflows/build.yaml | 2 +- .gitignore | 1 + build.ps1 | 16 +- .../Commands/Binders/BlogOptionsBinder.cs | 33 -- .../Binders/EssayScanServiceBinder.cs | 32 -- .../Binders/ImageCompressServiceBinder.cs | 21 -- src/YaeBlog/Commands/Binders/LoggerBinder.cs | 18 -- src/YaeBlog/Commands/YaeBlogCommand.cs | 296 ------------------ src/YaeBlog/Dockerfile | 6 +- .../WebApplicationBuilderExtensions.cs | 22 +- src/YaeBlog/Program.cs | 24 +- src/YaeBlog/Services/YaeCommandService.cs | 281 +++++++++++++++++ src/YaeBlog/YaeBlog.csproj | 2 +- src/YaeBlog/appsettings.json | 2 +- src/YaeBlog/tailwind.ps1 | 11 + 15 files changed, 340 insertions(+), 427 deletions(-) delete mode 100644 src/YaeBlog/Commands/Binders/BlogOptionsBinder.cs delete mode 100644 src/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs delete mode 100644 src/YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs delete mode 100644 src/YaeBlog/Commands/Binders/LoggerBinder.cs delete mode 100644 src/YaeBlog/Commands/YaeBlogCommand.cs create mode 100644 src/YaeBlog/Services/YaeCommandService.cs create mode 100644 src/YaeBlog/tailwind.ps1 diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 84b530f..59b6f2d 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -13,8 +13,8 @@ jobs: lfs: true - name: Build project. run: | + git submodule update --init podman pull mcr.azure.cn/dotnet/aspnet:10.0 - cd YaeBlog pwsh build.ps1 build - name: Workaround to make sure podman-login working. run: | diff --git a/.gitignore b/.gitignore index c9f783c..c7e73eb 100644 --- a/.gitignore +++ b/.gitignore @@ -184,6 +184,7 @@ DocProject/Help/html # Click-Once directory publish/ +out/ # Publish Web Output *.[Pp]ublish.xml diff --git a/build.ps1 b/build.ps1 index ee766f1..6d4a3e4 100755 --- a/build.ps1 +++ b/build.ps1 @@ -3,16 +3,15 @@ [cmdletbinding()] param( [Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the build target")] - [ValidateSet("tailwind", "publish", "compress", "build", "dev", "new", "watch", "serve", "list")] + [ValidateSet("publish", "compress", "build", "dev", "new", "watch", "serve")] [string]$Target, - [string]$Output = "wwwroot", [string]$Essay, [switch]$Compress, [string]$Root = "source" ) begin { - if ($Target -eq "tailwind") + if (($Target -eq "tailwind") -or ($Target -eq "build")) { # Handle tailwind specially. return @@ -82,8 +81,10 @@ process { function Build-Image { $commitId = git rev-parse --short=10 HEAD - dotnet publish - podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId + dotnet publish ./src/YaeBlog/YaeBlog.csproj -o out + podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId ` + -f ./src/YaeBlog/Dockerfile + Remove-Item -Recurse -Force ./out } function Start-Develop { @@ -111,11 +112,6 @@ process { switch ($Target) { - "tailwind" { - Write-Host "Build tailwind css into $Output." - pnpm tailwindcss -i wwwroot/tailwind.css -o $Output/tailwind.g.css - break - } "publish" { Write-Host "Publish essay $Essay..." dotnet run -- publish $Essay diff --git a/src/YaeBlog/Commands/Binders/BlogOptionsBinder.cs b/src/YaeBlog/Commands/Binders/BlogOptionsBinder.cs deleted file mode 100644 index ee60ba7..0000000 --- a/src/YaeBlog/Commands/Binders/BlogOptionsBinder.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.CommandLine.Binding; -using System.Text.Json; -using Microsoft.Extensions.Options; -using YaeBlog.Models; - -namespace YaeBlog.Commands.Binders; - -public sealed class BlogOptionsBinder : BinderBase> -{ - protected override IOptions GetBoundValue(BindingContext bindingContext) - { - bindingContext.AddService>(_ => - { - FileInfo settings = new(Path.Combine(Environment.CurrentDirectory, "appsettings.json")); - if (!settings.Exists) - { - throw new InvalidOperationException("Failed to load YaeBlog configurations."); - } - - using StreamReader reader = settings.OpenText(); - using JsonDocument document = JsonDocument.Parse(reader.ReadToEnd()); - JsonElement root = document.RootElement; - JsonElement optionSection = root.GetProperty(BlogOptions.OptionName); - - BlogOptions? result = optionSection.Deserialize(); - return result is null - ? throw new InvalidOperationException("Failed to load YaeBlog configuration in appsettings.json.") - : new OptionsWrapper(result); - }); - - return bindingContext.GetRequiredService>(); - } -} diff --git a/src/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs b/src/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs deleted file mode 100644 index 5d7e1d5..0000000 --- a/src/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.CommandLine.Binding; -using Microsoft.Extensions.Options; -using YaeBlog.Abstraction; -using YaeBlog.Models; -using YaeBlog.Services; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace YaeBlog.Commands.Binders; - -public sealed class EssayScanServiceBinder : BinderBase -{ - protected override IEssayScanService GetBoundValue(BindingContext bindingContext) - { - bindingContext.AddService(provider => - { - DeserializerBuilder deserializerBuilder = new(); - deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance); - deserializerBuilder.IgnoreUnmatchedProperties(); - - SerializerBuilder serializerBuilder = new(); - serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance); - - IOptions options = provider.GetRequiredService>(); - ILogger logger = provider.GetRequiredService>(); - - return new EssayScanService(serializerBuilder.Build(), deserializerBuilder.Build(), options, logger); - }); - - return bindingContext.GetRequiredService(); - } -} diff --git a/src/YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs b/src/YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs deleted file mode 100644 index 95d9f51..0000000 --- a/src/YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.CommandLine.Binding; -using YaeBlog.Abstraction; -using YaeBlog.Services; - -namespace YaeBlog.Commands.Binders; - -public sealed class ImageCompressServiceBinder : BinderBase -{ - protected override ImageCompressService GetBoundValue(BindingContext bindingContext) - { - bindingContext.AddService(provider => - { - IEssayScanService essayScanService = provider.GetRequiredService(); - ILogger logger = provider.GetRequiredService>(); - - return new ImageCompressService(essayScanService, logger); - }); - - return bindingContext.GetRequiredService(); - } -} diff --git a/src/YaeBlog/Commands/Binders/LoggerBinder.cs b/src/YaeBlog/Commands/Binders/LoggerBinder.cs deleted file mode 100644 index 47bd24c..0000000 --- a/src/YaeBlog/Commands/Binders/LoggerBinder.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.CommandLine.Binding; - -namespace YaeBlog.Commands.Binders; - -public sealed class LoggerBinder : BinderBase> -{ - protected override ILogger GetBoundValue(BindingContext bindingContext) - { - bindingContext.AddService(_ => LoggerFactory.Create(builder => builder.AddConsole())); - bindingContext.AddService>(provider => - { - ILoggerFactory factory = provider.GetRequiredService(); - return factory.CreateLogger(); - }); - - return bindingContext.GetRequiredService>(); - } -} diff --git a/src/YaeBlog/Commands/YaeBlogCommand.cs b/src/YaeBlog/Commands/YaeBlogCommand.cs deleted file mode 100644 index 770b339..0000000 --- a/src/YaeBlog/Commands/YaeBlogCommand.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System.CommandLine; -using Microsoft.Extensions.Options; -using YaeBlog.Abstraction; -using YaeBlog.Commands.Binders; -using YaeBlog.Components; -using YaeBlog.Extensions; -using YaeBlog.Models; -using YaeBlog.Services; - -namespace YaeBlog.Commands; - -public sealed class YaeBlogCommand -{ - private readonly RootCommand _rootCommand = new("YaeBlog Cli"); - - public YaeBlogCommand() - { - AddServeCommand(_rootCommand); - AddWatchCommand(_rootCommand); - AddListCommand(_rootCommand); - AddNewCommand(_rootCommand); - AddUpdateCommand(_rootCommand); - AddPublishCommand(_rootCommand); - AddScanCommand(_rootCommand); - AddCompressCommand(_rootCommand); - } - - public Task RunAsync(string[] args) - { - return _rootCommand.InvokeAsync(args); - } - - private static void AddServeCommand(RootCommand rootCommand) - { - Command serveCommand = new("serve", "Start http server."); - rootCommand.AddCommand(serveCommand); - - serveCommand.SetHandler(async context => - { - WebApplicationBuilder builder = WebApplication.CreateBuilder(); - - builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); - builder.Services.AddControllers(); - builder.AddYaeBlog(); - builder.AddServer(); - - WebApplication application = builder.Build(); - - application.MapStaticAssets(); - application.UseAntiforgery(); - application.UseYaeBlog(); - - application.MapRazorComponents() - .AddInteractiveServerRenderMode(); - application.MapControllers(); - - CancellationToken token = context.GetCancellationToken(); - await application.RunAsync(token); - }); - } - - private static void AddWatchCommand(RootCommand rootCommand) - { - Command command = new("watch", "Start a blog watcher that re-render when file changes."); - rootCommand.AddCommand(command); - - command.SetHandler(async context => - { - WebApplicationBuilder builder = WebApplication.CreateBuilder(); - - builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); - builder.Services.AddControllers(); - builder.AddYaeBlog(); - builder.AddWatcher(); - - WebApplication application = builder.Build(); - - application.MapStaticAssets(); - application.UseAntiforgery(); - application.UseYaeBlog(); - - application.MapRazorComponents() - .AddInteractiveServerRenderMode(); - application.MapControllers(); - - CancellationToken token = context.GetCancellationToken(); - await application.RunAsync(token); - }); - } - - private static void AddNewCommand(RootCommand rootCommand) - { - Command newCommand = new("new", "Create a new blog file and image directory."); - rootCommand.AddCommand(newCommand); - - Argument filenameArgument = new(name: "blog name", description: "The created blog filename."); - newCommand.AddArgument(filenameArgument); - - newCommand.SetHandler(async (file, blogOption, _, essayScanService) => - { - BlogContents contents = await essayScanService.ScanContents(); - - if (contents.Posts.Any(content => content.BlogName == file)) - { - Console.WriteLine("There exists the same title blog in posts."); - return; - } - - await essayScanService.SaveBlogContent(new BlogContent( - new FileInfo(Path.Combine(blogOption.Value.Root, "drafts", file + ".md")), - new MarkdownMetadata - { - Title = file, - Date = DateTimeOffset.Now.ToString("o"), - UpdateTime = DateTimeOffset.Now.ToString("o") - }, - string.Empty, true, [], [])); - - Console.WriteLine($"Created new blog '{file}."); - }, filenameArgument, new BlogOptionsBinder(), new LoggerBinder(), - new EssayScanServiceBinder()); - } - - private static void AddUpdateCommand(RootCommand rootCommand) - { - Command newCommand = new("update", "Update the blog essay."); - rootCommand.AddCommand(newCommand); - - Argument filenameArgument = new(name: "blog name", description: "The blog filename to update."); - newCommand.AddArgument(filenameArgument); - - newCommand.SetHandler(async (file, _, _, essayScanService) => - { - Console.WriteLine("HINT: The update command only consider published blogs."); - BlogContents contents = await essayScanService.ScanContents(); - - BlogContent? content = contents.Posts.FirstOrDefault(c => c.BlogName == file); - if (content is null) - { - Console.WriteLine($"Target essay {file} is not exist."); - return; - } - - content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o"); - await essayScanService.SaveBlogContent(content, content.IsDraft); - }, filenameArgument, - new BlogOptionsBinder(), new LoggerBinder(), new EssayScanServiceBinder()); - } - - private static void AddListCommand(RootCommand rootCommand) - { - Command command = new("list", "List all blogs"); - rootCommand.AddCommand(command); - - command.SetHandler(async (_, _, essyScanService) => - { - BlogContents contents = await essyScanService.ScanContents(); - - Console.WriteLine($"All {contents.Posts.Count} Posts:"); - foreach (BlogContent content in contents.Posts.OrderBy(x => x.BlogName)) - { - Console.WriteLine($" - {content.BlogName}"); - } - - Console.WriteLine($"All {contents.Drafts.Count} Drafts:"); - foreach (BlogContent content in contents.Drafts.OrderBy(x => x.BlogName)) - { - Console.WriteLine($" - {content.BlogName}"); - } - }, new BlogOptionsBinder(), new LoggerBinder(), new EssayScanServiceBinder()); - } - - private static void AddScanCommand(RootCommand rootCommand) - { - Command command = new("scan", "Scan unused and not found images."); - rootCommand.AddCommand(command); - - Option removeOption = - new(name: "--rm", description: "Remove unused images.", getDefaultValue: () => false); - command.AddOption(removeOption); - - command.SetHandler(async (_, _, essayScanService, removeOptionValue) => - { - BlogContents contents = await essayScanService.ScanContents(); - List unusedImages = (from content in contents - from image in content.Images - where image is { IsUsed: false } - select image).ToList(); - - if (unusedImages.Count != 0) - { - Console.WriteLine("Found unused images:"); - Console.WriteLine("HINT: use '--rm' to remove unused images."); - } - - foreach (BlogImageInfo image in unusedImages) - { - Console.WriteLine($" - {image.File.FullName}"); - } - - if (removeOptionValue) - { - foreach (BlogImageInfo image in unusedImages) - { - image.File.Delete(); - } - } - - Console.WriteLine("Used not existed images:"); - - foreach (BlogContent content in contents) - { - foreach (FileInfo file in content.NotfoundImages) - { - Console.WriteLine($"- {file.Name} in {content.BlogName}"); - } - } - }, new BlogOptionsBinder(), new LoggerBinder(), new EssayScanServiceBinder(), removeOption); - } - - private static void AddPublishCommand(RootCommand rootCommand) - { - Command command = new("publish", "Publish a new blog file."); - rootCommand.AddCommand(command); - - Argument filenameArgument = new(name: "blog name", description: "The published blog filename."); - command.AddArgument(filenameArgument); - - command.SetHandler(async (blogOptions, _, essayScanService, filename) => - { - BlogContents contents = await essayScanService.ScanContents(); - - BlogContent? content = (from blog in contents.Drafts - where blog.BlogName == filename - select blog).FirstOrDefault(); - - if (content is null) - { - Console.WriteLine("Target blog does not exist."); - return; - } - - // 设置发布的时间 - content.Metadata.Date = DateTimeOffset.Now.ToString("o"); - content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o"); - - // 将选中的博客文件复制到posts - await essayScanService.SaveBlogContent(content, isDraft: false); - - // 复制图片文件夹 - DirectoryInfo sourceImageDirectory = - new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName)); - DirectoryInfo targetImageDirectory = - new(Path.Combine(blogOptions.Value.Root, "posts", content.BlogName)); - - if (sourceImageDirectory.Exists) - { - targetImageDirectory.Create(); - foreach (FileInfo file in sourceImageDirectory.EnumerateFiles()) - { - file.CopyTo(Path.Combine(targetImageDirectory.FullName, file.Name), true); - } - - sourceImageDirectory.Delete(true); - } - - // 删除原始的文件 - FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName + ".md")); - sourceBlogFile.Delete(); - }, new BlogOptionsBinder(), - new LoggerBinder(), new EssayScanServiceBinder(), filenameArgument); - } - - private static void AddCompressCommand(RootCommand rootCommand) - { - Command command = new("compress", "Compress png/jpeg image to webp image to reduce size."); - rootCommand.Add(command); - - Option dryRunOption = new("--dry-run", description: "Dry run the compression task but not write.", - getDefaultValue: () => false); - command.AddOption(dryRunOption); - - command.SetHandler(ImageCommandHandler, - new BlogOptionsBinder(), new LoggerBinder(), new LoggerBinder(), - new EssayScanServiceBinder(), new ImageCompressServiceBinder(), dryRunOption); - } - - private static async Task ImageCommandHandler(IOptions _, ILogger _1, - ILogger _2, - IEssayScanService _3, ImageCompressService imageCompressService, bool dryRun) - { - await imageCompressService.Compress(dryRun); - } -} diff --git a/src/YaeBlog/Dockerfile b/src/YaeBlog/Dockerfile index 021e1d0..515b8c7 100644 --- a/src/YaeBlog/Dockerfile +++ b/src/YaeBlog/Dockerfile @@ -4,8 +4,10 @@ ARG COMMIT_ID ENV COMMIT_ID=${COMMIT_ID} WORKDIR /app -COPY bin/Release/net10.0/publish/ ./ +COPY out/ ./ COPY source/ ./source/ -COPY appsettings.json . +COPY src/YaeBlog/appsettings.json . + +ENV BLOG__ROOT="./source" ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"] diff --git a/src/YaeBlog/Extensions/WebApplicationBuilderExtensions.cs b/src/YaeBlog/Extensions/WebApplicationBuilderExtensions.cs index e75050a..192412c 100644 --- a/src/YaeBlog/Extensions/WebApplicationBuilderExtensions.cs +++ b/src/YaeBlog/Extensions/WebApplicationBuilderExtensions.cs @@ -1,4 +1,5 @@ using AngleSharp; +using Microsoft.Extensions.Options; using YaeBlog.Abstraction; using YaeBlog.Services; using YaeBlog.Models; @@ -27,22 +28,25 @@ public static class WebApplicationBuilderExtensions .AddTransient() .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() .AddSingleton(); return builder; } - public WebApplicationBuilder AddServer() + public WebApplicationBuilder AddYaeCommand(string[] arguments) { - builder.Services.AddHostedService(); + builder.Services.AddHostedService(provider => + { + IEssayScanService essayScanService = provider.GetRequiredService(); + IOptions blogOptions = provider.GetRequiredService>(); + ILogger logger = provider.GetRequiredService>(); + IHostApplicationLifetime applicationLifetime = provider.GetRequiredService(); - return builder; - } - - public WebApplicationBuilder AddWatcher() - { - builder.Services.AddTransient(); - builder.Services.AddHostedService(); + return new YaeCommandService(arguments, essayScanService, provider, blogOptions, logger, + applicationLifetime); + }); return builder; } diff --git a/src/YaeBlog/Program.cs b/src/YaeBlog/Program.cs index e29050d..d100ee0 100644 --- a/src/YaeBlog/Program.cs +++ b/src/YaeBlog/Program.cs @@ -1,4 +1,22 @@ -using YaeBlog.Commands; +using YaeBlog.Components; +using YaeBlog.Extensions; -YaeBlogCommand command = new(); -await command.RunAsync(args); +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); +builder.Services.AddControllers(); +builder.AddYaeBlog(); +builder.AddYaeCommand(args); + +WebApplication application = builder.Build(); + +application.MapStaticAssets(); +application.UseAntiforgery(); +application.UseYaeBlog(); + +application.MapRazorComponents() + .AddInteractiveServerRenderMode(); +application.MapControllers(); + +await application.RunAsync(); diff --git a/src/YaeBlog/Services/YaeCommandService.cs b/src/YaeBlog/Services/YaeCommandService.cs new file mode 100644 index 0000000..e4dd2ed --- /dev/null +++ b/src/YaeBlog/Services/YaeCommandService.cs @@ -0,0 +1,281 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Text; +using Microsoft.Extensions.Options; +using YaeBlog.Abstraction; +using YaeBlog.Core.Exceptions; +using YaeBlog.Models; + +namespace YaeBlog.Services; + +public class YaeCommandService( + string[] arguments, + IEssayScanService essayScanService, + IServiceProvider serviceProvider, + IOptions blogOptions, + ILogger logger, + IHostApplicationLifetime applicationLifetime) + : IHostedService +{ + private readonly BlogOptions _blogOptions = blogOptions.Value; + private bool _oneShotCommandFlag = true; + + public async Task StartAsync(CancellationToken cancellationToken) + { + RootCommand rootCommand = new("YaeBlog CLI"); + + RegisterServeCommand(rootCommand); + RegisterWatchCommand(rootCommand, cancellationToken); + + RegisterNewCommand(rootCommand); + RegisterUpdateCommand(rootCommand); + RegisterScanCommand(rootCommand); + RegisterPublishCommand(rootCommand); + RegisterCompressCommand(rootCommand); + + int exitCode = await rootCommand.InvokeAsync(arguments); + + if (exitCode != 0) + { + throw new BlogCommandException($"YaeBlog command exited with no-zero code {exitCode}"); + } + + if (_oneShotCommandFlag) + { + applicationLifetime.StopApplication(); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private void RegisterServeCommand(RootCommand rootCommand) + { + Command command = new("serve", "Start http server."); + rootCommand.AddCommand(command); + + command.SetHandler(HandleServeCommand); + + // When invoking the root command without sub command, fallback to serve command. + rootCommand.SetHandler(HandleServeCommand); + } + + private async Task HandleServeCommand(InvocationContext context) + { + _oneShotCommandFlag = false; + + logger.LogInformation("Failed to load cache, re-render essays."); + RendererService rendererService = serviceProvider.GetRequiredService(); + await rendererService.RenderAsync(); + } + + private void RegisterWatchCommand(RootCommand rootCommand, CancellationToken cancellationToken) + { + Command command = new("watch", "Start a blog watcher that re-render when file changes."); + rootCommand.AddCommand(command); + + command.SetHandler(async _ => + { + _oneShotCommandFlag = false; + + // BlogHotReloadService is derived from BackgroundService, but we do not let framework trigger it. + BlogHotReloadService blogHotReloadService = serviceProvider.GetRequiredService(); + await blogHotReloadService.StartAsync(cancellationToken); + }); + } + + private void RegisterNewCommand(RootCommand rootCommand) + { + Command command = new("new", "Create a new blog file and image directory."); + rootCommand.AddCommand(command); + + Argument filenameArgument = new(name: "blog name", description: "The created blog filename."); + command.AddArgument(filenameArgument); + + command.SetHandler(HandleNewCommand, filenameArgument); + } + + private async Task HandleNewCommand(string filename) + { + BlogContents contents = await essayScanService.ScanContents(); + + if (contents.Posts.Any(content => content.BlogName == filename)) + { + throw new BlogCommandException("There exits the same title blog in posts."); + } + + await essayScanService.SaveBlogContent(new BlogContent( + new FileInfo(Path.Combine(_blogOptions.Root, "drafts", filename + ".md")), + new MarkdownMetadata + { + Title = filename, + Date = DateTimeOffset.Now.ToString("o"), + UpdateTime = DateTimeOffset.Now.ToString("o") + }, + string.Empty, true, [], [] + )); + + logger.LogInformation("Create new blog '{}'", filename); + } + + private void RegisterUpdateCommand(RootCommand rootCommand) + { + Command command = new("update", "Update the blog essay."); + rootCommand.AddCommand(command); + + Argument filenameArgument = new(name: "blog name", description: "The blog filename to update."); + command.AddArgument(filenameArgument); + + command.SetHandler(HandleUpdateCommand, filenameArgument); + } + + private async Task HandleUpdateCommand(string filename) + { + logger.LogInformation("The update command only considers published blogs."); + BlogContents contents = await essayScanService.ScanContents(); + + BlogContent? content = contents.Posts.FirstOrDefault(c => c.BlogName == filename); + if (content is null) + { + throw new BlogCommandException($"Target essay {filename} is not exist."); + } + + content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o"); + await essayScanService.SaveBlogContent(content, content.IsDraft); + logger.LogInformation("Update time of essay '{}' updated.", content.BlogName); + } + + private void RegisterScanCommand(RootCommand rootCommand) + { + Command command = new("scan", "Scan unused and not found images."); + rootCommand.AddCommand(command); + + Option removeOption = + new(name: "--rm", description: "Remove unused images.", getDefaultValue: () => false); + command.AddOption(removeOption); + + command.SetHandler(HandleScanCommand, removeOption); + } + + private async Task HandleScanCommand(bool removeUnusedImages) + { + BlogContents contents = await essayScanService.ScanContents(); + List unusedImages = (from content in contents + from image in content.Images + where image is { IsUsed: false } + select image).ToList(); + + if (unusedImages.Count != 0) + { + StringBuilder builder = new(); + builder.Append("Found unused images:").Append('\n'); + + foreach (BlogImageInfo image in unusedImages) + { + builder.Append('\t').Append("- ").Append(image.File.FullName).Append('\n'); + } + + logger.LogInformation("{}", builder.ToString()); + logger.LogInformation("HINT: use '--rm' to remove unused images."); + } + + if (removeUnusedImages) + { + foreach (BlogImageInfo image in unusedImages) + { + image.File.Delete(); + } + } + + StringBuilder infoBuilder = new(); + infoBuilder.Append("Used not existed images:\n"); + + bool flag = false; + foreach (BlogContent content in contents) + { + foreach (FileInfo file in content.NotfoundImages) + { + flag = true; + infoBuilder.Append('\t').Append("- ").Append(file.Name).Append(" in ").Append(content.BlogName) + .Append('\n'); + } + } + + if (flag) + { + logger.LogInformation("{}", infoBuilder.ToString()); + } + } + + private void RegisterPublishCommand(RootCommand rootCommand) + { + Command command = new("publish", "Publish a new blog file."); + rootCommand.AddCommand(command); + + Argument filenameArgument = new(name: "blog name", description: "The published blog filename."); + command.AddArgument(filenameArgument); + + command.SetHandler(HandlePublishCommand, filenameArgument); + } + + private async Task HandlePublishCommand(string filename) + { + BlogContents contents = await essayScanService.ScanContents(); + + BlogContent? content = (from blog in contents.Drafts + where blog.BlogName == filename + select blog).FirstOrDefault(); + + if (content is null) + { + throw new BlogCommandException("Target blog doest not exist."); + } + + logger.LogInformation("Publish blog {}", content.BlogName); + + // 设置发布的时间 + content.Metadata.Date = DateTimeOffset.Now.ToString("o"); + content.Metadata.UpdateTime = DateTimeOffset.Now.ToString("o"); + + // 将选中的博客文件复制到posts + await essayScanService.SaveBlogContent(content, isDraft: false); + + // 复制图片文件夹 + DirectoryInfo sourceImageDirectory = + new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName)); + DirectoryInfo targetImageDirectory = + new(Path.Combine(blogOptions.Value.Root, "posts", content.BlogName)); + + if (sourceImageDirectory.Exists) + { + targetImageDirectory.Create(); + foreach (FileInfo file in sourceImageDirectory.EnumerateFiles()) + { + file.CopyTo(Path.Combine(targetImageDirectory.FullName, file.Name), true); + } + + sourceImageDirectory.Delete(true); + } + + // 删除原始的文件 + FileInfo sourceBlogFile = new(Path.Combine(blogOptions.Value.Root, "drafts", content.BlogName + ".md")); + sourceBlogFile.Delete(); + } + + private void RegisterCompressCommand(RootCommand rootCommand) + { + Command command = new("compress", "Compress png/jpeg image to webp image to reduce size."); + rootCommand.Add(command); + + Option dryRunOption = new("--dry-run", description: "Dry run the compression task but not write.", + getDefaultValue: () => false); + command.AddOption(dryRunOption); + + command.SetHandler(HandleCompressCommand, dryRunOption); + } + + private async Task HandleCompressCommand(bool dryRun) + { + ImageCompressService imageCompressService = serviceProvider.GetRequiredService(); + await imageCompressService.Compress(dryRun); + } +} diff --git a/src/YaeBlog/YaeBlog.csproj b/src/YaeBlog/YaeBlog.csproj index d25927b..645a443 100644 --- a/src/YaeBlog/YaeBlog.csproj +++ b/src/YaeBlog/YaeBlog.csproj @@ -27,6 +27,6 @@ pnpm install - pwsh ../../build.ps1 tailwind + pwsh tailwind.ps1 diff --git a/src/YaeBlog/appsettings.json b/src/YaeBlog/appsettings.json index f55e6fd..d0fe6e4 100644 --- a/src/YaeBlog/appsettings.json +++ b/src/YaeBlog/appsettings.json @@ -18,7 +18,7 @@ "Links": [ { "Name": "Ichirinko", - "Description": "这是个大哥", + "Description": "黑历史集合地,naive的代价", "Link": "https://ichirinko.top", "AvatarImage": "https://ichirinko-blog-img-1.oss-cn-shenzhen.aliyuncs.com/Pic_res/img/202209122110798.png" }, diff --git a/src/YaeBlog/tailwind.ps1 b/src/YaeBlog/tailwind.ps1 new file mode 100644 index 0000000..3d19da1 --- /dev/null +++ b/src/YaeBlog/tailwind.ps1 @@ -0,0 +1,11 @@ +#!/pwsh + +[cmdletbinding()] +param( + [string]$Output = "wwwroot" +) + +end { + Write-Host "Build tailwind css into $Output." + pnpm tailwindcss -i wwwroot/tailwind.css -o $Output/tailwind.g.css +}