diff options
Diffstat (limited to 'back-end/src/Model/PostManager.cs')
-rw-r--r-- | back-end/src/Model/PostManager.cs | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/back-end/src/Model/PostManager.cs b/back-end/src/Model/PostManager.cs new file mode 100644 index 0000000..22e3682 --- /dev/null +++ b/back-end/src/Model/PostManager.cs @@ -0,0 +1,232 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: CMNext +* Package: Content.Publishing.Blog.Admin +* File: PostManager.cs +* +* CMNext is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* CMNext is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Minio; +using Minio.DataModel.Tracing; + +using VNLib.Hashing; +using VNLib.Utils.IO; +using VNLib.Utils.Logging; +using VNLib.Net.Http; +using VNLib.Plugins; +using VNLib.Plugins.Extensions.Loading; + +using Content.Publishing.Blog.Admin.Storage; + +namespace Content.Publishing.Blog.Admin.Model +{ + + internal sealed class PostManager : IBlogPostManager + { + private readonly IStorageFacade Storage; + private readonly IRssFeedGenerator FeedGenerator; + private readonly ContentManager ContentMan; + + public PostManager(PluginBase plugin) + { + //Get minio client + Storage = plugin.GetOrCreateSingleton<ManagedStorage>(); + + //Get feed generator + FeedGenerator = plugin.GetOrCreateSingleton<FeedGenerator>(); + + //Get content manager + ContentMan = plugin.GetOrCreateSingleton<ContentManager>(); + } + + ///<inheritdoc/> + public async Task<PostMeta?> GetPostAsync(IChannelContext context, string postId, CancellationToken cancellation) + { + _ = context ?? throw new ArgumentNullException(nameof(context)); + _ = postId ?? throw new ArgumentNullException(nameof(postId)); + + //Read the index into memory + IRecordDb<PostMeta> db = await GetPostIndexAsync(context, cancellation); + + //Get the post meta + return db.GetRecord(postId); + } + + ///<inheritdoc/> + public async Task<PostMeta[]> GetPostsAsync(IChannelContext context, CancellationToken cancellation) + { + _ = context ?? throw new ArgumentNullException(nameof(context)); + + //Read the index into memory + IRecordDb<PostMeta> db = await GetPostIndexAsync(context, cancellation); + + //Return post metas + return db.GetRecords().ToArray(); + } + + ///<inheritdoc/> + public async Task PublishPostAsync(IChannelContext context, PostMeta post, CancellationToken cancellation) + { + _ = context ?? throw new ArgumentNullException(nameof(context)); + _ = post ?? throw new ArgumentNullException(nameof(post)); + + //Read the index into memory + IRecordDb<PostMeta> db = await GetPostIndexAsync(context, cancellation); + + //Update index modifed time and post date + post.Date = post.Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + //Compute post id before publishing to storage + ComputePostId(post); + + //Add post to the index + db.SetRecord(post); + + //Update the index + await SetPostIndexAsync(context, db, cancellation); + + //Create empty post content + await ContentMan.CreateNewPostContent(context, post.Id!, cancellation); + } + + ///<inheritdoc/> + public async Task DeletePostAsync(IChannelContext context, string postId, CancellationToken cancellation) + { + _ = context ?? throw new ArgumentNullException(nameof(context)); + _ = postId ?? throw new ArgumentNullException(nameof(postId)); + + //Get the index + IRecordDb<PostMeta> db = await GetPostIndexAsync(context, cancellation); + + //Remove the post from the index if it exists + PostMeta? post = db.GetRecord(postId); + + if (post == null) + { + return; + } + + db.RemoveRecord(postId); + + //Remove post content before flushing db changes + await ContentMan.DeleteContentAsync(context, postId, cancellation); + + //update feed after post deletion + await UpdateIndexAndFeed(context, db, cancellation); + } + + ///<inheritdoc/> + public async Task<bool> UpdatePostAsync(IChannelContext context, PostMeta post, CancellationToken cancellation) + { + _ = context ?? throw new ArgumentNullException(nameof(context)); + _ = post?.Id ?? throw new ArgumentNullException(nameof(post)); + + //Get the index + IRecordDb<PostMeta> db = await GetPostIndexAsync(context, cancellation); + + //Try to get the post by its id + PostMeta? oldMeta = db.GetRecord(post.Id); + + if (oldMeta == null) + { + return false; + } + + //Update modified time + post.Date = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + //Save old time + post.Created = oldMeta.Created; + + //Remove the old post meta + db.SetRecord(post); + + //Update the index and feed after post update + await UpdateIndexAndFeed(context, db, cancellation); + + return true; + } + + + private async Task UpdateIndexAndFeed(IChannelContext context, IRecordDb<PostMeta> index, CancellationToken cancellation) + { + //Write the index back to the bucket + await Storage.StoreAsync(context, context.IndexPath, index, cancellation); + + //Update feed + if (context.Feed != null) + { + await UpdateRssFeed(context, index.GetRecords(), cancellation); + } + } + + private async Task UpdateRssFeed(IChannelContext context, IEnumerable<PostMeta> meta, CancellationToken cancellation) + { + using VnMemoryStream feedData = new(); + + //Build the feed from posts + FeedGenerator.BuildFeed(context, meta, feedData); + + //Rewind the feed stream + feedData.Seek(0, System.IO.SeekOrigin.Begin); + + //Write the feed to the bucket + await Storage.SetObjectDataAsync(context, feedData, context.Feed!.FeedPath, ContentType.Rss, cancellation); + } + + #region Load/Store Db + + private Task<IRecordDb<PostMeta>> GetPostIndexAsync(IChannelContext channel, CancellationToken cancellation) + { + //Read the index into memory + return Storage.LoadDbAsync<PostMeta>(channel, channel.IndexPath, cancellation); + } + + private Task SetPostIndexAsync(IChannelContext channel, IRecordDb<PostMeta> db, CancellationToken cancellation) + { + //Read the index into memory + return Storage.StoreAsync(channel, channel.IndexPath, db, cancellation); + } + + #endregion + + /* + * Computes a post id based on its meta information and produces a sha1 hash + * to use as a unique id for the post + */ + static void ComputePostId(PostMeta post) + { + post.Id = ManagedHash.ComputeHexHash($"{post.Title}.{post.Author}.{post.Summary}.{post.Date}", HashAlg.SHA1).ToLowerInvariant(); + } + + internal record class ReqLogger(ILogProvider Log) : IRequestLogger + { + public void LogRequest(RequestToLog requestToLog, ResponseToLog responseToLog, double durationMs) + { + Log.Debug("S3 result\n{method} {uri} HTTP {ms}ms\nHTTP {status} {message}\n{content}", + requestToLog.Method, requestToLog.Resource, durationMs, + responseToLog.StatusCode, responseToLog.ErrorMessage, responseToLog.Content + ); + } + } + } +} |