From 4085b0d99c4535e5cbe2b8a64f636f7c46631ed1 Mon Sep 17 00:00:00 2001 From: jackfiled Date: Sun, 25 Aug 2024 15:38:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=BE=E7=89=87=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E5=92=8C=E5=8F=91=E5=B8=83=E5=91=BD=E4=BB=A4=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://git.rrricardo.top/jackfiled/YaeBlog/pulls/5 --- .../Abstractions/IEssayScanService.cs | 2 + YaeBlog.Core/Models/ImageScanResult.cs | 3 + YaeBlog.Core/Services/EssayScanService.cs | 91 ++++++++++++++- YaeBlog/Commands/CommandExtensions.cs | 104 +++++++++++++++++- YaeBlog/Program.cs | 2 + YaeBlog/source/drafts/test-essay.md | 6 - YaeBlog/source/posts/archlinux-sop.md | 2 +- YaeBlog/source/posts/big-homework.md | 2 +- ...19-20-21-30-Screenshot_20230119_202008.png | 3 - YaeBlog/source/posts/daily-linux-0.md | 2 +- ...12-13-28-11-Screenshot_20230112_132756.png | 3 - YaeBlog/source/posts/daily-linux-1.md | 4 +- YaeBlog/source/posts/daily-linux-2.md | 2 +- .../daily-linux-2/image-20230720202327220.png | 3 - YaeBlog/source/posts/daily-linux-3.md | 2 +- YaeBlog/source/posts/daily-linux-4.md | 2 +- YaeBlog/source/posts/parser-combinator.md | 4 +- YaeBlog/source/posts/vscode-in-browser.md | 2 +- 18 files changed, 208 insertions(+), 31 deletions(-) create mode 100644 YaeBlog.Core/Models/ImageScanResult.cs delete mode 100644 YaeBlog/source/drafts/test-essay.md delete mode 100644 YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-21-30-Screenshot_20230119_202008.png delete mode 100644 YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-11-Screenshot_20230112_132756.png delete mode 100644 YaeBlog/source/posts/daily-linux-2/image-20230720202327220.png diff --git a/YaeBlog.Core/Abstractions/IEssayScanService.cs b/YaeBlog.Core/Abstractions/IEssayScanService.cs index 56957c8..5395b9a 100644 --- a/YaeBlog.Core/Abstractions/IEssayScanService.cs +++ b/YaeBlog.Core/Abstractions/IEssayScanService.cs @@ -7,4 +7,6 @@ public interface IEssayScanService public Task ScanContents(); public Task SaveBlogContent(BlogContent content, bool isDraft = true); + + public Task ScanImages(); } diff --git a/YaeBlog.Core/Models/ImageScanResult.cs b/YaeBlog.Core/Models/ImageScanResult.cs new file mode 100644 index 0000000..53e8422 --- /dev/null +++ b/YaeBlog.Core/Models/ImageScanResult.cs @@ -0,0 +1,3 @@ +namespace YaeBlog.Core.Models; + +public record struct ImageScanResult(List UnusedImages, List NotFoundImages); diff --git a/YaeBlog.Core/Services/EssayScanService.cs b/YaeBlog.Core/Services/EssayScanService.cs index 5ac4036..29c5cac 100644 --- a/YaeBlog.Core/Services/EssayScanService.cs +++ b/YaeBlog.Core/Services/EssayScanService.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using YaeBlog.Core.Abstractions; @@ -9,7 +10,7 @@ using YamlDotNet.Serialization; namespace YaeBlog.Core.Services; -public class EssayScanService( +public partial class EssayScanService( ISerializer yamlSerializer, IDeserializer yamlDeserializer, IOptions blogOptions, @@ -34,6 +35,11 @@ public class EssayScanService( ? new FileInfo(Path.Combine(drafts.FullName, content.FileName + ".md")) : new FileInfo(Path.Combine(posts.FullName, content.FileName + ".md")); + if (!isDraft) + { + content.Metadata.Date = DateTime.Now; + } + if (targetFile.Exists) { logger.LogWarning("Blog {} exists, overriding.", targetFile.Name); @@ -44,7 +50,15 @@ public class EssayScanService( await writer.WriteAsync("---\n"); await writer.WriteAsync(yamlSerializer.Serialize(content.Metadata)); await writer.WriteAsync("---\n"); - await writer.WriteAsync("\n"); + + if (isDraft) + { + await writer.WriteLineAsync(""); + } + else + { + await writer.WriteAsync(content.FileContent); + } } private async Task> ScanContentsInternal(DirectoryInfo directory) @@ -96,6 +110,79 @@ public class EssayScanService( return contents; } + public async Task ScanImages() + { + BlogContents contents = await ScanContents(); + ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts); + + List unusedFiles = []; + List notFoundFiles = []; + + ImageScanResult draftResult = await ScanUnusedImagesInternal(contents.Drafts, drafts); + ImageScanResult postResult = await ScanUnusedImagesInternal(contents.Posts, posts); + + unusedFiles.AddRange(draftResult.UnusedImages); + notFoundFiles.AddRange(draftResult.NotFoundImages); + unusedFiles.AddRange(postResult.UnusedImages); + notFoundFiles.AddRange(postResult.NotFoundImages); + + return new ImageScanResult(unusedFiles, notFoundFiles); + } + + private static Task ScanUnusedImagesInternal(IEnumerable contents, + DirectoryInfo root) + { + Regex imageRegex = ImageRegex(); + ConcurrentBag unusedImage = []; + ConcurrentBag notFoundImage = []; + + Parallel.ForEach(contents, content => + { + MatchCollection result = imageRegex.Matches(content.FileContent); + DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName)); + + Dictionary usedDictionary; + + if (imageDirectory.Exists) + { + usedDictionary = (from file in imageDirectory.EnumerateFiles() + select new KeyValuePair(file.FullName, false)).ToDictionary(); + } + else + { + usedDictionary = []; + } + + foreach (Match match in result) + { + string imageName = match.Groups[1].Value; + + FileInfo usedFile = imageName.Contains(content.FileName) + ? new FileInfo(Path.Combine(root.FullName, imageName)) + : new FileInfo(Path.Combine(root.FullName, content.FileName, imageName)); + + if (usedDictionary.TryGetValue(usedFile.FullName, out _)) + { + usedDictionary[usedFile.FullName] = true; + } + else + { + notFoundImage.Add(usedFile); + } + } + + foreach (KeyValuePair pair in usedDictionary.Where(p => !p.Value)) + { + unusedImage.Add(new FileInfo(pair.Key)); + } + }); + + return Task.FromResult(new ImageScanResult(unusedImage.ToList(), notFoundImage.ToList())); + } + + [GeneratedRegex(@"\!\[.*?\]\((.*?)\)")] + private static partial Regex ImageRegex(); + private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts) { root = Path.Combine(Environment.CurrentDirectory, root); diff --git a/YaeBlog/Commands/CommandExtensions.cs b/YaeBlog/Commands/CommandExtensions.cs index 0ea6ac6..bb7576d 100644 --- a/YaeBlog/Commands/CommandExtensions.cs +++ b/YaeBlog/Commands/CommandExtensions.cs @@ -81,6 +81,14 @@ public static class CommandExtensions newCommand.SetHandler(async (file, _, _, essayScanService) => { + BlogContents contents = await essayScanService.ScanContents(); + + if (contents.Posts.Any(content => content.FileName == file)) + { + Console.WriteLine("There exists the same title blog in posts."); + return; + } + await essayScanService.SaveBlogContent(new BlogContent { FileName = file, @@ -96,23 +104,113 @@ public static class CommandExtensions public static void AddListCommand(this RootCommand rootCommand) { Command command = new("list", "List all blogs"); - rootCommand.Add(command); + 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) + foreach (BlogContent content in contents.Posts.OrderBy(x => x.FileName)) { Console.WriteLine($" - {content.FileName}"); } Console.WriteLine($"All {contents.Drafts.Count} Drafts:"); - foreach (BlogContent content in contents.Drafts) + foreach (BlogContent content in contents.Drafts.OrderBy(x => x.FileName)) { Console.WriteLine($" - {content.FileName}"); } }, new BlogOptionsBinder(), new LoggerBinder(), new EssayScanServiceBinder()); } + + public static void AddScanCommand(this 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) => + { + ImageScanResult result = await essayScanService.ScanImages(); + + if (result.UnusedImages.Count != 0) + { + Console.WriteLine("Found unused images:"); + Console.WriteLine("HINT: use '--rm' to remove unused images."); + } + + foreach (FileInfo image in result.UnusedImages) + { + Console.WriteLine($" - {image.FullName}"); + } + + if (removeOptionValue) + { + foreach (FileInfo image in result.UnusedImages) + { + image.Delete(); + } + } + + Console.WriteLine("Used not existed images:"); + + foreach (FileInfo image in result.NotFoundImages) + { + Console.WriteLine($" - {image.FullName}"); + } + }, new BlogOptionsBinder(), new LoggerBinder(), new EssayScanServiceBinder(), removeOption); + } + + public static void AddPublishCommand(this 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.FileName == filename + select blog).FirstOrDefault(); + + if (content is null) + { + Console.WriteLine("Target blog does not exist."); + return; + } + + // 将选中的博客文件复制到posts + await essayScanService.SaveBlogContent(content, isDraft: false); + + // 复制图片文件夹 + DirectoryInfo sourceImageDirectory = + new(Path.Combine(blogOptions.Value.Root, "drafts", content.FileName)); + DirectoryInfo targetImageDirectory = + new(Path.Combine(blogOptions.Value.Root, "posts", content.FileName)); + + 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.FileName + ".md")); + sourceBlogFile.Delete(); + }, new BlogOptionsBinder(), + new LoggerBinder(), new EssayScanServiceBinder(), filenameArgument); + } } diff --git a/YaeBlog/Program.cs b/YaeBlog/Program.cs index 4cd3540..482e3c7 100644 --- a/YaeBlog/Program.cs +++ b/YaeBlog/Program.cs @@ -7,5 +7,7 @@ rootCommand.AddServeCommand(); rootCommand.AddNewCommand(); rootCommand.AddListCommand(); rootCommand.AddWatchCommand(); +rootCommand.AddScanCommand(); +rootCommand.AddPublishCommand(); await rootCommand.InvokeAsync(args); diff --git a/YaeBlog/source/drafts/test-essay.md b/YaeBlog/source/drafts/test-essay.md deleted file mode 100644 index 2eb5d88..0000000 --- a/YaeBlog/source/drafts/test-essay.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: test-essay -date: 2024-08-22T22:31:34.3177253+08:00 -tags: ---- - diff --git a/YaeBlog/source/posts/archlinux-sop.md b/YaeBlog/source/posts/archlinux-sop.md index 944feef..2a2a440 100644 --- a/YaeBlog/source/posts/archlinux-sop.md +++ b/YaeBlog/source/posts/archlinux-sop.md @@ -1,5 +1,5 @@ --- -title: 日用Linux挑战第五篇 ArchLinux标准安装流程 +title: 日用Linux挑战 第5篇 标准安装流程 date: 2024-7-16 20:08:37 tags: - Linux diff --git a/YaeBlog/source/posts/big-homework.md b/YaeBlog/source/posts/big-homework.md index 040cb05..e699423 100644 --- a/YaeBlog/source/posts/big-homework.md +++ b/YaeBlog/source/posts/big-homework.md @@ -44,7 +44,7 @@ date: 2022-07-27 11:34:49 而且采用 `Git`还有一个好处,采用 `Github`的 `Insight`功能可以轻松的看出大家的贡献值()。 -![img](1.png "贡献") +![img](1.png) ## 一些技术上的收获 diff --git a/YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-21-30-Screenshot_20230119_202008.png b/YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-21-30-Screenshot_20230119_202008.png deleted file mode 100644 index 1fbd658..0000000 --- a/YaeBlog/source/posts/compile-mediapipe/2023-01-19-20-21-30-Screenshot_20230119_202008.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:15b1e6bc0d87f46937650d98703403b0536a7a61f30477418ae03a8b6361c252 -size 26745 diff --git a/YaeBlog/source/posts/daily-linux-0.md b/YaeBlog/source/posts/daily-linux-0.md index b5bd2ff..413c1f3 100644 --- a/YaeBlog/source/posts/daily-linux-0.md +++ b/YaeBlog/source/posts/daily-linux-0.md @@ -1,5 +1,5 @@ --- -title: 日用Linux挑战 第0篇 +title: 日用Linux挑战 第0篇 初见Arch Linux tags: - Linux - 随笔 diff --git a/YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-11-Screenshot_20230112_132756.png b/YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-11-Screenshot_20230112_132756.png deleted file mode 100644 index 3715dd9..0000000 --- a/YaeBlog/source/posts/daily-linux-0/2023-01-12-13-28-11-Screenshot_20230112_132756.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8fec70fe79a7bfdac23725417d92df203673547191c8195c75416bd5fa615b11 -size 3580670 diff --git a/YaeBlog/source/posts/daily-linux-1.md b/YaeBlog/source/posts/daily-linux-1.md index abae00c..c280fc3 100644 --- a/YaeBlog/source/posts/daily-linux-1.md +++ b/YaeBlog/source/posts/daily-linux-1.md @@ -1,5 +1,5 @@ --- -title: 日用Linux挑战第1篇 +title: 日用Linux挑战 第1篇 问题与挑战 tags: - Linux - 随笔 @@ -45,7 +45,7 @@ date: 2023-03-08 22:37:29 简单的说,我不认为现在`Linux`已经准备好切换到`Wayland`下了。 -> 听说最精`Ubuntu 22.04`已经默认使用`Wayland`作为显示协议了,等我有了其他的电脑可以试一试,看看商业公司的加入能不能带来一点转机。 +> 听说最新的`Ubuntu 22.04`已经默认使用`Wayland`作为显示协议了,等我有了其他的电脑可以试一试,看看商业公司的加入能不能带来一点转机。 ## 使用中发现的问题 diff --git a/YaeBlog/source/posts/daily-linux-2.md b/YaeBlog/source/posts/daily-linux-2.md index be84541..04ebae4 100644 --- a/YaeBlog/source/posts/daily-linux-2.md +++ b/YaeBlog/source/posts/daily-linux-2.md @@ -1,5 +1,5 @@ --- -title: 日用Linux挑战 第2篇 +title: 日用Linux挑战 第2篇 Wayland tags: - 随笔 - Linux diff --git a/YaeBlog/source/posts/daily-linux-2/image-20230720202327220.png b/YaeBlog/source/posts/daily-linux-2/image-20230720202327220.png deleted file mode 100644 index 2875bea..0000000 --- a/YaeBlog/source/posts/daily-linux-2/image-20230720202327220.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9798a673e0da356e74874d4ac445f5a7920fa45662e688660da73dd72ac9f682 -size 405626 diff --git a/YaeBlog/source/posts/daily-linux-3.md b/YaeBlog/source/posts/daily-linux-3.md index b1a6160..bf8bde1 100644 --- a/YaeBlog/source/posts/daily-linux-3.md +++ b/YaeBlog/source/posts/daily-linux-3.md @@ -1,5 +1,5 @@ --- -title: 日用Linux挑战 第三篇 +title: 日用Linux挑战 第3篇 放弃Wayland tags: - 随笔 - Linux diff --git a/YaeBlog/source/posts/daily-linux-4.md b/YaeBlog/source/posts/daily-linux-4.md index 79366b8..83d9c35 100644 --- a/YaeBlog/source/posts/daily-linux-4.md +++ b/YaeBlog/source/posts/daily-linux-4.md @@ -1,5 +1,5 @@ --- -title: 日用Linux挑战 第四篇 +title: 日用Linux挑战 第4篇 新的开始 tags: - Linux - 随笔 diff --git a/YaeBlog/source/posts/parser-combinator.md b/YaeBlog/source/posts/parser-combinator.md index 661499a..baf2b98 100644 --- a/YaeBlog/source/posts/parser-combinator.md +++ b/YaeBlog/source/posts/parser-combinator.md @@ -44,7 +44,7 @@ bool ParserBoolValue(string input) } ``` -这就是一个**解析器(Parser)**的原型实现:输入需要识别的字符串,输出识别的结果。在这个原型方法中,我们可以很容易发现两个问题: +这就是一个 **解析器(Parser)** 的原型实现:输入需要识别的字符串,输出识别的结果。在这个原型方法中,我们可以很容易发现两个问题: 1. 解析器并不会**消耗**输入的字符串,缺少一个读取字符串的游标。 2. 解析器输出结果的方式并不高明,输出的结果只能是解析成功情况的结果,解析失败的情况需要通过抛出一个自定义的异常`InvalidInputException`,但是考虑到一个解析器解析失败是非常常见的情况,在这里使用异常方式返回错误结果会导致非常严重的性能问题。 @@ -64,7 +64,7 @@ public abstract class Parser 上面解析器基类中,类上的泛型参数`T`表示该解析器最终解析结果的类型,解析函数`Parse`的泛型参数`TState`是实现了输入状态`IReadState`的类型,返回的类型`ParseResult`就是上文中提到的解析结果基类。 -在设计完解析器之后,该谈一谈**组合子(Combinator)**了。实际上组合子就是将多个解析器组合到一起的一系列函数,输入一个或者多个解析器,输出一个合并之后的解析器。容易想到,各种解析器组合在一起的方式千千万万,但是实际上我们只需要实现一系列基本的组合子,就可以通过综合使用各种解析器和组合子将各种需要的解析器和组合子实现出现。实际上,这也是解析器组合子思想的集中体现,通过基础的“砖块”(解析器)和“水泥“(组合子)设计和实现各种构建,最终建造出宏伟的高楼。 +在设计完解析器之后,该谈一谈 **组合子(Combinator)** 了。实际上组合子就是将多个解析器组合到一起的一系列函数,输入一个或者多个解析器,输出一个合并之后的解析器。容易想到,各种解析器组合在一起的方式千千万万,但是实际上我们只需要实现一系列基本的组合子,就可以通过综合使用各种解析器和组合子将各种需要的解析器和组合子实现出现。实际上,这也是解析器组合子思想的集中体现,通过基础的“砖块”(解析器)和“水泥“(组合子)设计和实现各种构建,最终建造出宏伟的高楼。 基础解析器和组合子的选择因人而异,但是一个常见的组合是: diff --git a/YaeBlog/source/posts/vscode-in-browser.md b/YaeBlog/source/posts/vscode-in-browser.md index fabede1..a24b8af 100644 --- a/YaeBlog/source/posts/vscode-in-browser.md +++ b/YaeBlog/source/posts/vscode-in-browser.md @@ -55,7 +55,7 @@ cert: false 在进行了这些更改之后,我们再次输入code-server重启服务,如果一次顺利,我们可以看见以下的启动信息 ![启动信息](./vscode-in-browser/1.png) 我们可以打开浏览器,在地址栏中输入你的服务器公网IP加上你自己设置的端口号,就可以打开自己的VSCode Online界面了。 -![主界面](./vscode-in-browser/1.png) +![主界面](./vscode-in-browser/2.png) 输入自己的设置密码,就可以开始把浏览器中的VSCode当作自己本地计算机上的VSCode使用了,不过其中的文件是位于自己的服务器上的。 >如果你和我一样使用的阿里云的服务器,可能还需要到服务器的管理界面设置安全组放行相应的端口,具体参考[这篇文章](https://help.aliyun.com/document_detail/59086.html?spm=5176.10173289.help.dexternal.4ff02e77892BZP)