From 6ac162a124cfb5bafc1db3d59dd50853aaa817bd Mon Sep 17 00:00:00 2001 From: jackfiled Date: Sun, 25 Aug 2024 14:52:18 +0800 Subject: [PATCH] add: Scan unused and not existed image command. --- .../Abstractions/IEssayScanService.cs | 2 + YaeBlog.Core/Models/ImageScanResult.cs | 3 + YaeBlog.Core/Services/EssayScanService.cs | 76 ++++++++++++++++++- YaeBlog/Commands/CommandExtensions.cs | 43 ++++++++++- YaeBlog/Program.cs | 1 + 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 YaeBlog.Core/Models/ImageScanResult.cs 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..cff5b11 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, @@ -96,6 +97,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 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..18fc659 100644 --- a/YaeBlog/Commands/CommandExtensions.cs +++ b/YaeBlog/Commands/CommandExtensions.cs @@ -96,7 +96,7 @@ 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) => { @@ -115,4 +115,45 @@ public static class CommandExtensions } }, 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); + } } diff --git a/YaeBlog/Program.cs b/YaeBlog/Program.cs index 4cd3540..5bbf620 100644 --- a/YaeBlog/Program.cs +++ b/YaeBlog/Program.cs @@ -7,5 +7,6 @@ rootCommand.AddServeCommand(); rootCommand.AddNewCommand(); rootCommand.AddListCommand(); rootCommand.AddWatchCommand(); +rootCommand.AddScanCommand(); await rootCommand.InvokeAsync(args);