feat: 图片扫描和发布命令 (#5)
All checks were successful
Build blog docker image / Build-Blog-Image (push) Successful in 1m26s

Reviewed-on: #5
This commit is contained in:
jackfiled 2024-08-25 15:38:58 +08:00
parent 9111affeec
commit 4085b0d99c
18 changed files with 208 additions and 31 deletions

View File

@ -7,4 +7,6 @@ public interface IEssayScanService
public Task<BlogContents> ScanContents();
public Task SaveBlogContent(BlogContent content, bool isDraft = true);
public Task<ImageScanResult> ScanImages();
}

View File

@ -0,0 +1,3 @@
namespace YaeBlog.Core.Models;
public record struct ImageScanResult(List<FileInfo> UnusedImages, List<FileInfo> NotFoundImages);

View File

@ -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> 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("<!--more-->\n");
if (isDraft)
{
await writer.WriteLineAsync("<!--more-->");
}
else
{
await writer.WriteAsync(content.FileContent);
}
}
private async Task<ConcurrentBag<BlogContent>> ScanContentsInternal(DirectoryInfo directory)
@ -96,6 +110,79 @@ public class EssayScanService(
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 static 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)
{
root = Path.Combine(Environment.CurrentDirectory, root);

View File

@ -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<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);
}
public static void AddPublishCommand(this RootCommand rootCommand)
{
Command command = new("publish", "Publish a new blog file.");
rootCommand.AddCommand(command);
Argument<string> 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<EssayScanService>(), new EssayScanServiceBinder(), filenameArgument);
}
}

View File

@ -7,5 +7,7 @@ rootCommand.AddServeCommand();
rootCommand.AddNewCommand();
rootCommand.AddListCommand();
rootCommand.AddWatchCommand();
rootCommand.AddScanCommand();
rootCommand.AddPublishCommand();
await rootCommand.InvokeAsync(args);

View File

@ -1,6 +0,0 @@
---
title: test-essay
date: 2024-08-22T22:31:34.3177253+08:00
tags:
---
<!--more-->

View File

@ -1,5 +1,5 @@
---
title: 日用Linux挑战第五篇 ArchLinux标准安装流程
title: 日用Linux挑战 第5篇 标准安装流程
date: 2024-7-16 20:08:37
tags:
- Linux

View File

@ -44,7 +44,7 @@ date: 2022-07-27 11:34:49
而且采用 `Git`还有一个好处,采用 `Github``Insight`功能可以轻松的看出大家的贡献值()。
![img](1.png "贡献")
![img](1.png)
## 一些技术上的收获

View File

@ -1,5 +1,5 @@
---
title: 日用Linux挑战 第0篇
title: 日用Linux挑战 第0篇 初见Arch Linux
tags:
- Linux
- 随笔

View File

@ -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`作为显示协议了,等我有了其他的电脑可以试一试,看看商业公司的加入能不能带来一点转机。
## 使用中发现的问题

View File

@ -1,5 +1,5 @@
---
title: 日用Linux挑战 第2篇
title: 日用Linux挑战 第2篇 Wayland
tags:
- 随笔
- Linux

Binary file not shown.

View File

@ -1,5 +1,5 @@
---
title: 日用Linux挑战 第三篇
title: 日用Linux挑战 第3篇 放弃Wayland
tags:
- 随笔
- Linux

View File

@ -1,5 +1,5 @@
---
title: 日用Linux挑战 第四篇
title: 日用Linux挑战 第4篇 新的开始
tags:
- Linux
- 随笔

View File

@ -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)