Creates data integrity health checks
This commit is contained in:
@@ -77,5 +77,12 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
/// <remarks>Here, <paramref name="filter"/> can be null but <paramref name="ordering"/> cannot.</remarks>
|
||||
IEnumerable<TEntity> GetPage(IQuery<TEntity> query, long pageIndex, int pageSize, out long totalRecords,
|
||||
IQuery<TEntity> filter, Ordering ordering);
|
||||
|
||||
/// <summary>
|
||||
/// Checks the data integrity of the node paths stored in the database
|
||||
/// </summary>
|
||||
bool VerifyNodePaths(out int[] invalidIds);
|
||||
|
||||
void FixNodePaths();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
}
|
||||
|
||||
// content type alias is invariant
|
||||
if(ordering.OrderBy.InvariantEquals("contentTypeAlias"))
|
||||
if (ordering.OrderBy.InvariantEquals("contentTypeAlias"))
|
||||
{
|
||||
var joins = Sql()
|
||||
.InnerJoin<ContentTypeDto>("ctype").On<ContentDto, ContentTypeDto>((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype");
|
||||
@@ -477,6 +477,169 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
|
||||
IQuery<TEntity> filter,
|
||||
Ordering ordering);
|
||||
|
||||
public bool VerifyNodePaths(out int[] invalidIds)
|
||||
{
|
||||
var invalid = new List<int>();
|
||||
|
||||
var sql = SqlContext.Sql()
|
||||
.Select<NodeDto>()
|
||||
.From<NodeDto>()
|
||||
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
// TODO: Could verify sort orders here too
|
||||
|
||||
var currentParentIds = new HashSet<int> { -1 };
|
||||
var prevParentIds = currentParentIds;
|
||||
var lastLevel = -1;
|
||||
|
||||
// use a forward cursor (query)
|
||||
foreach (var node in Database.Query<NodeDto>(sql))
|
||||
{
|
||||
if (node.Level != lastLevel)
|
||||
{
|
||||
// changing levels
|
||||
prevParentIds = currentParentIds;
|
||||
currentParentIds = null;
|
||||
lastLevel = node.Level;
|
||||
}
|
||||
|
||||
if (currentParentIds == null)
|
||||
{
|
||||
// we're reset
|
||||
currentParentIds = new HashSet<int>();
|
||||
}
|
||||
|
||||
currentParentIds.Add(node.NodeId);
|
||||
|
||||
var pathParts = node.Path.Split(',');
|
||||
|
||||
if (!prevParentIds.Contains(node.ParentId))
|
||||
{
|
||||
// invalid, this will be because the level is wrong
|
||||
invalid.Add(node.NodeId);
|
||||
}
|
||||
else if (pathParts.Length < 2)
|
||||
{
|
||||
// invalid path
|
||||
invalid.Add(node.NodeId);
|
||||
}
|
||||
else if (pathParts.Length - 1 != node.Level)
|
||||
{
|
||||
// invalid, either path or level is wrong
|
||||
invalid.Add(node.NodeId);
|
||||
}
|
||||
else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString())
|
||||
{
|
||||
// invalid path
|
||||
invalid.Add(node.NodeId);
|
||||
}
|
||||
else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString())
|
||||
{
|
||||
// invalid path
|
||||
invalid.Add(node.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
invalidIds = invalid.ToArray();
|
||||
return invalid.Count == 0;
|
||||
}
|
||||
|
||||
public void FixNodePaths()
|
||||
{
|
||||
// TODO: We can probably combine this logic with the above
|
||||
|
||||
var invalid = new List<(int child, int parent)>();
|
||||
|
||||
var sql = SqlContext.Sql()
|
||||
.Select<NodeDto>()
|
||||
.From<NodeDto>()
|
||||
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
|
||||
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
|
||||
|
||||
// TODO: Could verify sort orders here too
|
||||
|
||||
var updated = new List<NodeDto>();
|
||||
var missingParentIds = new Dictionary<int, List<NodeDto>>();
|
||||
var currentParentIds = new HashSet<int> { -1 };
|
||||
var prevParentIds = currentParentIds;
|
||||
var lastLevel = -1;
|
||||
|
||||
// use a forward cursor (query)
|
||||
foreach (var node in Database.Query<NodeDto>(sql))
|
||||
{
|
||||
if (node.Level != lastLevel)
|
||||
{
|
||||
// changing levels
|
||||
prevParentIds = currentParentIds;
|
||||
currentParentIds = null;
|
||||
lastLevel = node.Level;
|
||||
}
|
||||
|
||||
if (currentParentIds == null)
|
||||
{
|
||||
// we're reset
|
||||
currentParentIds = new HashSet<int>();
|
||||
}
|
||||
|
||||
currentParentIds.Add(node.NodeId);
|
||||
|
||||
var pathParts = node.Path.Split(',');
|
||||
|
||||
if (!prevParentIds.Contains(node.ParentId))
|
||||
{
|
||||
// invalid, this will be because the level is wrong (which prob means path is wrong too)
|
||||
invalid.Add((node.NodeId, node.ParentId));
|
||||
if (missingParentIds.TryGetValue(node.ParentId, out var childIds))
|
||||
childIds.Add(node);
|
||||
else
|
||||
missingParentIds[node.ParentId] = new List<NodeDto> {node};
|
||||
}
|
||||
else if (pathParts.Length < 2)
|
||||
{
|
||||
// invalid path
|
||||
invalid.Add((node.NodeId, node.ParentId));
|
||||
}
|
||||
else if (pathParts.Length - 1 != node.Level)
|
||||
{
|
||||
// invalid, either path or level is wrong
|
||||
invalid.Add((node.NodeId, node.ParentId));
|
||||
}
|
||||
else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString())
|
||||
{
|
||||
// invalid path
|
||||
invalid.Add((node.NodeId, node.ParentId));
|
||||
}
|
||||
else if (pathParts[pathParts.Length - 2] != node.ParentId.ToString())
|
||||
{
|
||||
// invalid path
|
||||
invalid.Add((node.NodeId, node.ParentId));
|
||||
}
|
||||
else
|
||||
{
|
||||
// it's valid
|
||||
|
||||
if (missingParentIds.TryGetValue(node.NodeId, out var invalidNodes))
|
||||
{
|
||||
// this parent has been flagged as missing which means one or more of it's children was ordered
|
||||
// wrong and was checked first. So now we can try to rebuild the invalid paths.
|
||||
|
||||
foreach (var invalidNode in invalidNodes)
|
||||
{
|
||||
invalidNode.Level = (short) (node.Level + 1);
|
||||
invalidNode.Path = node.Path + "," + invalidNode.NodeId;
|
||||
updated.Add(invalidNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in updated)
|
||||
{
|
||||
Database.Update(node);
|
||||
}
|
||||
}
|
||||
|
||||
// here, filter can be null and ordering cannot
|
||||
protected IEnumerable<TEntity> GetPage<TDto>(IQuery<TEntity> query,
|
||||
long pageIndex, int pageSize, out long totalRecords,
|
||||
|
||||
@@ -526,6 +526,6 @@ namespace Umbraco.Core.Services
|
||||
OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,16 @@
|
||||
/// TODO: Start sharing the logic!
|
||||
/// </summary>
|
||||
public interface IContentServiceBase : IService
|
||||
{ }
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Checks the data integrity of the node paths/levels stored in the database
|
||||
/// </summary>
|
||||
bool VerifyNodePaths(out int[] invalidIds);
|
||||
|
||||
/// <summary>
|
||||
/// Fixes the data integrity of node paths/levels stored in the database
|
||||
/// </summary>
|
||||
void FixNodePaths();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2375,6 +2375,26 @@ namespace Umbraco.Core.Services.Implement
|
||||
return OperationResult.Succeed(evtMsgs);
|
||||
}
|
||||
|
||||
public bool VerifyNodePaths(out int[] invalidIds)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
return _documentRepository.VerifyNodePaths(out invalidIds);
|
||||
}
|
||||
}
|
||||
|
||||
public void FixNodePaths()
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
_documentRepository.FixNodePaths();
|
||||
|
||||
// TODO: We're going to have to clear all caches
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Methods
|
||||
|
||||
@@ -1139,6 +1139,28 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public bool VerifyNodePaths(out int[] invalidIds)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
return _mediaRepository.VerifyNodePaths(out invalidIds);
|
||||
}
|
||||
}
|
||||
|
||||
public void FixNodePaths()
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.MediaTree);
|
||||
_mediaRepository.FixNodePaths();
|
||||
|
||||
// TODO: We're going to have to clear all caches
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -1358,5 +1380,7 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck.Checks.Data
|
||||
{
|
||||
[HealthCheck(
|
||||
"73DD0C1C-E0CA-4C31-9564-1DCA509788AF",
|
||||
"Database integrity check",
|
||||
Description = "Checks for various data integrity issues in the Umbraco database.",
|
||||
Group = "Data Integrity")]
|
||||
public class DatabaseIntegrityCheck : HealthCheck
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private const string _fixMediaPaths = "fixMediaPaths";
|
||||
private const string _fixContentPaths = "fixContentPaths";
|
||||
|
||||
public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_mediaService = mediaService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the status for this health check
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override IEnumerable<HealthCheckStatus> GetStatus()
|
||||
{
|
||||
//return the statuses
|
||||
return new[]
|
||||
{
|
||||
CheckContent(),
|
||||
CheckMedia()
|
||||
};
|
||||
}
|
||||
|
||||
private HealthCheckStatus CheckMedia()
|
||||
{
|
||||
return CheckPaths(_fixMediaPaths, "Fix media paths", "media", () =>
|
||||
{
|
||||
var mediaPaths = _mediaService.VerifyNodePaths(out var invalidMediaPaths);
|
||||
return (mediaPaths, invalidMediaPaths);
|
||||
});
|
||||
}
|
||||
|
||||
private HealthCheckStatus CheckContent()
|
||||
{
|
||||
return CheckPaths(_fixContentPaths, "Fix content paths", "content", () =>
|
||||
{
|
||||
var contentPaths = _contentService.VerifyNodePaths(out var invalidContentPaths);
|
||||
return (contentPaths, invalidContentPaths);
|
||||
});
|
||||
}
|
||||
|
||||
private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, Func<(bool success, int[] invalidPaths)> doCheck)
|
||||
{
|
||||
var result = doCheck();
|
||||
|
||||
var actions = new List<HealthCheckAction>();
|
||||
if (!result.success)
|
||||
{
|
||||
actions.Add(new HealthCheckAction(actionAlias, Id)
|
||||
{
|
||||
Name = actionName
|
||||
});
|
||||
}
|
||||
|
||||
return new HealthCheckStatus(result.success
|
||||
? $"All {entityType} paths are valid"
|
||||
: $"There are {result.invalidPaths.Length} invalid {entityType} paths")
|
||||
{
|
||||
ResultType = result.success ? StatusResultType.Success : StatusResultType.Error,
|
||||
Actions = actions
|
||||
};
|
||||
}
|
||||
|
||||
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
|
||||
{
|
||||
switch (action.Alias)
|
||||
{
|
||||
case _fixContentPaths:
|
||||
_contentService.FixNodePaths();
|
||||
return CheckContent();
|
||||
case _fixMediaPaths:
|
||||
_mediaService.FixNodePaths();
|
||||
return CheckMedia();
|
||||
default:
|
||||
throw new InvalidOperationException("Action not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,7 @@
|
||||
<Compile Include="Editors\MacrosController.cs" />
|
||||
<Compile Include="Editors\RelationTypeController.cs" />
|
||||
<Compile Include="Editors\TinyMceController.cs" />
|
||||
<Compile Include="HealthCheck\Checks\Data\DatabaseIntegrityCheck.cs" />
|
||||
<Compile Include="ImageCropperTemplateCoreExtensions.cs" />
|
||||
<Compile Include="IUmbracoContextFactory.cs" />
|
||||
<Compile Include="Install\ChangesMonitor.cs" />
|
||||
|
||||
Reference in New Issue
Block a user