Skip to content
Merged
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
50 changes: 46 additions & 4 deletions uSync.Core/Serialization/Serializers/ContentSerializerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ protected virtual async Task<XElement> SerializeInfoAsync(TObject item, SyncSeri
parentKey = parent.Key;
parentName = parent.Name;
}
else
{
// the parent might not be of our object type (e.g a blueprint's
// parent can be a DocumentBlueprintContainer, not a DocumentBlueprint)
// so fall back to an untyped lookup rather than losing the parent entirely.
var entity = syncMappers.EntityCache.GetEntity(item.ParentId);
if (entity != null)
{
parentKey = entity.Key;
parentName = entity.Name ?? parentName;
}
}
}
}

Expand Down Expand Up @@ -315,6 +327,8 @@ protected override async Task<SyncAttempt<TObject>> CanDeserializeAsync(XElement
return SyncAttempt<TObject>.Succeed("No check", ChangeType.NoChange);
}

protected virtual async Task<ITreeEntity?> CreateParentIfMissingAsync(XElement parentNode, string path) => null;

protected virtual async Task<IEnumerable<uSyncChange>> DeserializeBaseAsync(TObject item, XElement node, SyncSerializerOptions options)
{
var info = node?.Element(uSyncConstants.Xml.Info);
Expand Down Expand Up @@ -355,6 +369,10 @@ protected virtual async Task<IEnumerable<uSyncChange>> DeserializeBaseAsync(TObj
}
}

// last chance blueprints will create the missing containers.
parent ??= await CreateParentIfMissingAsync(parentNode,
info?.Element(uSyncConstants.Xml.Path).ValueOrDefault(string.Empty) ?? string.Empty);

if (parent != null)
{
parentId = parent.Id;
Expand Down Expand Up @@ -918,6 +936,21 @@ private string GetFriendlyPath(string path)
(item.Name ?? item.Id.ToString()).ToSafeAlias(shortStringHelper));
}

// some paths contain ids for items that are not of our object type
// (e.g a blueprint's parent can be a DocumentBlueprintContainer, not
// a DocumentBlueprint) - so for anything still unresolved, fall back
// to an untyped lookup rather than leaving the raw id in the path.
var unresolvedIds = lookups.Where(id => items.All(x => x.Id != id));
foreach (var id in unresolvedIds)
{
var entity = syncMappers.EntityCache.GetEntity(id);
if (entity == null) continue;

AddToNameCache(entity.Id, entity.Key, entity.Name ?? entity.Id.ToString());
friendlyPath = friendlyPath.Replace($"[{entity.Id}]",
(entity.Name ?? entity.Id.ToString()).ToSafeAlias(shortStringHelper));
}

return friendlyPath;
}
catch (Exception ex)
Expand Down Expand Up @@ -1007,9 +1040,18 @@ public override async Task<SyncAttempt<XElement>> SerializeEmptyAsync(TObject it
public override string ItemAlias(TObject item)
=> item.Name ?? item.Id.ToString();

protected async Task<TObject?> FindParentAsync(XElement node, bool searchByAlias = false)
protected virtual async Task<ITreeEntity?> FindItemAsTreeEntityAsync(Guid key)
=> await FindItemAsync(key);

protected virtual async Task<ITreeEntity?> FindItemAsTreeEntityAsync(string alias)
=> await FindItemAsync(alias);

protected virtual async Task<ITreeEntity?> FindByPathAsTreeEntityAsync(IEnumerable<string> folders, bool failIfNotExits)
=> await FindByPathAsync(folders, failIfNotExits);

protected async Task<ITreeEntity?> FindParentAsync(XElement node, bool searchByAlias = false)
{
var item = default(TObject);
var item = default(ITreeEntity);

if (node == null) return default;

Expand All @@ -1019,7 +1061,7 @@ public override string ItemAlias(TObject item)
if (logger.IsEnabled(LogLevel.Trace))
logger.LogTrace("Looking for Parent by Key {Key}", key);

item = await FindItemAsync(key);
item = await FindItemAsTreeEntityAsync(key);
if (item != null) return item;
}

Expand All @@ -1032,7 +1074,7 @@ public override string ItemAlias(TObject item)

if (!string.IsNullOrEmpty(alias))
{
item = await FindItemAsync(node.ValueOrDefault(alias));
item = await FindItemAsTreeEntityAsync(node.ValueOrDefault(alias));
}
}

Expand Down
111 changes: 101 additions & 10 deletions uSync.Core/Serialization/Serializers/ContentTemplateSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;

using uSync.Core.Documents;
using uSync.Core.Extensions;
Expand All @@ -19,6 +20,7 @@ namespace uSync.Core.Serialization.Serializers;
public class ContentTemplateSerializer : ContentSerializer, ISyncSerializer<IContent>
{
private readonly IContentTypeService _contentTypeService;
private readonly IContentBlueprintContainerService _containerService;

public ContentTemplateSerializer(
IEntityService entityService,
Expand All @@ -31,10 +33,12 @@ public ContentTemplateSerializer(
SyncValueMapperCollection syncMappers,
IUserService userService,
ITemplateService templateService,
ISyncDocumentUrlCleaner urlCleaner)
ISyncDocumentUrlCleaner urlCleaner,
IContentBlueprintContainerService containerService)
: base(entityService, languageService, relationService, shortStringHelper, logger, contentService, syncMappers, userService, templateService, urlCleaner)
{
_contentTypeService = contentTypeService;
_containerService = containerService;
this.umbracoObjectType = UmbracoObjectTypes.DocumentBlueprint;
}

Expand Down Expand Up @@ -115,6 +119,7 @@ protected override async Task<SyncAttempt<IContent>> DeserializeCoreAsync(XEleme
// TODO: Umbraco 8 bug, the key is sometimes an old version
var entity = entityService.Get(key);
if (entity != null)

return contentService.GetBlueprintById(entity.Id);

return null;
Expand All @@ -129,18 +134,104 @@ protected override async Task<SyncAttempt<IContent>> DeserializeCoreAsync(XEleme
if (contentType == null) return
Attempt.Fail<IContent?>(null, new ArgumentException($"Missing content Type {itemType}"));

IContent item;
if (parent != null)
{
item = new Content(alias, (IContent)parent, contentType);
}
else
// parent can be either an existing blueprint (unlikely) or the
// DocumentBlueprintContainer (folder) the blueprint lives in, so
// we create by id rather than assuming it's always an IContent.
var item = new Content(alias, parent?.Id ?? -1, contentType);

return Attempt.Succeed<IContent?>(item);
});
}

