Skip to content

Commit 35532fa

Browse files
authored
v14.19.0 (#854)
2 parents 3f833e2 + 5071b1b commit 35532fa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+691
-234
lines changed

Deployment/mssql-migration.sql

+26
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,29 @@ IF EXISTS (
133133
BEGIN
134134
ALTER TABLE [dbo].[BlogConfiguration] DROP COLUMN Id;
135135
END
136+
137+
-- v14.19
138+
IF EXISTS (
139+
SELECT 1
140+
FROM INFORMATION_SCHEMA.COLUMNS
141+
WHERE TABLE_NAME = 'Post'
142+
AND COLUMN_NAME = 'Revision'
143+
)
144+
BEGIN
145+
ALTER TABLE [dbo].[Post] DROP COLUMN Revision;
146+
END
147+
148+
IF NOT EXISTS (SELECT * FROM sys.objects
149+
WHERE object_id = OBJECT_ID(N'[dbo].[PostView]') AND type in (N'U'))
150+
BEGIN
151+
CREATE TABLE [dbo].[PostView](
152+
[PostId] [uniqueidentifier] NOT NULL,
153+
[RequestCount] [int] NOT NULL,
154+
[ViewCount] [int] NOT NULL,
155+
[BeginTimeUtc] [datetime] NOT NULL,
156+
CONSTRAINT [PK_PostView] PRIMARY KEY CLUSTERED
157+
(
158+
[PostId] ASC
159+
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
160+
) ON [PRIMARY]
161+
END

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Moonglade is a personal blogging platform optimized for deployment on [**Microso
1010

1111
- **Deployment Options:** While Azure is recommended, Moonglade Blog can also be deployed on other cloud providers or on-premises environments.
1212

13+
- **Regulation:** Due to regulation concerns in China, Moonglade will run in read-only mode when deployed in China. If you are in China, please consider another blogging platform.
1314

1415
### Full Deploy on Azure
1516

src/Directory.Build.props

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<Authors>Edi Wang</Authors>
44
<Company>edi.wang</Company>
55
<Copyright>(C) 2025 [email protected]</Copyright>
6-
<AssemblyVersion>14.18.0</AssemblyVersion>
7-
<FileVersion>14.18.0</FileVersion>
8-
<Version>14.18.0</Version>
6+
<AssemblyVersion>14.19.0</AssemblyVersion>
7+
<FileVersion>14.19.0</FileVersion>
8+
<Version>14.19.0</Version>
99
</PropertyGroup>
1010
</Project>

src/Moonglade.Auth/Moonglade.Auth.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
</ItemGroup>
1717
<ItemGroup>
1818
<FrameworkReference Include="Microsoft.AspNetCore.App" />
19-
<PackageReference Include="Microsoft.Identity.Web" Version="3.6.1" />
19+
<PackageReference Include="Microsoft.Identity.Web" Version="3.6.2" />
2020
</ItemGroup>
2121
<ItemGroup>
2222
<ProjectReference Include="..\Moonglade.Configuration\Moonglade.Configuration.csproj" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using Microsoft.Extensions.Logging;
2+
using Moonglade.Data;
3+
using System.Collections.Concurrent;
4+
5+
namespace Moonglade.Core.PostFeature;
6+
7+
public record AddRequestCountCommand(Guid PostId) : IRequest<int>;
8+
9+
public class AddRequestCountCommandHandler(
10+
MoongladeRepository<PostViewEntity> postViewRepo,
11+
ILogger<AddRequestCountCommandHandler> logger) : IRequestHandler<AddRequestCountCommand, int>
12+
{
13+
// Ugly code to prevent race condition, which will make Moonglade a single instance application, shit
14+
private static readonly ConcurrentDictionary<Guid, SemaphoreSlim> _locks = new();
15+
16+
public async Task<int> Handle(AddRequestCountCommand request, CancellationToken cancellationToken)
17+
{
18+
var postLock = _locks.GetOrAdd(request.PostId, _ => new SemaphoreSlim(1, 1));
19+
await postLock.WaitAsync(cancellationToken);
20+
21+
try
22+
{
23+
var entity = await postViewRepo.GetByIdAsync(request.PostId, cancellationToken);
24+
if (entity is null)
25+
{
26+
entity = new PostViewEntity
27+
{
28+
PostId = request.PostId,
29+
RequestCount = 1,
30+
BeginTimeUtc = DateTime.UtcNow
31+
};
32+
33+
await postViewRepo.AddAsync(entity, cancellationToken);
34+
35+
logger.LogInformation("New request added for {PostId}", request.PostId);
36+
return 1;
37+
}
38+
39+
entity.RequestCount++;
40+
await postViewRepo.UpdateAsync(entity, cancellationToken);
41+
42+
logger.LogInformation("Request count updated for {PostId}, {RequestCount}", request.PostId, entity.RequestCount);
43+
44+
return entity.RequestCount;
45+
}
46+
catch (Exception ex)
47+
{
48+
// Not fatal error, eat it and do not block application from running
49+
logger.LogError(ex, "Failed to add request count for {PostId}", request.PostId);
50+
return -1;
51+
}
52+
finally
53+
{
54+
postLock.Release();
55+
56+
if (postLock.CurrentCount == 1)
57+
{
58+
_locks.TryRemove(request.PostId, out _);
59+
}
60+
}
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using Microsoft.Extensions.Logging;
2+
using Moonglade.Data;
3+
using System.Collections.Concurrent;
4+
5+
namespace Moonglade.Core.PostFeature;
6+
7+
public record AddViewCountCommand(Guid PostId, string Ip) : IRequest<int>;
8+
9+
public class AddViewCountCommandHandler(
10+
MoongladeRepository<PostViewEntity> postViewRepo,
11+
ILogger<AddViewCountCommandHandler> logger) : IRequestHandler<AddViewCountCommand, int>
12+
{
13+
// Ugly code to prevent race condition, which will make Moonglade a single instance application, shit
14+
private static readonly ConcurrentDictionary<Guid, SemaphoreSlim> _locks = new();
15+
16+
public async Task<int> Handle(AddViewCountCommand request, CancellationToken cancellationToken)
17+
{
18+
var postLock = _locks.GetOrAdd(request.PostId, _ => new SemaphoreSlim(1, 1));
19+
await postLock.WaitAsync(cancellationToken);
20+
21+
try
22+
{
23+
var entity = await postViewRepo.GetByIdAsync(request.PostId, cancellationToken);
24+
if (entity is null) return 0;
25+
26+
entity.ViewCount++;
27+
await postViewRepo.UpdateAsync(entity, cancellationToken);
28+
29+
logger.LogInformation("View count updated for {PostId}, {ViewCount}", request.PostId, entity.ViewCount);
30+
31+
return entity.ViewCount;
32+
}
33+
catch (Exception ex)
34+
{
35+
// Not fatal error, eat it and do not block application from running
36+
logger.LogError(ex, "Failed to add view count for {PostId}", request.PostId);
37+
return -1;
38+
}
39+
finally
40+
{
41+
postLock.Release();
42+
43+
if (postLock.CurrentCount == 1)
44+
{
45+
_locks.TryRemove(request.PostId, out _);
46+
}
47+
}
48+
}
49+
}

src/Moonglade.Core/PostFeature/CreatePostCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task<PostEntity> Handle(CreatePostCommand request, CancellationToke
2626
abs = ContentProcessor.GetPostAbstract(
2727
request.Payload.EditorContent,
2828
blogConfig.ContentSettings.PostAbstractWords,
29-
configuration.GetSection("Post:Editor").Get<EditorChoice>() == EditorChoice.Markdown);
29+
configuration.GetValue<EditorChoice>("Post:Editor") == EditorChoice.Markdown);
3030
}
3131
else
3232
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Moonglade.Data;
2+
3+
namespace Moonglade.Core.PostFeature;
4+
5+
public record GetPostViewQuery(Guid PostId) : IRequest<PostViewEntity>;
6+
7+
public class GetPostViewQueryHandler(MoongladeRepository<PostViewEntity> repo) : IRequestHandler<GetPostViewQuery, PostViewEntity>
8+
{
9+
public Task<PostViewEntity> Handle(GetPostViewQuery request, CancellationToken ct) => repo.GetByIdAsync(request.PostId, ct);
10+
}

src/Moonglade.Core/PostFeature/UpdatePostCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ private void UpdatePostDetails(PostEntity post, PostEditModel postEditModel, Dat
116116
? ContentProcessor.GetPostAbstract(
117117
postEditModel.EditorContent,
118118
_blogConfig.ContentSettings.PostAbstractWords,
119-
_configuration.GetSection("Post:Editor").Get<EditorChoice>() == EditorChoice.Markdown)
119+
_configuration.GetValue<EditorChoice>("Post:Editor") == EditorChoice.Markdown)
120120
: postEditModel.Abstract.Trim();
121121

122122
if (postEditModel.IsPublished && !post.IsPublished)

src/Moonglade.Data/BlogDbContext.cs

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public BlogDbContext(DbContextOptions options)
1919
public virtual DbSet<PostEntity> Post { get; set; }
2020
public virtual DbSet<PostCategoryEntity> PostCategory { get; set; }
2121
public virtual DbSet<PostTagEntity> PostTag { get; set; }
22+
public virtual DbSet<PostViewEntity> PostView { get; set; }
2223
public virtual DbSet<TagEntity> Tag { get; set; }
2324
public virtual DbSet<FriendLinkEntity> FriendLink { get; set; }
2425
public virtual DbSet<PageEntity> CustomPage { get; set; }
@@ -55,6 +56,7 @@ public static class BlogDbContextExtension
5556
{
5657
public static async Task ClearAllData(this BlogDbContext context)
5758
{
59+
await context.PostView.ExecuteDeleteAsync();
5860
await context.PostTag.ExecuteDeleteAsync();
5961
await context.PostCategory.ExecuteDeleteAsync();
6062
await context.CommentReply.ExecuteDeleteAsync();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace Moonglade.Data.Entities;
4+
5+
public class PostViewEntity
6+
{
7+
[Key]
8+
public Guid PostId { get; set; }
9+
10+
public int RequestCount { get; set; }
11+
12+
public int ViewCount { get; set; }
13+
14+
public DateTime BeginTimeUtc { get; set; }
15+
}

src/Moonglade.ImageStorage/IBlogImageStorage.cs

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ public interface IBlogImageStorage
66

77
Task<string> InsertAsync(string fileName, byte[] imageBytes);
88

9+
Task<string> InsertSecondaryAsync(string fileName, byte[] imageBytes);
10+
911
Task<ImageInfo> GetAsync(string fileName);
1012

1113
Task DeleteAsync(string fileName);
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
namespace Moonglade.ImageStorage.Providers;
22

3-
public class AzureBlobConfiguration(string connectionString, string containerName)
3+
public class AzureBlobConfiguration(string connectionString, string containerName, string secondaryContainerName = null)
44
{
55
public string ConnectionString { get; } = connectionString;
66

77
public string ContainerName { get; } = containerName;
8+
9+
public string SecondaryContainerName { get; } = secondaryContainerName;
810
}

0 commit comments

Comments
 (0)