diff --git a/uSync.BackOffice.Targets/appsettings-schema.usync.json b/uSync.BackOffice.Targets/appsettings-schema.usync.json
index e539eee12..c5acb1c13 100644
--- a/uSync.BackOffice.Targets/appsettings-schema.usync.json
+++ b/uSync.BackOffice.Targets/appsettings-schema.usync.json
@@ -205,7 +205,7 @@
},
"DisableNotificationSuppression": {
"type": "boolean",
- "description": "turns of use of the Notifications.Supress method, so notifications\nfire after every item is imported.\n ",
+ "description": "turns off use of the Notifications.Suppress method, so notifications\nfire after every item is imported.\n ",
"default": "true"
},
"BackgroundNotifications": {
@@ -217,9 +217,37 @@
"type": "boolean",
"description": "Move the uSync tree to it's own section in the back office. \n(requires a restart to take effect).\n ",
"default": false
+ },
+ "FolderMode": {
+ "description": "What type of mode the folder should work in (default, root, or production)\n ",
+ "default": "Normal",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/USyncBackOfficeConfigurationSyncFolderMode"
+ }
+ ]
+ },
+ "ProductionFolder": {
+ "type": "string",
+ "description": "location of the 'production' folder to use when in production mode, \nor when creating the production mode files.\n ",
+ "default": "uSync/production"
}
}
},
+ "USyncBackOfficeConfigurationSyncFolderMode": {
+ "type": "string",
+ "description": "uSync's folder mode - normal, root or production\n ",
+ "x-enumNames": [
+ "Normal",
+ "Root",
+ "Production"
+ ],
+ "enum": [
+ "Normal",
+ "Root",
+ "Production"
+ ]
+ },
"USyncuSyncSetsDefinition": {
"type": "object",
"properties": {
diff --git a/uSync.BackOffice/Configuration/SyncConfigService.cs b/uSync.BackOffice/Configuration/SyncConfigService.cs
index a142000ad..e1377ec8e 100644
--- a/uSync.BackOffice/Configuration/SyncConfigService.cs
+++ b/uSync.BackOffice/Configuration/SyncConfigService.cs
@@ -66,11 +66,8 @@ private class SyncFolderItem
///
public string GetWorkingFolder()
{
- var folders = FetchFolders();
-
- return Settings.IsRootSite
- ? folders[0].TrimStart('/')
- : folders.Last().TrimStart('/');
+ var folders = GetFolders();
+ return folders.Last().TrimStart('/');
}
///
@@ -78,9 +75,15 @@ public string[] GetFolders()
{
var folders = FetchFolders();
- return Settings.IsRootSite
- ? [folders[0].TrimStart('/')]
- : [.. folders.Select(x => x.TrimStart('/'))];
+ switch(Settings.FolderMode)
+ {
+ case SyncFolderMode.Root:
+ return [folders[0].TrimStart('/')];
+ case SyncFolderMode.Production:
+ return [Settings.ProductionFolder.TrimStart('/')];
+ default:
+ return [.. folders.Select(x => x.TrimStart('/'))];
+ }
}
///
diff --git a/uSync.BackOffice/Configuration/uSyncSettings.cs b/uSync.BackOffice/Configuration/uSyncSettings.cs
index c290fe195..08eb0c7c5 100644
--- a/uSync.BackOffice/Configuration/uSyncSettings.cs
+++ b/uSync.BackOffice/Configuration/uSyncSettings.cs
@@ -197,26 +197,22 @@ public class uSyncSettings
public string HideAddOns { get; set; } = "licence";
///
- /// turns of use of the Notifications.Supress method, so notifications
+ /// turns off use of the Notifications.Suppress method, so notifications
/// fire after every item is imported.
///
///
- /// I am not sure this does what i think it does, it doesn't suppress
- /// then fire at the end , it just suppresses them all.
+ /// this disables the internal uSync scope provider that delays all
+ /// non cancellable notifications until after the import is complete.
///
- /// until we have had time to look at this , we will leave this as
- /// disabled by default so all notification messages fire.
+ /// on v13 this is false, the import happens and then the notifications fire.
///
- /// for v13 thius is fine, but for v14, grouping the notifications
- /// can causes issues if something fails.
- ///
- /// So if a single content import fails then the whole batch doesn't
- /// get published properly (so no content for you :( ) .
+ /// on v16 the default is true, because some of the notifications appear to
+ /// be closely coupled to the save/publish process, and if something goes
+ /// wrong in one item's import it can cause a cascade of failures across
+ /// everything that might have been imported along with it.
///
- /// there might be something downlever we can do, but it likey means
- /// lots of core investigation to find that, for now 'old' school
- /// non suppressed notifications should be fine (if a little slower).
- ///
+ /// if the notifications are not suppressed, then if an item fails to import
+ /// it doesn't stop other items from being imported.
///
[DefaultValue("true")]
public bool DisableNotificationSuppression { get; set; } = true;
@@ -238,4 +234,39 @@ public class uSyncSettings
///
[DefaultValue(false)]
public bool MoveToSection { get; set; } = false;
+
+ ///
+ /// What type of mode the folder should work in (default, root, or production)
+ ///
+ [DefaultValue(SyncFolderMode.Normal)]
+ public SyncFolderMode FolderMode { get; set; } = SyncFolderMode.Normal;
+
+ ///
+ /// location of the 'production' folder to use when in production mode,
+ /// or when creating the production mode files.
+ ///
+ [DefaultValue("uSync/production")]
+ public string ProductionFolder { get; set; } = "uSync/production";
}
+
+///
+/// uSync's folder mode - normal, root or production
+///
+public enum SyncFolderMode
+{
+ ///
+ /// normal - expects individual files in the uSync folder(s)
+ ///
+ Normal,
+
+ ///
+ /// root - will read and write things to the root folder,
+ ///
+ Root,
+
+ ///
+ /// production - looks in a 'production' folder, expects a single file per handler.
+ ///
+ Production,
+};
+
diff --git a/uSync.BackOffice/Hubs/HubClientService.cs b/uSync.BackOffice/Hubs/HubClientService.cs
index 4ec28edca..e39cd2836 100644
--- a/uSync.BackOffice/Hubs/HubClientService.cs
+++ b/uSync.BackOffice/Hubs/HubClientService.cs
@@ -80,5 +80,27 @@ public void PostUpdate(string message, int count, int total)
/// get the uSync callbacks for this connection
///
///
- public uSyncCallbacks Callbacks() => new(this.PostSummary, this.PostUpdate);
+ public uSyncCallbacks Callbacks() => new(this.PostSummary, this.PostUpdate, this.SetCountRange, this.PostIncrementalUpdate);
+
+ private int _start = 0;
+ private int _end = 0;
+
+ ///
+ /// set a range (start to end) that we expect the next set of updates to be bound within.
+ ///
+ public void SetCountRange(int start, int end)
+ {
+ _start = start;
+ _end = end;
+ }
+
+ ///
+ /// post an update and increment the counter by one.
+ ///
+ public void PostIncrementalUpdate(string message)
+ {
+ _start++;
+ if (_start > _end) _end = _start;
+ this.PostUpdate(message, _start, _end);
+ }
}
diff --git a/uSync.BackOffice/Hubs/uSyncCallbacks.cs b/uSync.BackOffice/Hubs/uSyncCallbacks.cs
index baddc009e..fc94fc782 100644
--- a/uSync.BackOffice/Hubs/uSyncCallbacks.cs
+++ b/uSync.BackOffice/Hubs/uSyncCallbacks.cs
@@ -1,7 +1,30 @@
-using uSync.BackOffice.SyncHandlers.Interfaces;
+using uSync.BackOffice.Models;
+using uSync.BackOffice.SyncHandlers.Interfaces;
namespace uSync.BackOffice;
+///
+/// Callback event for SignalR hub
+///
+public delegate void SyncEventCallback(SyncProgressSummary summary);
+
+///
+/// callback delegate for SignalR messaging
+///
+public delegate void SyncUpdateCallback(string message, int count, int total);
+
+///
+/// callback delegate to set the start and end range for the update counters.
+///
+public delegate void SyncSetUpdateRange(int start, int end);
+
+///
+/// callback to send a update message and increment the counter by one so moving the progress bar.
+///
+///
+public delegate void SyncIncrementalUpdateCallback(string message);
+
+
///
/// Callback objects used to communicate via SignalR
///
@@ -17,6 +40,16 @@ public class uSyncCallbacks
///
public SyncUpdateCallback? Update { get; private set; }
+ ///
+ /// set a start and end range for the counter.
+ ///
+ public SyncSetUpdateRange? SetRange { get; private set; }
+
+ ///
+ /// update and increment callback.
+ ///
+ public SyncIncrementalUpdateCallback? IncrementalUpdate { get; private set; }
+
///
/// generate a new callback object
///
@@ -25,4 +58,14 @@ public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update)
this.Callback = callback;
this.Update = update;
}
+
+ ///
+ /// generate a callback object with range and incremental update
+ ///
+ public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update, SyncSetUpdateRange? updateRange, SyncIncrementalUpdateCallback incrementalUpdate)
+ : this(callback, update)
+ {
+ this.SetRange = updateRange;
+ this.IncrementalUpdate = incrementalUpdate;
+ }
}
diff --git a/uSync.BackOffice/Services/ISyncFileService.cs b/uSync.BackOffice/Services/ISyncFileService.cs
index 40c12c12b..839886777 100644
--- a/uSync.BackOffice/Services/ISyncFileService.cs
+++ b/uSync.BackOffice/Services/ISyncFileService.cs
@@ -135,6 +135,11 @@ public interface ISyncFileService
///
Task LoadXElementAsync(string file);
+ ///
+ /// merge all the files in the given folders into a single xml node, that can be bulk imported
+ ///
+ Task MakeSingleExportFromFolders(string[] folders, string itemType, ISyncTrackerBase? trackerBase, string fileName, string extension);
+
///
/// merge a list of files into a single XElement
///
diff --git a/uSync.BackOffice/Services/ISyncService.cs b/uSync.BackOffice/Services/ISyncService.cs
index a962c400e..9ccce0492 100644
--- a/uSync.BackOffice/Services/ISyncService.cs
+++ b/uSync.BackOffice/Services/ISyncService.cs
@@ -156,4 +156,10 @@ public interface ISyncService
/// trigger the end of the bulk process
///
Task FinishBulkProcessAsync(HandlerActions action, IEnumerable actions);
+
+ ///
+ /// merge the given folders in single 'production' files for each handler.
+ ///
+ Task MergeExportFolder(string[] paths, IEnumerable handlers);
+
}
\ No newline at end of file
diff --git a/uSync.BackOffice/Services/SyncFileService.cs b/uSync.BackOffice/Services/SyncFileService.cs
index bdf7dd3ca..9d79aa015 100644
--- a/uSync.BackOffice/Services/SyncFileService.cs
+++ b/uSync.BackOffice/Services/SyncFileService.cs
@@ -242,6 +242,7 @@ public async Task SaveFileAsync(string filename, string content)
{
CheckCharacters = false,
Async = true,
+ IgnoreWhitespace = true,
};
private static XmlWriterSettings _writerSettings = new XmlWriterSettings
@@ -251,8 +252,6 @@ public async Task SaveFileAsync(string filename, string content)
Async = true,
CloseOutput= false,
Indent = true,
-
-
};
///
@@ -323,7 +322,26 @@ public void CopyFolder(string source, string target)
{
File.Copy(file, file.Replace(resolvedSource, resolvedTarget), true);
}
+ }
+
+ public async Task MakeSingleExportFromFolders(string[] folders, string itemType, ISyncTrackerBase? trackerBase, string filename, string extension)
+ {
+ var merged = await MergeFoldersAsync(folders, extension, trackerBase);
+
+ var megaNode = new XElement(itemType + "s");
+ int count = 0;
+ foreach(var item in merged)
+ {
+ count++;
+ megaNode.Add(new XElement(item.Node));
+ }
+
+ var resolvedTargetFile = GetAbsPath(filename);
+ CreateFoldersForFile(resolvedTargetFile);
+
+ await SaveXElementAsync(megaNode, resolvedTargetFile);
+ return count;
}
///
diff --git a/uSync.BackOffice/Services/SyncService.cs b/uSync.BackOffice/Services/SyncService.cs
index 0efa3a9d6..6332db6be 100644
--- a/uSync.BackOffice/Services/SyncService.cs
+++ b/uSync.BackOffice/Services/SyncService.cs
@@ -27,12 +27,6 @@
namespace uSync.BackOffice;
-
-///
-/// Callback event for SignalR hub
-///
-public delegate void SyncEventCallback(SyncProgressSummary summary);
-
///
/// the service that does all the processing,
/// this forms the entry point as an API to
diff --git a/uSync.BackOffice/Services/SyncService_Files.cs b/uSync.BackOffice/Services/SyncService_Files.cs
index d4842ebd5..1d23577be 100644
--- a/uSync.BackOffice/Services/SyncService_Files.cs
+++ b/uSync.BackOffice/Services/SyncService_Files.cs
@@ -1,7 +1,13 @@
-using System;
+using Microsoft.Extensions.Logging;
+
+using System;
+using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
+using System.Threading.Tasks;
+
+using uSync.BackOffice.SyncHandlers.Models;
namespace uSync.BackOffice;
@@ -104,4 +110,29 @@ private static string CleanPathForZip(string path)
=> Path.GetFullPath(
path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))
.TrimEnd(Path.DirectorySeparatorChar);
+
+ ///
+ public async Task MergeExportFolder(string[] paths, IEnumerable handlers)
+ {
+ var totalMerged = 0;
+
+ foreach (var handler in handlers)
+ {
+ var serializerType = handler.Handler.GetSerializerType();
+ var baseTracker = handler.Handler.GetBaseTracker();
+ if (serializerType is null || baseTracker is null)
+ {
+ _logger.LogWarning("Handler {Handler} does not support file merging", handler.Handler.Alias);
+ continue;
+ }
+
+ var folders = paths.Select(x => Path.Combine(x, handler.Handler.DefaultFolder)).ToArray();
+ var targetFileName = Path.Combine(_uSyncConfig.Settings.ProductionFolder,
+ handler.Handler.DefaultFolder + "." + _uSyncConfig.Settings.DefaultExtension);
+
+ totalMerged += await _syncFileService.MakeSingleExportFromFolders(folders, serializerType, baseTracker, targetFileName, _uSyncConfig.Settings.DefaultExtension);
+ }
+
+ return totalMerged;
+ }
}
diff --git a/uSync.BackOffice/Services/SyncService_Handlers.cs b/uSync.BackOffice/Services/SyncService_Handlers.cs
index 6d204e05f..99958a7f7 100644
--- a/uSync.BackOffice/Services/SyncService_Handlers.cs
+++ b/uSync.BackOffice/Services/SyncService_Handlers.cs
@@ -38,9 +38,19 @@ public async Task> ReportHandlerAsync(string handler, u
if (handlerPair == null) return [];
var folders = GetHandlerFolders(GetFolderFromOptions(options), handlerPair.Handler);
+ var productionFile = $"{folders.Last()}.{_uSyncConfig.Settings.DefaultExtension}";
+ if (_syncFileService.FileExists(productionFile))
+ return await ReportMergedFile(productionFile, handlerPair, options);
+
return await handlerPair.Handler.ReportAsync(folders, handlerPair.Settings, options.Callbacks?.Update);
}
+ private async Task> ReportMergedFile(string filename, HandlerConfigPair handlerPair, uSyncImportOptions options)
+ {
+ var node = await _syncFileService.LoadXElementAsync(filename);
+ return await handlerPair.Handler.ReportElementAsync(node, filename, handlerPair.Settings, options);
+ }
+
/// >
public async Task> ImportHandlerAsync(string handlerAlias, uSyncImportOptions options)
{
@@ -68,12 +78,15 @@ public async Task> ImportHandlerAsync(string handlerAli
backgroundTaskQueue: _backgroundTaskQueue,
options.Callbacks?.Update);
- var results = await handlerPair.Handler.ImportAllAsync(folders, handlerPair.Settings, options);
+ List results;
- // _logger.LogDebug("< Import Handler {handler}", handlerAlias);
+ var productionFile = $"{folders.Last()}.{_uSyncConfig.Settings.DefaultExtension}";
+ if (_syncFileService.FileExists(productionFile))
+ results = [.. await ImportMergedFile(productionFile, handlerPair, options)];
+ else
+ results = [.. await handlerPair.Handler.ImportAllAsync(folders, handlerPair.Settings, options)];
scope?.Complete();
-
return results;
}
}
@@ -83,6 +96,12 @@ public async Task> ImportHandlerAsync(string handlerAli
}
}
+ private async Task> ImportMergedFile(string filename, HandlerConfigPair handlerPair, uSyncImportOptions options)
+ {
+ var node = await _syncFileService.LoadXElementAsync(filename);
+ return await handlerPair.Handler.ImportElementAsync(node, filename, handlerPair.Settings, options);
+ }
+
/// >
public async Task> PerformPostImportAsync(string[] folders, string handlerSet, IEnumerable actions)
{
diff --git a/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs
index b58ae9cd9..6576b964d 100644
--- a/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs
+++ b/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using System.Xml.XPath;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
@@ -66,40 +67,7 @@ public DataTypeHandler(
_dataTypeContainerService = dataTypeContainerService;
}
- ///
- /// Process all DataType actions at the end of the import process
- ///
- ///
- /// Datatypes have to exist early on so DocumentTypes can reference them, but
- /// some doctypes reference content or document types, so we re-process them
- /// at the end of the import process to ensure those settings can be made too.
- ///
- /// HOWEVER: The above isn't a problem Umbraco 10+ - the references can be set
- /// before the actual doctypes exist, so we can do that in one pass.
- ///
- /// HOWEVER: If we move deletes to the end , we still need to process them.
- /// but deletes are always 'change' = 'Hidden', so we only process hidden changes
- ///
- public override async Task> ProcessPostImportAsync(IEnumerable actions, HandlerSettings config)
- {
- if (actions == null || !actions.Any()) return [];
-
- var results = new List();
- var options = new uSyncImportOptions { Flags = SerializerFlags.LastPass };
-
- // we only do deletes here.
- foreach (var action in actions.Where(x => x.Change == ChangeType.Hidden))
- {
- if (action.FileName is null) continue;
- results.AddRange(
- await ImportAsync(action.FileName, config, options));
- }
-
- results.AddRange(await CleanFoldersAsync(Guid.Empty));
-
- return results;
- }
-
+
///
/// Fetch a DataType Container from the DataTypeService
///
diff --git a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs
index 9bae83a8b..8450f5154 100644
--- a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs
+++ b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs
@@ -10,19 +10,25 @@
using uSync.Core;
using uSync.Core.Dependency;
using uSync.Core.Models;
+using uSync.Core.Tracking;
namespace uSync.BackOffice.SyncHandlers.Interfaces;
-///
-/// callback delegate for SignalR messaging
-///
-public delegate void SyncUpdateCallback(string message, int count, int total);
-
///
/// Handler interface for anything that wants to process elements via uSync
///
public interface ISyncHandler
{
+ ///
+ /// get the serializer type for the handler (e.g the name used in the xml)
+ ///
+ string? GetSerializerType() => null;
+
+ ///
+ /// gets the base tracker from the serializer (used to track changes, merge items).
+ ///
+ ISyncTrackerBase? GetBaseTracker() => null;
+
///
/// alias for handler, used when finding a handler
///
diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs
index 26a773b2c..4b76fb953 100644
--- a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs
+++ b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs
@@ -7,6 +7,7 @@
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
+using System.Xml.XPath;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
@@ -24,6 +25,8 @@
using uSync.Core.Dependency;
using uSync.Core.Serialization;
+using CoreConstants = uSync.Core.uSyncConstants;
+
namespace uSync.BackOffice.SyncHandlers;
///
@@ -111,13 +114,39 @@ public virtual async Task> ProcessPostImportAsync(IEnum
var results = new List();
var options = new uSyncImportOptions { Flags = SerializerFlags.LastPass };
+ // a cache of loaded files so we don't keep loading the same file multiple times
+ // in production mode the same file might contain multiple 'empty' nodes
+ // and they can be large, and require loading from disk multiple times would slow
+ // it all down.
+ Dictionary _loadedFiles = [];
+
// we only do deletes here.
foreach (var action in actions.Where(x => x.Change == ChangeType.Hidden))
{
if (action.FileName is null) continue;
- results.AddRange(await ImportAsync(action.FileName, config, options));
+ if (syncFileService.FileExists(action.FileName) is false) continue;
+
+ // load the file if we haven't already
+ if (_loadedFiles.TryGetValue(action.FileName, out XElement? xml) is false)
+ _loadedFiles[action.FileName] = await syncFileService.LoadXElementAsync(action.FileName);
+
+ if (_loadedFiles[action.FileName].Name.LocalName.Equals(CoreConstants.Serialization.Empty) is true)
+ {
+ // single
+ results.AddRange(await ImportSingleElementAsync(_loadedFiles[action.FileName], action.FileName, config, options));
+ }
+ else
+ {
+ // multiple ?
+ var node = _loadedFiles[action.FileName].XPathSelectElement($"//{CoreConstants.Serialization.Empty}[@Key='{action.Key}']");
+ if (node is null) continue;
+
+ results.AddRange(await ImportSingleElementAsync(node, action.FileName, config, options));
+ }
}
+ _loadedFiles.Clear();
+
results.AddRange(await CleanFoldersAsync(Guid.Empty));
return results;
diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs
index 05aaafc53..cdfaac4b8 100644
--- a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs
+++ b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs
@@ -169,6 +169,16 @@ public abstract class SyncHandlerRoot
///
protected readonly IShortStringHelper shortStringHelper;
+ ///
+ /// The serializer's item type (this is what the xml-node name will be).
+ ///
+ public string? GetSerializerType() => serializer.ItemType;
+
+ ///
+ /// tracker used by the serializer.
+ ///
+ public ISyncTrackerBase? GetBaseTracker() => trackers.FirstOrDefault() as ISyncTrackerBase;
+
///
/// Constructor, base for all handlers
///
@@ -268,11 +278,12 @@ public async Task> ImportAllAsync(string[] folders, Han
int count = 0;
int total = items.Count;
+ options.Callbacks?.SetRange?.Invoke(count, total);
+
foreach (var item in items)
{
count++;
- options.Callbacks?.Update?.Invoke($"Importing {Path.GetFileNameWithoutExtension(item.Path)}", count, total);
var result = await ImportElementAsync(item.Node, item.FileName, config, options);
foreach (var attempt in result)
@@ -435,13 +446,32 @@ virtual public async Task> ImportAsync(string file, Han
return await ImportAsync(file, config, options);
}
+ ///
+ virtual public async Task> ImportElementAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options)
+ {
+ if (node.Name.LocalName == this.serializer.ItemType + "s")
+ {
+ var actions = new List();
+ var elements = node.Elements().ToList();
+ options.Callbacks?.SetRange?.Invoke(0, elements.Count);
+ foreach (var item in elements)
+ {
+ actions.AddRange(await ImportSingleElementAsync(new XElement(item), filename, settings, options));
+ }
+ return actions;
+ }
+
+ return await ImportSingleElementAsync(node, filename, settings, options);
+ }
+
///
- /// Import a node, with settings and options
+ /// import a single XElement into umbraco.
///
///
- /// All Imports lead here
+ /// if the XElement contains multiple entries, then this method will not import them, if there is a possibility of that
+ /// then the ImportElementAsync method should be used - which splits them before loading this call.
///
- virtual public async Task> ImportElementAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options)
+ virtual protected async Task> ImportSingleElementAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options)
{
if (!await ShouldImportAsync(node, settings))
{
@@ -459,6 +489,8 @@ virtual public async Task> ImportElementAsync(XElement
try
{
+ options.Callbacks?.IncrementalUpdate?.Invoke(node.GetAlias());
+
// merge the options from the handler and any import options into our serializer options.
var serializerOptions = new SyncSerializerOptions(options.Flags, settings.Settings, options.UserId);
serializerOptions.MergeSettings(options.Settings);
@@ -1191,9 +1223,28 @@ protected virtual async Task> ReportFolderAsync(string
}
///
- /// Report on any changes for a single XML node.
+ /// Report on any changes for a single XML node. (may contain multiple items in a single node).
///
public virtual async Task> ReportElementAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options)
+ {
+ if (node.Name.LocalName == this.serializer.ItemType + "s")
+ {
+ var actions = new List();
+ foreach (var item in node.Elements())
+ {
+ var cleanItem = new XElement(item);
+ actions.AddRange(await ReportElementSingleAsync(cleanItem, filename, settings, options));
+ }
+ return actions;
+ }
+
+ return await ReportElementSingleAsync(node, filename, settings, options);
+ }
+
+ ///
+ /// Report on any changes for a single XML node.
+ ///
+ public virtual async Task> ReportElementSingleAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options)
{
try
{
diff --git a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs
index c5c819bbc..eefa99efe 100644
--- a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs
+++ b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs
@@ -103,6 +103,9 @@ public static IUmbracoBuilder AdduSync(this IUmbracoBuilder builder, Action(StatusCodes.Status200OK)]
+ public async Task MergeExportFolder()
+ {
+ var folders = _configService.GetFolders();
+ var handlers = _handlerFactory.GetValidHandlers(new BackOffice.SyncHandlers.Models.SyncHandlerOptions { Set = _configService.Settings.DefaultSet });
+ var result = await _syncService.MergeExportFolder(folders, handlers);
+ return Ok(result);
+ }
+}
diff --git a/uSync.Backoffice.Management.Api/Controllers/Actions/uSyncActionsController.cs b/uSync.Backoffice.Management.Api/Controllers/Actions/uSyncActionsController.cs
index 0de5a0a02..b4bfb55ae 100644
--- a/uSync.Backoffice.Management.Api/Controllers/Actions/uSyncActionsController.cs
+++ b/uSync.Backoffice.Management.Api/Controllers/Actions/uSyncActionsController.cs
@@ -33,7 +33,7 @@ public async Task> GetActions()
public async Task> GetActionsBySet(string setName)
{
return await Task.FromResult(_syncManagementService.GetActions(setName));
- }
+ }
}
diff --git a/uSync.Backoffice.Management.Client/usync-assets/src/workspace/views/settings/settings.element.ts b/uSync.Backoffice.Management.Client/usync-assets/src/workspace/views/settings/settings.element.ts
index 80767a0bd..b3be1d3c7 100644
--- a/uSync.Backoffice.Management.Client/usync-assets/src/workspace/views/settings/settings.element.ts
+++ b/uSync.Backoffice.Management.Client/usync-assets/src/workspace/views/settings/settings.element.ts
@@ -60,7 +60,7 @@ export class USyncSettingsViewElement extends UmbElementMixin(LitElement) {
.value=${this.settings?.exportAtStartup}>
diff --git a/uSync.Core/Serialization/SyncSerializerRoot.cs b/uSync.Core/Serialization/SyncSerializerRoot.cs
index cfac15cd2..017225de8 100644
--- a/uSync.Core/Serialization/SyncSerializerRoot.cs
+++ b/uSync.Core/Serialization/SyncSerializerRoot.cs
@@ -281,7 +281,7 @@ public virtual Task> SerializeEmptyAsync(TObject item, Syn
var node = XElementExtensions.MakeEmpty(ItemKey(item), change, alias);
- return Task.FromResult(SyncAttempt.Succeed("Empty", node, ChangeType.Removed, []));
+ return Task.FromResult(SyncAttempt.Succeed(uSyncConstants.Serialization.Empty, node, ChangeType.Removed, []));
}
diff --git a/uSync.Core/uSyncConstants.cs b/uSync.Core/uSyncConstants.cs
index bf84c7661..3057ba3d6 100644
--- a/uSync.Core/uSyncConstants.cs
+++ b/uSync.Core/uSyncConstants.cs
@@ -49,6 +49,9 @@ public static class Serialization
public const string Domain = "Domain";
+ ///
+ /// action files are 'empty' with an action to say what they do.
+ ///
public const string Empty = "Empty";
public const string RelationType = "RelationType";