feat: 图片扫描和发布命令 #5
|
@ -7,4 +7,6 @@ public interface IEssayScanService
|
||||||
public Task<BlogContents> ScanContents();
|
public Task<BlogContents> ScanContents();
|
||||||
|
|
||||||
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
|
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
|
||||||
|
|
||||||
|
public Task<ImageScanResult> ScanImages();
|
||||||
}
|
}
|
||||||
|
|
3
YaeBlog.Core/Models/ImageScanResult.cs
Normal file
3
YaeBlog.Core/Models/ImageScanResult.cs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
namespace YaeBlog.Core.Models;
|
||||||
|
|
||||||
|
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using YaeBlog.Core.Abstractions;
|
using YaeBlog.Core.Abstractions;
|
||||||
|
@ -9,7 +10,7 @@ using YamlDotNet.Serialization;
|
||||||
|
|
||||||
namespace YaeBlog.Core.Services;
|
namespace YaeBlog.Core.Services;
|
||||||
|
|
||||||
public class EssayScanService(
|
public partial class EssayScanService(
|
||||||
ISerializer yamlSerializer,
|
ISerializer yamlSerializer,
|
||||||
IDeserializer yamlDeserializer,
|
IDeserializer yamlDeserializer,
|
||||||
IOptions<BlogOptions> blogOptions,
|
IOptions<BlogOptions> blogOptions,
|
||||||
|
@ -96,6 +97,79 @@ public class EssayScanService(
|
||||||
return contents;
|
return contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ImageScanResult> ScanImages()
|
||||||
|
{
|
||||||
|
BlogContents contents = await ScanContents();
|
||||||
|
ValidateDirectory(_blogOptions.Root, out DirectoryInfo drafts, out DirectoryInfo posts);
|
||||||
|
|
||||||
|
List<FileInfo> unusedFiles = [];
|
||||||
|
List<FileInfo> 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<ImageScanResult> ScanUnusedImagesInternal(IEnumerable<BlogContent> contents,
|
||||||
|
DirectoryInfo root)
|
||||||
|
{
|
||||||
|
Regex imageRegex = ImageRegex();
|
||||||
|
ConcurrentBag<FileInfo> unusedImage = [];
|
||||||
|
ConcurrentBag<FileInfo> notFoundImage = [];
|
||||||
|
|
||||||
|
Parallel.ForEach(contents, content =>
|
||||||
|
{
|
||||||
|
MatchCollection result = imageRegex.Matches(content.FileContent);
|
||||||
|
DirectoryInfo imageDirectory = new(Path.Combine(root.FullName, content.FileName));
|
||||||
|
|
||||||
|
Dictionary<string, bool> usedDictionary;
|
||||||
|
|
||||||
|
if (imageDirectory.Exists)
|
||||||
|
{
|
||||||
|
usedDictionary = (from file in imageDirectory.EnumerateFiles()
|
||||||
|
select new KeyValuePair<string, bool>(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<string, bool> 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)
|
private void ValidateDirectory(string root, out DirectoryInfo drafts, out DirectoryInfo posts)
|
||||||
{
|
{
|
||||||
root = Path.Combine(Environment.CurrentDirectory, root);
|
root = Path.Combine(Environment.CurrentDirectory, root);
|
||||||
|
|
|
@ -96,7 +96,7 @@ public static class CommandExtensions
|
||||||
public static void AddListCommand(this RootCommand rootCommand)
|
public static void AddListCommand(this RootCommand rootCommand)
|
||||||
{
|
{
|
||||||
Command command = new("list", "List all blogs");
|
Command command = new("list", "List all blogs");
|
||||||
rootCommand.Add(command);
|
rootCommand.AddCommand(command);
|
||||||
|
|
||||||
command.SetHandler(async (_, _, essyScanService) =>
|
command.SetHandler(async (_, _, essyScanService) =>
|
||||||
{
|
{
|
||||||
|
@ -115,4 +115,45 @@ public static class CommandExtensions
|
||||||
}
|
}
|
||||||
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
}, new BlogOptionsBinder(), new LoggerBinder<EssayScanService>(), new EssayScanServiceBinder());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void AddScanCommand(this RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command command = new("scan", "Scan unused and not found images.");
|
||||||
|
rootCommand.AddCommand(command);
|
||||||
|
|
||||||
|
Option<bool> 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<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,5 +7,6 @@ rootCommand.AddServeCommand();
|
||||||
rootCommand.AddNewCommand();
|
rootCommand.AddNewCommand();
|
||||||
rootCommand.AddListCommand();
|
rootCommand.AddListCommand();
|
||||||
rootCommand.AddWatchCommand();
|
rootCommand.AddWatchCommand();
|
||||||
|
rootCommand.AddScanCommand();
|
||||||
|
|
||||||
await rootCommand.InvokeAsync(args);
|
await rootCommand.InvokeAsync(args);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user