Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions OrchardCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
<Project Path="src/OrchardCore.Modules/OrchardCore.ContentFields/OrchardCore.ContentFields.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.ContentLocalization/OrchardCore.ContentLocalization.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.ContentPreview/OrchardCore.ContentPreview.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.Contents.VersionPruning/OrchardCore.Contents.VersionPruning.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.Contents/OrchardCore.Contents.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.ContentTypes/OrchardCore.ContentTypes.csproj" />
<Project Path="src/OrchardCore.Modules/OrchardCore.CustomSettings/OrchardCore.CustomSettings.csproj" />
Expand Down
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()
Copy link
Copy Markdown
Member

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.

{
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 =>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
container.Alter<ContentVersionPruningSettings>(nameof(ContentVersionPruningSettings), settings =>
container.Alter<ContentVersionPruningSettings>(settings =>

{
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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// The content types to prune. When empty, all content types are pruned.
/// The content types to prune. When empty, no content types are pruned.

/// </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; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.ThenBy(x => x.Id)

.Skip(minVersionsToKeep);

result.AddRange(ordered);
}

return result;
}
}
Loading
Loading