diff --git a/uSync.BackOffice.Targets/appsettings-schema.usync.json b/uSync.BackOffice.Targets/appsettings-schema.usync.json index 6c64cb33..278bc8f7 100644 --- a/uSync.BackOffice.Targets/appsettings-schema.usync.json +++ b/uSync.BackOffice.Targets/appsettings-schema.usync.json @@ -248,6 +248,10 @@ "description": "Override the group the handler belongs too.", "default": "" }, + "CreateClean": { + "type": "boolean", + "description": "create a corresponding _clean file for this export \n " + }, "Settings": { "type": "object", "description": "Additional settings for the handler", diff --git a/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs b/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs index d1f10da4..4c0df1d2 100644 --- a/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs +++ b/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs @@ -50,6 +50,14 @@ public class HandlerSettings [DefaultValue("")] public string Group { get; set; } = string.Empty; + /// + /// create a corresponding _clean file for this export + /// + /// + /// the clean file will only get created if the item in question has children. + /// + public bool CreateClean { get; set; } = false; + /// /// Additional settings for the handler /// diff --git a/uSync.BackOffice/Services/uSyncService_Single.cs b/uSync.BackOffice/Services/uSyncService_Single.cs index 32cc9bc3..6113ee54 100644 --- a/uSync.BackOffice/Services/uSyncService_Single.cs +++ b/uSync.BackOffice/Services/uSyncService_Single.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Scoping; using Umbraco.Extensions; using uSync.BackOffice.Extensions; @@ -29,11 +29,21 @@ public partial class uSyncService public IEnumerable ReportPartial(string folder, uSyncPagedImportOptions options, out int total) { var orderedNodes = LoadOrderedNodes(folder); + return ReportPartial(orderedNodes, options, out total); + } + + /// + /// perform a paged report with the supplied ordered nodes + /// + public IEnumerable ReportPartial(IList orderedNodes, uSyncPagedImportOptions options, out int total) + { total = orderedNodes.Count; var actions = new List(); var lastType = string.Empty; + var folder = Path.GetDirectoryName(orderedNodes.FirstOrDefault()?.FileName ?? options.RootFolder); + SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); HandlerConfigPair handlerPair = null; @@ -47,11 +57,13 @@ public IEnumerable ReportPartial(string folder, uSyncPagedImportOpt { lastType = itemType; handlerPair = _handlerFactory.GetValidHandlerByTypeName(itemType, syncHandlerOptions); + + handlerPair?.Handler.PreCacheFolderKeys(folder, orderedNodes.Select(x => x.Key).ToList()); } if (handlerPair == null) { - _logger.LogWarning("No handler was found for {alias} ({itemType}) item might not process correctly", itemType); + _logger.LogWarning("No handler for {itemType} {alias}", itemType, item.Node.GetAlias()); continue; } @@ -73,12 +85,20 @@ public IEnumerable ReportPartial(string folder, uSyncPagedImportOpt /// Perform a paged Import against a given folder /// public IEnumerable ImportPartial(string folder, uSyncPagedImportOptions options, out int total) + { + var orderedNodes = LoadOrderedNodes(folder); + return ImportPartial(orderedNodes, options, out total); + } + + /// + /// perform an import of items from the suppled ordered node list. + /// + public IEnumerable ImportPartial(IList orderedNodes, uSyncPagedImportOptions options, out int total) { lock (_importLock) { using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) { - var orderedNodes = LoadOrderedNodes(folder); total = orderedNodes.Count; @@ -99,20 +119,23 @@ public IEnumerable ImportPartial(string folder, uSyncPagedImportOpt { foreach (var item in orderedNodes.Skip(options.PageNumber * options.PageSize).Take(options.PageSize)) { + if (item.Node == null) + item.Node = XElement.Load(item.FileName); + var itemType = item.Node.GetItemType(); if (!itemType.InvariantEquals(lastType)) { lastType = itemType; handlerPair = _handlerFactory.GetValidHandlerByTypeName(itemType, syncHandlerOptions); - // special case, blueprints looks like IContent items, except they are slightly different - // so we check for them specifically and get the handler for the entity rather than the object type. - if (item.Node.IsContent() && item.Node.IsBlueprint()) - { - lastType = UdiEntityType.DocumentBlueprint; - handlerPair = _handlerFactory.GetValidHandlerByEntityType(UdiEntityType.DocumentBlueprint); - } - } + // special case, blueprints looks like IContent items, except they are slightly different + // so we check for them specifically and get the handler for the entity rather than the object type. + if (item.Node.IsContent() && item.Node.IsBlueprint()) + { + lastType = UdiEntityType.DocumentBlueprint; + handlerPair = _handlerFactory.GetValidHandlerByEntityType(UdiEntityType.DocumentBlueprint); + } + } if (handlerPair == null) { @@ -306,7 +329,7 @@ private SyncHandlerOptions HandlerOptionsFromPaged(uSyncPagedImportOptions optio /// /// Load the xml in a folder in level order so we process the higher level items first. /// - private IList LoadOrderedNodes(string folder) + public IList LoadOrderedNodes(string folder) { var files = _syncFileService.GetFiles(folder, $"*.{_uSyncConfig.Settings.DefaultExtension}", true); @@ -322,19 +345,6 @@ private IList LoadOrderedNodes(string folder) .ToList(); } - private class OrderedNodeInfo - { - public OrderedNodeInfo(string filename, XElement node) - { - this.FileName = filename; - this.Node = node; - } - - public XElement Node { get; set; } - public string FileName { get; set; } - } - - /// /// calculate the percentage progress we are making between a range. /// @@ -343,6 +353,38 @@ public OrderedNodeInfo(string filename, XElement node) /// private int CalculateProgress(int value, int total, int min, int max) => (int)(min + (((float)value / total) * (max - min))); + } + + /// + /// detail for a usync file that can be ordered + /// + public class OrderedNodeInfo + { + /// + /// constructor + /// + public OrderedNodeInfo(string filename, XElement node) + { + FileName = filename; + Node = node; + Key = node.GetKey(); + } + + /// + /// xml element of the node + /// + public XElement Node { get; set; } + /// + /// the Guid key for this item, so we can cache the list of keys + /// + public Guid Key { get; set; } + + /// + /// path to the physical file + /// + public string FileName { get; set; } } + + } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs index 8911f335..a64cb808 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs @@ -58,6 +58,10 @@ public ContentHandler( this.serializer = syncItemFactory.GetSerializer("ContentSerializer"); } + /// + protected override bool HasChildren(IContent item) + => contentService.HasChildren(item.Id); + /// /// Get child items /// diff --git a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandlerBase.cs b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandlerBase.cs index aaf06223..ec555713 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandlerBase.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandlerBase.cs @@ -103,7 +103,7 @@ private bool ImportTrashedItem(XElement node, HandlerSettings config) { // unless the setting is explicit we don't import trashed items. var trashed = node.Element("Info")?.Element("Trashed").ValueOrDefault(false); - if (trashed.GetValueOrDefault(false) && !config.GetSetting("ImportTrashed", true)) return false; + if (trashed.GetValueOrDefault(false) && !config.GetSetting("ImportTrashed", false)) return false; return true; } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/MediaHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/MediaHandler.cs index 7498360b..49fbd247 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/MediaHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/MediaHandler.cs @@ -51,6 +51,10 @@ public MediaHandler( this.mediaService = mediaService; } + /// + protected override bool HasChildren(IMedia item) + => mediaService.HasChildren(item.Id); + /// protected override IEnumerable GetChildItems(IEntity parent) { diff --git a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs index 76762769..88c252f4 100644 --- a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs @@ -76,7 +76,7 @@ public interface ISyncHandler string EntityType { get; } /// - /// The type name of the items hanled (Item.getType().ToString()) + /// The type name of the items handled (Item.getType().ToString()) /// string TypeName { get; } @@ -95,7 +95,7 @@ public interface ISyncHandler /// /// folder to use when exporting /// Handler settings to use for export - /// Callbacks to keep UI uptodate + /// Callbacks to keep UI upto date /// List of actions detailing changes IEnumerable ExportAll(string folder, HandlerSettings settings, SyncUpdateCallback callback); @@ -124,7 +124,7 @@ public interface ISyncHandler /// folder to use when Importing /// Handler settings to use for import /// Force the import even if the settings haven't changed - /// Callbacks to keep UI uptodate + /// Callbacks to keep UI upto date /// List of actions detailing changes IEnumerable ImportAll(string folder, HandlerSettings settings, bool force, SyncUpdateCallback callback); @@ -138,7 +138,7 @@ public interface ISyncHandler /// /// folder to use when reporting /// Handler settings to use for report - /// Callbacks to keep UI uptodate + /// Callbacks to keep UI upto date /// List of actions detailing changes IEnumerable Report(string folder, HandlerSettings settings, SyncUpdateCallback callback); @@ -154,14 +154,21 @@ public interface ISyncHandler IEnumerable ImportSecondPass(uSyncAction action, HandlerSettings settings, uSyncImportOptions options); /// - /// default impimentation, roothandler does do this. + /// default implementation, root handler does do this. /// Udi FindFromNode(XElement node) => null; /// - /// is this a current node (roothandler can do this too) + /// is this a current node (root handler can do this too) /// ChangeType GetItemStatus(XElement node) => ChangeType.NoChange; + + /// + /// precaches the keys of a folder + /// + /// + /// + void PreCacheFolderKeys(string folder, IList keys) { } } } diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerBase.cs index 06a57561..a9a10092 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerBase.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerBase.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; using uSync.BackOffice.Configuration; using uSync.BackOffice.Services; @@ -50,6 +51,10 @@ public SyncHandlerBase( this.entityService = entityService; } + /// + protected override bool HasChildren(TObject item) + => entityService.GetChildren(item.Id).Any(); + /// /// given a folder we calculate what items we can remove, becuase they are /// not in one the the files in the folder. @@ -189,7 +194,12 @@ protected override IEnumerable GetChildItems(IEntity parent) /// virtual protected IEnumerable GetChildItems(int parent) { - if (this.itemObjectType != UmbracoObjectTypes.Unknown) + if (this.itemObjectType == UmbracoObjectTypes.Unknown) + return Enumerable.Empty(); + + var cacheKey = $"{GetCacheKeyBase()}_parent_{parent}"; + + return runtimeCache.GetCacheItem(cacheKey, () => { if (parent == -1) { @@ -201,9 +211,7 @@ virtual protected IEnumerable GetChildItems(int parent) // load it, so GetChildren without the object type is quicker. return entityService.GetChildren(parent); } - } - - return Enumerable.Empty(); + }, null); } /// diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs index d1d2d16c..e2252510 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs @@ -609,6 +609,20 @@ protected virtual IEnumerable CleanFolder(string cleanFile, bool re } } + /// + /// pre-populates the cache folder key list. + /// + /// + /// this means if we are calling the process multiple times, + /// we can optimise the key code and only load it once. + /// + public void PreCacheFolderKeys(string folder, IList folderKeys) + { + var cacheKey = $"{GetCacheKeyBase()}_{folder.GetHashCode()}"; + runtimeCache.ClearByKey(cacheKey) ; + runtimeCache.GetCacheItem(cacheKey, () => folderKeys); + } + /// /// Get the GUIDs for all items in a folder /// @@ -624,10 +638,11 @@ protected IList GetFolderKeys(string folder, bool flat) var cacheKey = $"{GetCacheKeyBase()}_{folderKey}"; - logger.LogDebug("Getting Folder Keys : {cacheKey}", cacheKey); return runtimeCache.GetCacheItem(cacheKey, () => { + logger.LogDebug("Getting Folder Keys : {cacheKey}", cacheKey); + // when it's not flat structure we also get the sub folders. (extra defensive get them all) var keys = new List(); var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}", !flat).ToList(); @@ -853,6 +868,11 @@ virtual public IEnumerable Export(TObject item, string folder, Hand { // only write the file to disk if it should be exported. syncFileService.SaveXElement(attempt.Item, filename); + + if (config.CreateClean && HasChildren(item)) + { + CreateCleanFile(GetItemKey(item), filename); + } } else { @@ -865,6 +885,31 @@ virtual public IEnumerable Export(TObject item, string folder, Hand return uSyncActionHelper.SetAction(attempt, filename, GetItemKey(item), this.Alias).AsEnumerableOfOne(); } + /// + /// does this item have any children ? + /// + /// + /// on items where we can check this (quickly) we can reduce the number of checks we might + /// make on child items or cleaning up where we don't need to. + /// + protected virtual bool HasChildren(TObject item) + => true; + + private void CreateCleanFile(Guid key, string filename) + { + if (string.IsNullOrWhiteSpace(filename) || key == Guid.Empty) + return; + + var folder = Path.GetDirectoryName(filename); + var name = Path.GetFileNameWithoutExtension(filename); + + var cleanPath = Path.Combine(folder, $"{name}_clean.config"); + + var node = XElementExtensions.MakeEmpty(key, SyncActionType.Clean, $"clean {name} children"); + node.Add(new XAttribute("itemType", serializer.ItemType)); + syncFileService.SaveXElement(node, cleanPath); + } + #endregion #region Reporting @@ -1749,7 +1794,11 @@ private string GetNameFromFileOrNode(string filename, XElement node) => !string.IsNullOrWhiteSpace(filename) ? filename : node.GetAlias(); - private string GetCacheKeyBase() + /// + /// get thekey for any caches we might call (thread based cache value) + /// + /// + protected string GetCacheKeyBase() => $"keycache_{this.Alias}_{Thread.CurrentThread.ManagedThreadId}"; private string PrepCaches() diff --git a/uSync.Core/Serialization/Serializers/ContentSerializer.cs b/uSync.Core/Serialization/Serializers/ContentSerializer.cs index 7f44d2a2..acc02213 100644 --- a/uSync.Core/Serialization/Serializers/ContentSerializer.cs +++ b/uSync.Core/Serialization/Serializers/ContentSerializer.cs @@ -173,7 +173,8 @@ protected override SyncAttempt DeserializeCore(XElement node, SyncSeri if (node.Element("Info") != null) { var trashed = node.Element("Info").Element("Trashed").ValueOrDefault(false); - details.AddNotNull(HandleTrashedState(item, trashed)); + var restoreParent = node.Element("Info").Element("Trashed").Attribute("Parent").ValueOrDefault(Guid.Empty); + details.AddNotNull(HandleTrashedState(item, trashed, restoreParent)); } details.AddNotNull(DeserializeTemplate(item, node)); @@ -395,29 +396,35 @@ private ContentSchedule FindSchedule(ContentScheduleCollection currentSchedules, } - protected override uSyncChange HandleTrashedState(IContent item, bool trashed) + protected override uSyncChange HandleTrashedState(IContent item, bool trashed, Guid restoreParentKey) { if (!trashed && item.Trashed) { // if the item is trashed, then the change of it's parent // should restore it (as long as we do a move!) - contentService.Move(item, item.ParentId); + + var restoreParentId = GetRelationParentId(item, restoreParentKey, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias); + contentService.Move(item, restoreParentId); // clean out any relations for this item (some versions of Umbraco don't do this on a Move) - CleanRelations(item, "relateParentDocumentOnDelete"); + CleanRelations(item, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias); - return uSyncChange.Update("Restored", item.Name, "Recycle Bin", item.ParentId.ToString()); + return uSyncChange.Update("Restored", item.Name, "Recycle Bin", restoreParentKey.ToString()); } else if (trashed && !item.Trashed) { + // not already in the recycle bin? + if (item.ParentId > Constants.System.RecycleBinContent) + { + // clean any relations that may be there (stops an error) + CleanRelations(item, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias); - // clean any relations that may be there (stops an error) - CleanRelations(item, "relateParentDocumentOnDelete"); + // move to the recycle bin + contentService.MoveToRecycleBin(item); + } - // move to the recycle bin - contentService.MoveToRecycleBin(item); return uSyncChange.Update("Moved to Bin", item.Name, "", "Recycle Bin"); } @@ -432,8 +439,9 @@ protected virtual Attempt DoSaveOrPublish(IContent item, XElement node, return Attempt.Succeed("No Changes"); } + var trashed = item.Trashed || (node.Element("Info")?.Element("Trashed").ValueOrDefault(false) ?? false); var publishedNode = node.Element("Info")?.Element("Published"); - if (!item.Trashed && publishedNode != null) + if (!trashed && publishedNode != null) { var schedules = GetSchedules(node.Element("Info")?.Element("Schedule")); diff --git a/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs b/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs index 0300a0d9..e53bc176 100644 --- a/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs +++ b/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs @@ -371,20 +371,25 @@ protected virtual IEnumerable DeserializeBase(TObject item, XElemen } } - if (item.ParentId != parentId) + if (!item.Trashed) { - changes.AddUpdate(uSyncConstants.Xml.Parent, item.ParentId, parentId); - logger.LogTrace("{Id} Setting Parent {ParentId}", item.Id, parentId); - item.ParentId = parentId; - } + // we change if its not in the bin, + // if its in the bin it will get fixed by handle trashed state. + if (item.ParentId != parentId) + { + changes.AddUpdate(uSyncConstants.Xml.Parent, item.ParentId, parentId); + logger.LogTrace("{Id} Setting Parent {ParentId}", item.Id, parentId); + item.ParentId = parentId; + } - // the following are calculated (not in the file - // because they might change without this node being saved). - if (item.Path != nodePath) - { - changes.AddUpdate(uSyncConstants.Xml.Path, item.Path, nodePath); - logger.LogDebug("{Id} Setting Path {idPath} was {oldPath}", item.Id, nodePath, item.Path); - item.Path = nodePath; + // the following are calculated (not in the file + // because they might change without this node being saved). + if (item.Path != nodePath) + { + changes.AddUpdate(uSyncConstants.Xml.Path, item.Path, nodePath); + logger.LogDebug("{Id} Setting Path {idPath} was {oldPath}", item.Id, nodePath, item.Path); + item.Path = nodePath; + } } if (item.Level != nodeLevel) @@ -394,7 +399,17 @@ protected virtual IEnumerable DeserializeBase(TObject item, XElemen item.Level = nodeLevel; } } - + else // trashed. + { + // we need to set the parent to something, + // or the move will fail. + if (item.ParentId == -1) + { + item.ParentId = item is IContent + ? Constants.System.RecycleBinContent + : Constants.System.RecycleBinMedia; + } + } var key = node.GetKey(); if (key != Guid.Empty && item.Key != key) @@ -626,7 +641,12 @@ protected uSyncChange HandleSortOrder(TObject item, int sortOrder) return null; } - protected abstract uSyncChange HandleTrashedState(TObject item, bool trashed); + [Obsolete("Pass in a restore guid for the parent - should relationships be missing")] + protected virtual uSyncChange HandleTrashedState(TObject item, bool trashed) + => uSyncChange.NoChange($"Member/{item.Name}", item.Name); + + protected virtual uSyncChange HandleTrashedState(TObject item, bool trashed, Guid restoreParent) + => uSyncChange.NoChange($"Member/{item.Name}", item.Name); protected string GetExportValue(object value, IPropertyType propertyType, string culture, string segment) { @@ -679,13 +699,17 @@ protected override Attempt FindOrCreate(XElement node) var alias = node.GetAlias(); - var parentKey = node.Attribute(uSyncConstants.Xml.Parent).ValueOrDefault(Guid.Empty); + var parentKey = node.Element(uSyncConstants.Xml.Info) + ?.Element(uSyncConstants.Xml.Parent) + ?.Attribute(uSyncConstants.Xml.Key) + .ValueOrDefault(Guid.Empty) ?? Guid.Empty; + if (parentKey != Guid.Empty) { item = FindItem(alias, parentKey); if (item != null) return Attempt.Succeed(item); } - + // create var parent = default(TObject); @@ -982,6 +1006,28 @@ protected void CleanRelations(TObject item, string relationType) } + protected int GetRelationParentId(TObject item, Guid restoreParentKey, string relationType) + { + var parentId = -1; + try + { + var deleteRelations = relationService.GetByChild(item, relationType); + if (deleteRelations.Any()) + parentId = deleteRelations.FirstOrDefault()?.ParentId ?? -1; + + if (parentId != -1) return parentId; + return restoreParentKey == Guid.Empty ? -1 : entityService.Get(restoreParentKey)?.Id ?? -1; + } + catch (Exception ex) + { + // unable to find an existing delete relation. + logger.LogWarning(ex, "Error finding restore relation"); + } + + return -1; + + } + private List GetExcludedProperties(SyncSerializerOptions options) { diff --git a/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs b/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs index 6ec16d26..281f956e 100644 --- a/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs +++ b/uSync.Core/Serialization/Serializers/ContentTypeBaseSerializer.cs @@ -1126,7 +1126,7 @@ private IPropertyType GetOrCreateProperty(TObject item, if (dataType == null) { - logger.LogWarning("Cannot find underling DataType {key} {alias} for {property} it is likely you are missing a package?", definitionKey, propertyEditorAlias, alias); + logger.LogWarning("Cannot find underling DataType {key} {alias} for {property} - Either your datatypes are out of sync or you are missing a package?", definitionKey, propertyEditorAlias, alias); return null; } diff --git a/uSync.Core/Serialization/Serializers/MediaSerializer.cs b/uSync.Core/Serialization/Serializers/MediaSerializer.cs index 92d77ea0..09ca7b12 100644 --- a/uSync.Core/Serialization/Serializers/MediaSerializer.cs +++ b/uSync.Core/Serialization/Serializers/MediaSerializer.cs @@ -65,7 +65,8 @@ protected override SyncAttempt DeserializeCore(XElement node, SyncSerial if (node.Element("Info") != null) { var trashed = node.Element("Info").Element("Trashed").ValueOrDefault(false); - details.AddNotNull( HandleTrashedState(item, trashed)); + var restoreParent = node.Element("Info").Element("Trashed").Attribute("Parent").ValueOrDefault(Guid.Empty); + details.AddNotNull(HandleTrashedState(item, trashed, restoreParent)); } var propertyAttempt = DeserializeProperties(item, node, options); @@ -103,22 +104,24 @@ protected override SyncAttempt DeserializeCore(XElement node, SyncSerial return SyncAttempt.Succeed(item.Name, item, ChangeType.Import, "", true, propertyAttempt.Result); } - protected override uSyncChange HandleTrashedState(IMedia item, bool trashed) + protected override uSyncChange HandleTrashedState(IMedia item, bool trashed, Guid restoreParentKey) { if (!trashed && item.Trashed) { // if the item is trashed, then moving it back to the parent value // restores it. - _mediaService.Move(item, item.ParentId); - CleanRelations(item, "relateParentMediaFolderOnDelete"); + var restoreParentId = GetRelationParentId(item, restoreParentKey, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias); + _mediaService.Move(item, restoreParentId); + + CleanRelations(item, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias); return uSyncChange.Update("Restored", item.Name, "Recycle Bin", item.ParentId.ToString()); } else if (trashed && !item.Trashed) { // clean any rouge relations - CleanRelations(item, "relateParentMediaFolderOnDelete"); + CleanRelations(item, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias); // move to the recycle bin _mediaService.MoveToRecycleBin(item);