Compare commits
6 Commits
master
...
c4241f7a19
| Author | SHA1 | Date | |
|---|---|---|---|
|
c4241f7a19
|
|||
|
82ffb4d4e5
|
|||
|
4de644036f
|
|||
|
0d10946ec1
|
|||
|
a3791596da
|
|||
|
2be09b8319
|
@@ -8,19 +8,19 @@ jobs:
|
|||||||
runs-on: archlinux
|
runs-on: archlinux
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code.
|
- name: Check out code.
|
||||||
uses: http://github-mirrors.infra.svc.cluster.local/actions/checkout.git@v4
|
uses: https://mirrors.rrricardo.top/actions/checkout.git@v4
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
- name: Build project.
|
- name: Build project.
|
||||||
run: |
|
run: |
|
||||||
git submodule update --init
|
|
||||||
podman pull mcr.azure.cn/dotnet/aspnet:10.0
|
podman pull mcr.azure.cn/dotnet/aspnet:10.0
|
||||||
|
cd YaeBlog
|
||||||
pwsh build.ps1 build
|
pwsh build.ps1 build
|
||||||
- name: Workaround to make sure podman-login working.
|
- name: Workaround to make sure podman-login working.
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /root/.docker
|
mkdir /root/.docker
|
||||||
- name: Login tencent cloud docker registry.
|
- name: Login tencent cloud docker registry.
|
||||||
uses: http://github-mirrors.infra.svc.cluster.local/actions/podman-login.git@v1
|
uses: https://mirrors.rrricardo.top/actions/podman-login.git@v1
|
||||||
with:
|
with:
|
||||||
registry: ccr.ccs.tencentyun.com
|
registry: ccr.ccs.tencentyun.com
|
||||||
username: 100044380877
|
username: 100044380877
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -184,7 +184,6 @@ DocProject/Help/html
|
|||||||
|
|
||||||
# Click-Once directory
|
# Click-Once directory
|
||||||
publish/
|
publish/
|
||||||
out/
|
|
||||||
|
|
||||||
# Publish Web Output
|
# Publish Web Output
|
||||||
*.[Pp]ublish.xml
|
*.[Pp]ublish.xml
|
||||||
|
|||||||
17
build.ps1
17
build.ps1
@@ -3,15 +3,16 @@
|
|||||||
[cmdletbinding()]
|
[cmdletbinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the build target")]
|
[Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the build target")]
|
||||||
[ValidateSet("publish", "compress", "build", "dev", "new", "watch", "serve")]
|
[ValidateSet("tailwind", "publish", "compress", "build", "dev", "new", "watch", "serve", "list")]
|
||||||
[string]$Target,
|
[string]$Target,
|
||||||
|
[string]$Output = "wwwroot",
|
||||||
[string]$Essay,
|
[string]$Essay,
|
||||||
[switch]$Compress,
|
[switch]$Compress,
|
||||||
[string]$Root = "source"
|
[string]$Root = "source"
|
||||||
)
|
)
|
||||||
|
|
||||||
begin {
|
begin {
|
||||||
if (($Target -eq "tailwind") -or ($Target -eq "build"))
|
if ($Target -eq "tailwind")
|
||||||
{
|
{
|
||||||
# Handle tailwind specially.
|
# Handle tailwind specially.
|
||||||
return
|
return
|
||||||
@@ -81,11 +82,8 @@ process {
|
|||||||
function Build-Image
|
function Build-Image
|
||||||
{
|
{
|
||||||
$commitId = git rev-parse --short=10 HEAD
|
$commitId = git rev-parse --short=10 HEAD
|
||||||
dotnet publish ./src/YaeBlog/YaeBlog.csproj -o out
|
dotnet publish
|
||||||
Write-Host "Succeed to build blog appliocation."
|
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId
|
||||||
podman build . -t ccr.ccs.tencentyun.com/jackfiled/blog --build-arg COMMIT_ID=$commitId `
|
|
||||||
-f ./src/YaeBlog/Dockerfile
|
|
||||||
Write-Host "Succeed to build ccr.ccs.tencentyun.com/jackfiled/blog image."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Start-Develop {
|
function Start-Develop {
|
||||||
@@ -113,6 +111,11 @@ process {
|
|||||||
|
|
||||||
switch ($Target)
|
switch ($Target)
|
||||||
{
|
{
|
||||||
|
"tailwind" {
|
||||||
|
Write-Host "Build tailwind css into $Output."
|
||||||
|
pnpm tailwindcss -i wwwroot/tailwind.css -o $Output/tailwind.g.css
|
||||||
|
break
|
||||||
|
}
|
||||||
"publish" {
|
"publish" {
|
||||||
Write-Host "Publish essay $Essay..."
|
Write-Host "Publish essay $Essay..."
|
||||||
dotnet run -- publish $Essay
|
dotnet run -- publish $Essay
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using DotNext;
|
using DotNext;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
using YaeBlog.Models;
|
using YaeBlog.Models;
|
||||||
@@ -10,7 +9,6 @@ namespace YaeBlog.Tests;
|
|||||||
public sealed class GiteaFetchServiceTests
|
public sealed class GiteaFetchServiceTests
|
||||||
{
|
{
|
||||||
private static readonly Mock<IOptions<GiteaOptions>> s_giteaOptionsMock = new();
|
private static readonly Mock<IOptions<GiteaOptions>> s_giteaOptionsMock = new();
|
||||||
private static readonly Mock<ILogger<GiteaFetchService>> s_logger = new();
|
|
||||||
private readonly GiteaFetchService _giteaFetchService;
|
private readonly GiteaFetchService _giteaFetchService;
|
||||||
|
|
||||||
public GiteaFetchServiceTests()
|
public GiteaFetchServiceTests()
|
||||||
@@ -18,10 +16,12 @@ public sealed class GiteaFetchServiceTests
|
|||||||
s_giteaOptionsMock.SetupGet(o => o.Value)
|
s_giteaOptionsMock.SetupGet(o => o.Value)
|
||||||
.Returns(new GiteaOptions
|
.Returns(new GiteaOptions
|
||||||
{
|
{
|
||||||
BaseAddress = "https://git.rrricardo.top/api/v1/", HeatMapUsername = "jackfiled"
|
BaseAddress = "https://git.rrricardo.top/api/v1/",
|
||||||
|
ApiKey = "7e33617e5d084199332fceec3e0cb04c6ddced55",
|
||||||
|
HeatMapUsername = "jackfiled"
|
||||||
});
|
});
|
||||||
|
|
||||||
_giteaFetchService = new GiteaFetchService(s_giteaOptionsMock.Object, new HttpClient(), s_logger.Object);
|
_giteaFetchService = new GiteaFetchService(s_giteaOptionsMock.Object, new HttpClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
33
src/YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
33
src/YaeBlog/Commands/Binders/BlogOptionsBinder.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using YaeBlog.Models;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class BlogOptionsBinder : BinderBase<IOptions<BlogOptions>>
|
||||||
|
{
|
||||||
|
protected override IOptions<BlogOptions> GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService<IOptions<BlogOptions>>(_ =>
|
||||||
|
{
|
||||||
|
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<BlogOptions>();
|
||||||
|
return result is null
|
||||||
|
? throw new InvalidOperationException("Failed to load YaeBlog configuration in appsettings.json.")
|
||||||
|
: new OptionsWrapper<BlogOptions>(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<IOptions<BlogOptions>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
32
src/YaeBlog/Commands/Binders/EssayScanServiceBinder.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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<IEssayScanService>
|
||||||
|
{
|
||||||
|
protected override IEssayScanService GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService<IEssayScanService>(provider =>
|
||||||
|
{
|
||||||
|
DeserializerBuilder deserializerBuilder = new();
|
||||||
|
deserializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
deserializerBuilder.IgnoreUnmatchedProperties();
|
||||||
|
|
||||||
|
SerializerBuilder serializerBuilder = new();
|
||||||
|
serializerBuilder.WithNamingConvention(CamelCaseNamingConvention.Instance);
|
||||||
|
|
||||||
|
IOptions<BlogOptions> options = provider.GetRequiredService<IOptions<BlogOptions>>();
|
||||||
|
ILogger<EssayScanService> logger = provider.GetRequiredService<ILogger<EssayScanService>>();
|
||||||
|
|
||||||
|
return new EssayScanService(serializerBuilder.Build(), deserializerBuilder.Build(), options, logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<IEssayScanService>();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs
Normal file
21
src/YaeBlog/Commands/Binders/ImageCompressServiceBinder.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
using YaeBlog.Abstraction;
|
||||||
|
using YaeBlog.Services;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class ImageCompressServiceBinder : BinderBase<ImageCompressService>
|
||||||
|
{
|
||||||
|
protected override ImageCompressService GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService(provider =>
|
||||||
|
{
|
||||||
|
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
|
||||||
|
ILogger<ImageCompressService> logger = provider.GetRequiredService<ILogger<ImageCompressService>>();
|
||||||
|
|
||||||
|
return new ImageCompressService(essayScanService, logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<ImageCompressService>();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
18
src/YaeBlog/Commands/Binders/LoggerBinder.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.CommandLine.Binding;
|
||||||
|
|
||||||
|
namespace YaeBlog.Commands.Binders;
|
||||||
|
|
||||||
|
public sealed class LoggerBinder<T> : BinderBase<ILogger<T>>
|
||||||
|
{
|
||||||
|
protected override ILogger<T> GetBoundValue(BindingContext bindingContext)
|
||||||
|
{
|
||||||
|
bindingContext.AddService(_ => LoggerFactory.Create(builder => builder.AddConsole()));
|
||||||
|
bindingContext.AddService<ILogger<T>>(provider =>
|
||||||
|
{
|
||||||
|
ILoggerFactory factory = provider.GetRequiredService<ILoggerFactory>();
|
||||||
|
return factory.CreateLogger<T>();
|
||||||
|
});
|
||||||
|
|
||||||
|
return bindingContext.GetRequiredService<ILogger<T>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
296
src/YaeBlog/Commands/YaeBlogCommand.cs
Normal file
296
src/YaeBlog/Commands/YaeBlogCommand.cs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
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<int> 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<App>()
|
||||||
|
.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<App>()
|
||||||
|
.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<string> 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<EssayScanService>(),
|
||||||
|
new EssayScanServiceBinder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddUpdateCommand(RootCommand rootCommand)
|
||||||
|
{
|
||||||
|
Command newCommand = new("update", "Update the blog essay.");
|
||||||
|
rootCommand.AddCommand(newCommand);
|
||||||
|
|
||||||
|
Argument<string> 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<EssayScanService>(), 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<EssayScanService>(), new EssayScanServiceBinder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddScanCommand(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) =>
|
||||||
|
{
|
||||||
|
BlogContents contents = await essayScanService.ScanContents();
|
||||||
|
List<BlogImageInfo> 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<EssayScanService>(), new EssayScanServiceBinder(), removeOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddPublishCommand(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.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<EssayScanService>(), 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<bool> 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<EssayScanService>(), new LoggerBinder<ImageCompressService>(),
|
||||||
|
new EssayScanServiceBinder(), new ImageCompressServiceBinder(), dryRunOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ImageCommandHandler(IOptions<BlogOptions> _, ILogger<EssayScanService> _1,
|
||||||
|
ILogger<ImageCompressService> _2,
|
||||||
|
IEssayScanService _3, ImageCompressService imageCompressService, bool dryRun)
|
||||||
|
{
|
||||||
|
await imageCompressService.Compress(dryRun);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,10 @@
|
|||||||
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
|
<Anchor Address="https://dotnet.microsoft.com" Text="@DotnetVersion"/>
|
||||||
驱动。
|
驱动。
|
||||||
</p>
|
</p>
|
||||||
@if (!string.IsNullOrEmpty(BuildCommitId))
|
<p class="text-md">
|
||||||
{
|
Build Commit #
|
||||||
<p class="text-md">
|
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId"/>
|
||||||
Build Commit #
|
</p>
|
||||||
<Anchor Address="@BuildCommitUrl" Text="@BuildCommitId" NewPage="true"/>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -27,7 +24,7 @@
|
|||||||
{
|
{
|
||||||
private static string DotnetVersion => $".NET {Environment.Version}";
|
private static string DotnetVersion => $".NET {Environment.Version}";
|
||||||
|
|
||||||
private static string? BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID");
|
private static string BuildCommitId => Environment.GetEnvironmentVariable("COMMIT_ID") ?? "local_build";
|
||||||
|
|
||||||
private static string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
|
private static string BuildCommitUrl => $"https://git.rrricardo.top/jackfiled/YaeBlog/commit/{BuildCommitId}";
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
<SvgGroup Transform="@GlobalMonthTransform">
|
<SvgGroup Transform="@GlobalMonthTransform">
|
||||||
@foreach ((int i, string text) in _monthIndices)
|
@foreach ((int i, string text) in _monthIndices)
|
||||||
{
|
{
|
||||||
<SvgText Content="@text" Transform="@(MonthTextTransform(i))" Class="text-[8px] font-light"/>
|
<SvgText Content="@text" Transform="@(MonthTextTransform(i))" Class="text-[10px]"/>
|
||||||
}
|
}
|
||||||
</SvgGroup>
|
</SvgGroup>
|
||||||
<SvgGroup Transform="@GlobalWeekTransform">
|
<SvgGroup Transform="@GlobalWeekTransform">
|
||||||
@foreach ((int i, string text) in Weekdays.Index())
|
@foreach ((int i, string text) in Weekdays.Index())
|
||||||
{
|
{
|
||||||
<SvgText Content="@text" Transform="@(DayTextTransform(i))" Class="text-[8px] font-light"/>
|
<SvgText Content="@text" Transform="@(DayTextTransform(i))" Class="text-[10px]"/>
|
||||||
}
|
}
|
||||||
</SvgGroup>
|
</SvgGroup>
|
||||||
<SvgGroup Transform="@GlobalMapTransform">
|
<SvgGroup Transform="@GlobalMapTransform">
|
||||||
@@ -23,8 +23,7 @@
|
|||||||
@foreach ((int j, GitContributionItem item) in contribution.Contributions.Index())
|
@foreach ((int j, GitContributionItem item) in contribution.Contributions.Index())
|
||||||
{
|
{
|
||||||
<Rectangle Width="@Width" Height="@Width" Transform="@(WeekdayGridTransform(j))"
|
<Rectangle Width="@Width" Height="@Width" Transform="@(WeekdayGridTransform(j))"
|
||||||
Class="@(GetColorByContribution(item.ContributionCount))"
|
Class="@(GetColorByContribution(item.ContributionCount))"/>
|
||||||
Id="@(item.ItemId)"/>
|
|
||||||
}
|
}
|
||||||
</SvgGroup>
|
</SvgGroup>
|
||||||
}
|
}
|
||||||
@@ -119,4 +118,5 @@
|
|||||||
_ => "fill-blue-800"
|
_ => "fill-blue-800"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,5 @@
|
|||||||
@Body
|
@Body
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer/>
|
<Foonter/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -32,5 +32,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer/>
|
<Foonter/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl page-starter">关于</h1>
|
<h1 class="text-4xl">关于</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl page-starter">归档</h1>
|
<h1 class="text-4xl">归档</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="page-starter"></div>
|
|
||||||
<div class="grid grid-cols-4">
|
<div class="grid grid-cols-4">
|
||||||
<div class="col-span-4 md:col-span-3">
|
<div class="col-span-4 md:col-span-3">
|
||||||
@foreach (BlogEssay essay in _essays)
|
@foreach (BlogEssay essay in _essays)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 id="title" class="text-4xl page-starter">@(_essay!.Title)</h1>
|
<h1 id="title" class="text-4xl">@(_essay!.Title)</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-4 py-2">
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl page-starter">
|
<h1 class="text-4xl">
|
||||||
友链
|
友链
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="col-span-3 md:col-span-2">
|
<div class="col-span-3 md:col-span-2">
|
||||||
<div class="flex flex-col gap-y-3 items-center md:items-start md:px-6">
|
<div class="flex flex-col gap-y-3 items-center md:items-start md:px-6">
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="text-3xl font-bold page-starter">初冬的朝阳</div>
|
<div class="text-3xl font-bold">初冬的朝阳</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-3xl page-starter">NotFound!</h3>
|
<h3 class="text-3xl">NotFound!</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="page-starter">
|
<div>
|
||||||
@if (TagName is null)
|
@if (TagName is null)
|
||||||
{
|
{
|
||||||
<h1 class="text-4xl">标签</h1>
|
<h1 class="text-4xl">标签</h1>
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"></RouteView>
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)"></RouteView>
|
||||||
}
|
}
|
||||||
|
|
||||||
<FocusOnNavigate RouteData="routeData" Selector="page-starter"/>
|
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||||
</Found>
|
</Found>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ ARG COMMIT_ID
|
|||||||
ENV COMMIT_ID=${COMMIT_ID}
|
ENV COMMIT_ID=${COMMIT_ID}
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY out/ ./
|
COPY bin/Release/net10.0/publish/ ./
|
||||||
COPY source/ ./source/
|
COPY source/ ./source/
|
||||||
COPY src/YaeBlog/appsettings.json .
|
COPY appsettings.json .
|
||||||
|
|
||||||
ENV BLOG__ROOT="./source"
|
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"]
|
ENTRYPOINT ["dotnet", "YaeBlog.dll", "serve"]
|
||||||
|
|||||||
@@ -18,17 +18,5 @@ public static class DateOnlyExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int DayNumberOfWeek
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return date.DayOfWeek switch
|
|
||||||
{
|
|
||||||
DayOfWeek.Sunday => 7,
|
|
||||||
_ => (int)date.DayOfWeek + 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using YaeBlog.Abstraction;
|
using YaeBlog.Abstraction;
|
||||||
using YaeBlog.Services;
|
using YaeBlog.Services;
|
||||||
using YaeBlog.Models;
|
using YaeBlog.Models;
|
||||||
@@ -28,25 +27,22 @@ public static class WebApplicationBuilderExtensions
|
|||||||
.AddTransient<HeadlinePostRenderProcessor>()
|
.AddTransient<HeadlinePostRenderProcessor>()
|
||||||
.AddTransient<EssayStylesPostRenderProcessor>()
|
.AddTransient<EssayStylesPostRenderProcessor>()
|
||||||
.AddTransient<GiteaFetchService>()
|
.AddTransient<GiteaFetchService>()
|
||||||
.AddTransient<BlogChangeWatcher>()
|
|
||||||
.AddTransient<BlogHotReloadService>()
|
|
||||||
.AddSingleton<GitHeapMapService>();
|
.AddSingleton<GitHeapMapService>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebApplicationBuilder AddYaeCommand(string[] arguments)
|
public WebApplicationBuilder AddServer()
|
||||||
{
|
{
|
||||||
builder.Services.AddHostedService<YaeCommandService>(provider =>
|
builder.Services.AddHostedService<BlogHostedService>();
|
||||||
{
|
|
||||||
IEssayScanService essayScanService = provider.GetRequiredService<IEssayScanService>();
|
|
||||||
IOptions<BlogOptions> blogOptions = provider.GetRequiredService<IOptions<BlogOptions>>();
|
|
||||||
ILogger<YaeCommandService> logger = provider.GetRequiredService<ILogger<YaeCommandService>>();
|
|
||||||
IHostApplicationLifetime applicationLifetime = provider.GetRequiredService<IHostApplicationLifetime>();
|
|
||||||
|
|
||||||
return new YaeCommandService(arguments, essayScanService, provider, blogOptions, logger,
|
return builder;
|
||||||
applicationLifetime);
|
}
|
||||||
});
|
|
||||||
|
public WebApplicationBuilder AddWatcher()
|
||||||
|
{
|
||||||
|
builder.Services.AddTransient<BlogChangeWatcher>();
|
||||||
|
builder.Services.AddHostedService<BlogHotReloadService>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ public class GiteaOptions
|
|||||||
|
|
||||||
[Required] public required string BaseAddress { get; init; }
|
[Required] public required string BaseAddress { get; init; }
|
||||||
|
|
||||||
public string? ApiKey { get; init; }
|
[Required] public required string ApiKey { get; init; }
|
||||||
|
|
||||||
[Required] public required string HeatMapUsername { get; init; }
|
[Required] public required string HeatMapUsername { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
namespace YaeBlog.Models;
|
namespace YaeBlog.Models;
|
||||||
|
|
||||||
public record GitContributionItem(DateOnly Time, long ContributionCount)
|
public record GitContributionItem(DateOnly Time, long ContributionCount);
|
||||||
{
|
|
||||||
public string ItemId => $"item-{Time:yyyy-MM-dd}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public record GitContributionGroupedByWeek(DateOnly Monday, List<GitContributionItem> Contributions);
|
public record GitContributionGroupedByWeek(DateOnly Monday, List<GitContributionItem> Contributions);
|
||||||
|
|||||||
@@ -1,22 +1,4 @@
|
|||||||
using YaeBlog.Components;
|
using YaeBlog.Commands;
|
||||||
using YaeBlog.Extensions;
|
|
||||||
|
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
YaeBlogCommand command = new();
|
||||||
|
await command.RunAsync(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<App>()
|
|
||||||
.AddInteractiveServerRenderMode();
|
|
||||||
application.MapControllers();
|
|
||||||
|
|
||||||
await application.RunAsync();
|
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ using YaeBlog.Models;
|
|||||||
|
|
||||||
namespace YaeBlog.Services;
|
namespace YaeBlog.Services;
|
||||||
|
|
||||||
public sealed class GitHeapMapService(
|
public sealed class GitHeapMapService(IServiceProvider serviceProvider, IOptions<GiteaOptions> giteaOptions,
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
IOptions<GiteaOptions> giteaOptions,
|
|
||||||
ILogger<GitHeapMapService> logger)
|
ILogger<GitHeapMapService> logger)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -85,24 +83,7 @@ public sealed class GitHeapMapService(
|
|||||||
groupedContribution.Contributions.Add(new GitContributionItem(date, contributions));
|
groupedContribution.Contributions.Add(new GitContributionItem(date, contributions));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the last contributing day is not today, fill the spacing.
|
// Not fill the last item and add directly.
|
||||||
// But be careful here! If the last grouped contribution is current week, just fill the spacing until today.
|
|
||||||
// If the last grouped contribution is before current week, first fill the blank week then fill until today.
|
|
||||||
while (groupedContribution.Monday < today.LastMonday)
|
|
||||||
{
|
|
||||||
FillSpacing(groupedContribution, today);
|
|
||||||
result.Add(groupedContribution);
|
|
||||||
groupedContribution = new GitContributionGroupedByWeek(groupedContribution.Monday.AddDays(7), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Currently the grouped contribution must be current week.
|
|
||||||
for (DateOnly date = groupedContribution.Monday.AddDays(groupedContribution.Contributions.Count);
|
|
||||||
date <= today;
|
|
||||||
date = date.AddDays(1))
|
|
||||||
{
|
|
||||||
groupedContribution.Contributions.Add(new GitContributionItem(date, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(groupedContribution);
|
result.Add(groupedContribution);
|
||||||
|
|
||||||
_gitContributionsGroupedByWeek = result;
|
_gitContributionsGroupedByWeek = result;
|
||||||
|
|||||||
@@ -10,38 +10,27 @@ namespace YaeBlog.Services;
|
|||||||
public sealed class GiteaFetchService
|
public sealed class GiteaFetchService
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<GiteaFetchService> _logger;
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions s_serializerOptions = new()
|
private static readonly JsonSerializerOptions s_serializerOptions = new()
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
RespectRequiredConstructorParameters = true, RespectNullableAnnotations = true
|
||||||
RespectRequiredConstructorParameters = true,
|
|
||||||
RespectNullableAnnotations = true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For test only.
|
/// For test only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal GiteaFetchService(IOptions<GiteaOptions> giteaOptions, HttpClient httpClient,
|
internal GiteaFetchService(IOptions<GiteaOptions> giteaOptions, HttpClient httpClient)
|
||||||
ILogger<GiteaFetchService> logger)
|
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
_httpClient.BaseAddress = new Uri(giteaOptions.Value.BaseAddress);
|
_httpClient.BaseAddress = new Uri(giteaOptions.Value.BaseAddress);
|
||||||
if (string.IsNullOrWhiteSpace(giteaOptions.Value.ApiKey))
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
{
|
new AuthenticationHeaderValue("Bearer", giteaOptions.Value.ApiKey);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Api Token is set.");
|
|
||||||
httpClient.DefaultRequestHeaders.Authorization =
|
|
||||||
new AuthenticationHeaderValue("token", giteaOptions.Value.ApiKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GiteaFetchService(IOptions<GiteaOptions> giteaOptions, IHttpClientFactory httpClientFactory,
|
public GiteaFetchService(IOptions<GiteaOptions> giteaOptions, IHttpClientFactory httpClientFactory) : this(
|
||||||
ILogger<GiteaFetchService> logger) : this(giteaOptions, httpClientFactory.CreateClient(), logger)
|
giteaOptions, httpClientFactory.CreateClient())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +50,6 @@ public sealed class GiteaFetchService
|
|||||||
new GiteaFetchException("Failed to fetch valid data."));
|
new GiteaFetchException("Failed to fetch valid data."));
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Fetch new user heat map data.");
|
|
||||||
return Result.FromValue(data.Select(i =>
|
return Result.FromValue(data.Select(i =>
|
||||||
new GitContributionItem(DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(i.Timestamp).DateTime),
|
new GitContributionItem(DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(i.Timestamp).DateTime),
|
||||||
i.Contributions)).ToList());
|
i.Contributions)).ToList());
|
||||||
|
|||||||
@@ -1,281 +0,0 @@
|
|||||||
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> blogOptions,
|
|
||||||
ILogger<YaeCommandService> 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<RendererService>();
|
|
||||||
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<BlogHotReloadService>();
|
|
||||||
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<string> 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<string> 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<bool> 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<BlogImageInfo> 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<string> 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<bool> 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<ImageCompressService>();
|
|
||||||
await imageCompressService.Compress(dryRun);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,6 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ClientAssetsRestoreCommand>pnpm install</ClientAssetsRestoreCommand>
|
<ClientAssetsRestoreCommand>pnpm install</ClientAssetsRestoreCommand>
|
||||||
<ClientAssetsBuildCommand>pwsh tailwind.ps1</ClientAssetsBuildCommand>
|
<ClientAssetsBuildCommand>pwsh ../../build.ps1 tailwind</ClientAssetsBuildCommand>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"Links": [
|
"Links": [
|
||||||
{
|
{
|
||||||
"Name": "Ichirinko",
|
"Name": "Ichirinko",
|
||||||
"Description": "黑历史集合地,naive的代价",
|
"Description": "这是个大哥",
|
||||||
"Link": "https://ichirinko.top",
|
"Link": "https://ichirinko.top",
|
||||||
"AvatarImage": "https://ichirinko-blog-img-1.oss-cn-shenzhen.aliyuncs.com/Pic_res/img/202209122110798.png"
|
"AvatarImage": "https://ichirinko-blog-img-1.oss-cn-shenzhen.aliyuncs.com/Pic_res/img/202209122110798.png"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user