-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Add new module for pruning old content items versions. #18983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| using Microsoft.AspNetCore.Routing; | ||
| using Microsoft.Extensions.Localization; | ||
| using OrchardCore.Contents.VersionPruning.Drivers; | ||
| using OrchardCore.Navigation; | ||
|
|
||
| namespace OrchardCore.Contents.VersionPruning; | ||
|
|
||
| public sealed class AdminMenu : AdminNavigationProvider | ||
| { | ||
| private static readonly RouteValueDictionary _routeValues = new() | ||
| { | ||
| { "area", "OrchardCore.Settings" }, | ||
| { "groupId", ContentVersionPruningSettingsDisplayDriver.GroupId }, | ||
| }; | ||
|
|
||
| internal readonly IStringLocalizer S; | ||
|
|
||
| public AdminMenu(IStringLocalizer<AdminMenu> stringLocalizer) | ||
| { | ||
| S = stringLocalizer; | ||
| } | ||
|
|
||
| protected override ValueTask BuildAsync(NavigationBuilder builder) | ||
| { | ||
| if (NavigationHelper.UseLegacyFormat()) | ||
| { | ||
| builder | ||
| .Add(S["Configuration"], configuration => configuration | ||
| .Add(S["Settings"], settings => settings | ||
| .Add(S["Content Version Pruning"], S["Content Version Pruning"], pruning => pruning | ||
| .Action("Index", "Admin", _routeValues) | ||
| .Permission(ContentVersionPruningPermissions.ManageContentVersionPruningSettings) | ||
| .LocalNav() | ||
| ) | ||
| ) | ||
| ); | ||
|
|
||
| return ValueTask.CompletedTask; | ||
| } | ||
|
|
||
| builder | ||
| .Add(S["Settings"], settings => settings | ||
| .Add(S["Content Version Pruning"], S["Content Version Pruning"].PrefixPosition(), pruning => pruning | ||
| .Action("Index", "Admin", _routeValues) | ||
| .Permission(ContentVersionPruningPermissions.ManageContentVersionPruningSettings) | ||
| .LocalNav() | ||
| ) | ||
| ); | ||
|
|
||
| return ValueTask.CompletedTask; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| using OrchardCore.Security.Permissions; | ||
|
|
||
| namespace OrchardCore.Contents.VersionPruning; | ||
|
|
||
| public static class ContentVersionPruningPermissions | ||
| { | ||
| public static readonly Permission ManageContentVersionPruningSettings = new( | ||
| "ManageContentVersionPruningSettings", | ||
| "Manage Content Version Pruning settings"); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,68 @@ | ||||||
| using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Mvc; | ||||||
| using Microsoft.AspNetCore.Mvc.Localization; | ||||||
| using OrchardCore.Contents.VersionPruning.Drivers; | ||||||
| using OrchardCore.Contents.VersionPruning.Models; | ||||||
| using OrchardCore.Contents.VersionPruning.Services; | ||||||
| using OrchardCore.DisplayManagement.Notify; | ||||||
| using OrchardCore.Entities; | ||||||
| using OrchardCore.Modules; | ||||||
| using OrchardCore.Settings; | ||||||
|
|
||||||
| namespace OrchardCore.Contents.VersionPruning.Controllers; | ||||||
|
|
||||||
| public sealed class AdminController : Controller | ||||||
| { | ||||||
| private readonly IAuthorizationService _authorizationService; | ||||||
| private readonly IContentVersionPruningService _pruningService; | ||||||
| private readonly ISiteService _siteService; | ||||||
| private readonly IClock _clock; | ||||||
| private readonly INotifier _notifier; | ||||||
|
|
||||||
| internal readonly IHtmlLocalizer H; | ||||||
|
|
||||||
| public AdminController( | ||||||
| IAuthorizationService authorizationService, | ||||||
| IContentVersionPruningService pruningService, | ||||||
| ISiteService siteService, | ||||||
| IClock clock, | ||||||
| INotifier notifier, | ||||||
| IHtmlLocalizer<AdminController> htmlLocalizer) | ||||||
| { | ||||||
| _authorizationService = authorizationService; | ||||||
| _pruningService = pruningService; | ||||||
| _siteService = siteService; | ||||||
| _clock = clock; | ||||||
| _notifier = notifier; | ||||||
| H = htmlLocalizer; | ||||||
| } | ||||||
|
|
||||||
| [HttpPost] | ||||||
| public async Task<IActionResult> Prune() | ||||||
| { | ||||||
| if (!await _authorizationService.AuthorizeAsync(User, ContentVersionPruningPermissions.ManageContentVersionPruningSettings)) | ||||||
| { | ||||||
| return Forbid(); | ||||||
| } | ||||||
|
|
||||||
| var settings = await _siteService.GetSettingsAsync<ContentVersionPruningSettings>(); | ||||||
|
|
||||||
| var pruned = await _pruningService.PruneVersionsAsync(settings); | ||||||
|
|
||||||
| var container = await _siteService.LoadSiteSettingsAsync(); | ||||||
| container.Alter<ContentVersionPruningSettings>(nameof(ContentVersionPruningSettings), settings => | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| { | ||||||
| settings.LastRunUtc = _clock.UtcNow; | ||||||
| }); | ||||||
|
|
||||||
| await _siteService.UpdateSiteSettingsAsync(container); | ||||||
|
|
||||||
| await _notifier.SuccessAsync(H["Content version pruning completed. {0} version(s) deleted.", pruned]); | ||||||
|
|
||||||
| return RedirectToAction("Index", "Admin", new | ||||||
| { | ||||||
| area = "OrchardCore.Settings", | ||||||
| groupId = ContentVersionPruningSettingsDisplayDriver.GroupId, | ||||||
| }); | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Http; | ||
| using OrchardCore.Contents.VersionPruning.Models; | ||
| using OrchardCore.Contents.VersionPruning.ViewModels; | ||
| using OrchardCore.DisplayManagement.Entities; | ||
| using OrchardCore.DisplayManagement.Handlers; | ||
| using OrchardCore.DisplayManagement.Views; | ||
| using OrchardCore.Settings; | ||
|
|
||
| namespace OrchardCore.Contents.VersionPruning.Drivers; | ||
|
|
||
| public sealed class ContentVersionPruningSettingsDisplayDriver : SiteDisplayDriver<ContentVersionPruningSettings> | ||
| { | ||
| public const string GroupId = "ContentVersionPruningSettings"; | ||
|
|
||
| private readonly IHttpContextAccessor _httpContextAccessor; | ||
| private readonly IAuthorizationService _authorizationService; | ||
|
|
||
| public ContentVersionPruningSettingsDisplayDriver( | ||
| IAuthorizationService authorizationService, | ||
| IHttpContextAccessor httpContextAccessor) | ||
| { | ||
| _authorizationService = authorizationService; | ||
| _httpContextAccessor = httpContextAccessor; | ||
| } | ||
|
|
||
| protected override string SettingsGroupId => GroupId; | ||
|
|
||
| public override async Task<IDisplayResult> EditAsync(ISite site, ContentVersionPruningSettings settings, BuildEditorContext context) | ||
| { | ||
| if (!await _authorizationService.AuthorizeAsync( | ||
| _httpContextAccessor.HttpContext?.User, | ||
| ContentVersionPruningPermissions.ManageContentVersionPruningSettings)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return Initialize<ContentVersionPruningSettingsViewModel>("ContentVersionPruningSettings_Edit", model => | ||
| { | ||
| model.RetentionDays = settings.RetentionDays; | ||
| model.VersionsToKeep = settings.VersionsToKeep; | ||
| model.Disabled = settings.Disabled; | ||
| model.ContentTypes = settings.ContentTypes; | ||
| model.LastRunUtc = settings.LastRunUtc; | ||
| }).Location("Content:5") | ||
| .OnGroup(GroupId); | ||
| } | ||
|
|
||
| public override async Task<IDisplayResult> UpdateAsync(ISite site, ContentVersionPruningSettings settings, UpdateEditorContext context) | ||
| { | ||
| if (!await _authorizationService.AuthorizeAsync( | ||
| _httpContextAccessor.HttpContext?.User, | ||
| ContentVersionPruningPermissions.ManageContentVersionPruningSettings)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var model = new ContentVersionPruningSettingsViewModel(); | ||
| await context.Updater.TryUpdateModelAsync(model, Prefix); | ||
|
|
||
| settings.RetentionDays = model.RetentionDays; | ||
| settings.VersionsToKeep = model.VersionsToKeep; | ||
| settings.Disabled = model.Disabled; | ||
| settings.ContentTypes = model.ContentTypes ?? []; | ||
|
|
||
| return await EditAsync(site, settings, context); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| using OrchardCore.Modules.Manifest; | ||
|
|
||
| [assembly: Module( | ||
| Name = "Content Version Pruning", | ||
| Author = ManifestConstants.OrchardCoreTeam, | ||
| Website = ManifestConstants.OrchardCoreWebsite, | ||
| Version = ManifestConstants.OrchardCoreVersion, | ||
| Description = "Provides a background task to prune old content item versions.", | ||
| Dependencies = ["OrchardCore.Contents"], | ||
| Category = "Content Management" | ||
| )] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||
| namespace OrchardCore.Contents.VersionPruning.Models; | ||||||
|
|
||||||
| public class ContentVersionPruningSettings | ||||||
| { | ||||||
| /// <summary> | ||||||
| /// The number of days after which non-latest, non-published content item versions are deleted. | ||||||
| /// </summary> | ||||||
| public int RetentionDays { get; set; } = 30; | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// The number of the most-recent archived (non-latest, non-published) versions to retain | ||||||
| /// per content item, regardless of age. | ||||||
| /// </summary> | ||||||
| public int VersionsToKeep { get; set; } = 1; | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// The content types to prune. When empty, all content types are pruned. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| /// </summary> | ||||||
| public string[] ContentTypes { get; set; } = []; | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// Whether the pruning background task is disabled. | ||||||
| /// </summary> | ||||||
| public bool Disabled { get; set; } | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// The last time the pruning task was run. | ||||||
| /// </summary> | ||||||
| public DateTime? LastRunUtc { get; set; } | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should not be part of the settings and it should not be configurable. I actually don’t even think it’s needed |
||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Razor"> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t think this needs to be added into a new module. Make it a feature in the contents module instead |
||
|
|
||
| <PropertyGroup> | ||
| <AddRazorSupportForMvc>true</AddRazorSupportForMvc> | ||
| <!-- NuGet properties--> | ||
| <Title>OrchardCore Contents Version Pruning</Title> | ||
| <Description>$(OCCMSDescription) | ||
|
|
||
| Provides a background task to prune old content item versions.</Description> | ||
| <PackageTags>$(PackageTags) OrchardCoreCMS ContentManagement</PackageTags> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\OrchardCore\OrchardCore.Admin.Abstractions\OrchardCore.Admin.Abstractions.csproj" /> | ||
| <ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentManagement\OrchardCore.ContentManagement.csproj" /> | ||
| <ProjectReference Include="..\..\OrchardCore\OrchardCore.Data.YesSql\OrchardCore.Data.YesSql.csproj" /> | ||
| <ProjectReference Include="..\..\OrchardCore\OrchardCore.DisplayManagement\OrchardCore.DisplayManagement.csproj" /> | ||
| <ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" /> | ||
| <ProjectReference Include="..\..\OrchardCore\OrchardCore.Navigation.Core\OrchardCore.Navigation.Core.csproj" /> | ||
| <ProjectReference Include="..\..\OrchardCore\OrchardCore.Settings.Core\OrchardCore.Settings.Core.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| using OrchardCore.Security.Permissions; | ||
|
|
||
| namespace OrchardCore.Contents.VersionPruning; | ||
|
|
||
| public sealed class Permissions : IPermissionProvider | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Internal |
||
| { | ||
| private readonly IEnumerable<Permission> _allPermissions = | ||
| [ | ||
| ContentVersionPruningPermissions.ManageContentVersionPruningSettings, | ||
| ]; | ||
|
|
||
| public Task<IEnumerable<Permission>> GetPermissionsAsync() | ||
| => Task.FromResult(_allPermissions); | ||
|
|
||
| public IEnumerable<PermissionStereotype> GetDefaultStereotypes() => | ||
| [ | ||
| new PermissionStereotype | ||
| { | ||
| Name = OrchardCoreConstants.Roles.Administrator, | ||
| Permissions = _allPermissions, | ||
| }, | ||
| ]; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| using OrchardCore.ContentManagement; | ||
|
|
||
| namespace OrchardCore.Contents.VersionPruning.Services; | ||
|
|
||
| public static class ContentVersionPruningSelector | ||
| { | ||
| public static List<ContentItem> SelectForDeletion(IEnumerable<ContentItem> candidates, int minVersionsToKeep) | ||
| { | ||
| var result = new List<ContentItem>(); | ||
|
|
||
| foreach (var group in candidates.GroupBy(x => x.ContentItemId)) | ||
| { | ||
| // Sort newest-first so Skip() protects the most-recent N versions. | ||
| // Null ModifiedUtc is treated as oldest: Nullable.Compare places null before | ||
| // any non-null value, so descending order puts null-dated versions last. | ||
| var ordered = group | ||
| .Where(x => | ||
| !x.Latest && | ||
| !x.Published) | ||
| .OrderByDescending(x => x.ModifiedUtc, Comparer<DateTime?>.Create(Nullable.Compare)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. .ThenBy(x => x.Id) |
||
| .Skip(minVersionsToKeep); | ||
|
|
||
| result.AddRange(ordered); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a background task not controller.