protected override async Task<Attempt<IContent?>> FindOrCreateAsync(XElement node)
{
var item = await FindItemAsync(node);
if (item is not null) return Attempt.Succeed(item);

var info = node.Element(uSyncConstants.Xml.Info);
var alias = node.GetAlias();

var parentNode = info?.Element(uSyncConstants.Xml.Parent);
var parentKey = parentNode?.Attribute(uSyncConstants.Xml.Key).ValueOrDefault(Guid.Empty) ?? Guid.Empty;

ITreeEntity? parent = null;

if (parentKey != Guid.Empty)
{
item = await FindItemAsync(alias, parentKey);
if (item is not null) return Attempt.Succeed(item);

parent = await FindItemAsTreeEntityAsync(parentKey);

// the parent might not be another blueprint, but the
// DocumentBlueprintContainer (folder) the blueprint lives in.
parent ??= await FindOrCreateContainerAsync(parentKey, parentNode?.Value ?? string.Empty,
info?.Element(uSyncConstants.Xml.Path).ValueOrDefault(string.Empty) ?? string.Empty);
}

var contentTypeAlias = info?.Element("ContentType").ValueOrDefault(node.Name.LocalName) ?? node.Name.LocalName;

return await CreateItemAsync(alias, parent, contentTypeAlias);
}


protected override async Task<ITreeEntity?> FindItemAsTreeEntityAsync(Guid key)
=> await base.FindItemAsTreeEntityAsync(key) ?? await FindContainerAsync(key);

private async Task<EntityContainer?> FindContainerAsync(Guid key)
=> await _containerService.GetAsync(key);

protected override async Task<ITreeEntity?> CreateParentIfMissingAsync(XElement parentNode, string path)
{
var key = parentNode.Attribute(uSyncConstants.Xml.Key).ValueOrDefault(Guid.Empty);
var name = parentNode.ValueOrDefault(string.Empty);
if (key == Guid.Empty || string.IsNullOrEmpty(name)) return null;

return await FindOrCreateContainerAsync(key, name, path);
}

/// <summary>
/// find (or create) the DocumentBlueprintContainer folder chain for a blueprint,
/// the same way missing folders get created on the way in for content types / data types.
/// </summary>
private async Task<EntityContainer?> FindOrCreateContainerAsync(Guid key, string name, string friendlyPath)
{
var container = await FindContainerAsync(key);
if (container is not null) return container;

if (string.IsNullOrWhiteSpace(name)) return null;

// the friendly path is '/folder/folder/blueprintName' - the folder chain
// is everything except the blueprint's own name at the end.
var folderNames = friendlyPath.ToDelimitedList("/").ToList();
if (folderNames.Count > 0) folderNames.RemoveAt(folderNames.Count - 1);
if (folderNames.Count == 0) folderNames.Add(name);

EntityContainer? parent = null;

for (var index = 0; index < folderNames.Count; index++)
{
var folderName = folderNames[index];
var isTargetFolder = index == folderNames.Count - 1;

var existing = (await _containerService.GetAsync(folderName, index + 1))
.FirstOrDefault(x => x.Name.InvariantEquals(folderName) &&
(parent == null || x.ParentId == parent.Id));

if (existing is not null)
{
item = new Content(alias, -1, contentType);
parent = existing;
continue;
}

return Attempt.Succeed(item);
});
var containerKey = isTargetFolder ? key : Guid.NewGuid();
var attempt = await _containerService.CreateAsync(containerKey, folderName, parent?.Key, Constants.Security.SuperUserKey);
if (!attempt.Success || attempt.Result is null) return parent;

parent = attempt.Result;
}

return parent;
}

protected override Task<SyncContentUpdateResult> DoSaveOrPublishAsync(IContent item, XElement node, SyncSerializerOptions options)
Expand Down
4 changes: 2 additions & 2 deletions uSync.Core/Serialization/SyncTreeSerializerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ protected virtual Task<bool> HasParentItemAsync(XElement node)
/// <summary>
/// calculates the Umbraco Path value for an item, based on the parent
/// </summary>
protected string CalculateNodePath(TObject item, TObject? parent)
protected string CalculateNodePath(TObject item, ITreeEntity? parent)
{
if (parent == null)
{
Expand All @@ -103,7 +103,7 @@ protected string CalculateNodePath(TObject item, TObject? parent)
/// <summary>
/// calculates the Level based on the parent.
/// </summary>
protected int CalculateNodeLevel(TObject item, TObject? parent)
protected int CalculateNodeLevel(TObject item, ITreeEntity? parent)
{
if (parent == null)
{
Expand Down
Loading