diff --git a/Directory.Build.props b/Directory.Build.props index b2227c76..1bd4721a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 13.0.0 + 14.0.0 Kevin Jump Jumoo @@ -15,11 +15,11 @@ usync-logo.png - CS0618 + enable + +13.o : Umbraco v13 release +14.0 : Umbraco v14 release]]> true diff --git a/dist/build-package.ps1 b/dist/build-package.ps1 index 1f216dde..d66778d5 100644 --- a/dist/build-package.ps1 +++ b/dist/build-package.ps1 @@ -101,7 +101,7 @@ XCOPY "$outFolder\*.nupkg" "C:\Source\localgit" /Q /Y if ($push) { ""; "##### Pushing to our nighly package feed"; "----------------------------------" ; "" - nuget push "$outFolder\*.nupkg" -ApiKey AzureDevOps -src https://pkgs.dev.azure.com/jumoo/Public/_packaging/nightly/nuget/v3/index.json + .\nuget.exe push "$outFolder\*.nupkg" -ApiKey AzureDevOps -src https://pkgs.dev.azure.com/jumoo/Public/_packaging/nightly/nuget/v3/index.json Remove-Item ".\last-push-*" Out-File -FilePath ".\last-push-$fullVersion.txt" -InputObject $fullVersion diff --git a/uSync.AutoTemplates/AutoTemplateComposer.cs b/uSync.AutoTemplates/AutoTemplateComposer.cs index 6cdab42a..c056436f 100644 --- a/uSync.AutoTemplates/AutoTemplateComposer.cs +++ b/uSync.AutoTemplates/AutoTemplateComposer.cs @@ -1,13 +1,12 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; -namespace uSync.AutoTemplates +namespace uSync.AutoTemplates; + +public class AutoTemplateComposer : IComposer { - public class AutoTemplateComposer : IComposer + public void Compose(IUmbracoBuilder builder) { - public void Compose(IUmbracoBuilder builder) - { - builder.AdduSyncAutoTemplates(); - } + builder.AdduSyncAutoTemplates(); } } diff --git a/uSync.AutoTemplates/AutoTemplateNotificationHandler.cs b/uSync.AutoTemplates/AutoTemplateNotificationHandler.cs index 06afda8b..68e083a8 100644 --- a/uSync.AutoTemplates/AutoTemplateNotificationHandler.cs +++ b/uSync.AutoTemplates/AutoTemplateNotificationHandler.cs @@ -2,44 +2,43 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Notifications; -namespace uSync.AutoTemplates +namespace uSync.AutoTemplates; + +public class AutoTemplateNotificationHandler : + INotificationHandler, + INotificationHandler { - public class AutoTemplateNotificationHandler : - INotificationHandler, - INotificationHandler - { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly TemplateWatcher _templateWatcher; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly TemplateWatcher _templateWatcher; - public AutoTemplateNotificationHandler( - IHostingEnvironment hostingEnvironment, - TemplateWatcher templateWatcher) - { - _hostingEnvironment = hostingEnvironment; - _templateWatcher = templateWatcher; - } + public AutoTemplateNotificationHandler( + IHostingEnvironment hostingEnvironment, + TemplateWatcher templateWatcher) + { + _hostingEnvironment = hostingEnvironment; + _templateWatcher = templateWatcher; + } - public void Handle(UmbracoApplicationStartingNotification notification) - { - // we only run in debug mode. - if (!_hostingEnvironment.IsDebugMode) return; + public void Handle(UmbracoApplicationStartingNotification notification) + { + // we only run in debug mode. + if (!_hostingEnvironment.IsDebugMode) return; - // we only run when Umbraco is setup. - if (notification.RuntimeLevel == Umbraco.Cms.Core.RuntimeLevel.Run) - { - _templateWatcher.CheckViewsFolder(); - _templateWatcher.WatchViewsFolder(); - } + // we only run when Umbraco is setup. + if (notification.RuntimeLevel == Umbraco.Cms.Core.RuntimeLevel.Run) + { + _templateWatcher.CheckViewsFolder(); + _templateWatcher.WatchViewsFolder(); } + } - public void Handle(TemplateSavingNotification notification) + public void Handle(TemplateSavingNotification notification) + { + foreach (var item in notification.SavedEntities) { - foreach (var item in notification.SavedEntities) - { - // tells the watcher this has been saved in umbraco. - _templateWatcher.QueueChange(item.Alias); - } + // tells the watcher this has been saved in umbraco. + _templateWatcher.QueueChange(item.Alias); } } } diff --git a/uSync.AutoTemplates/AutoTemplates.cs b/uSync.AutoTemplates/AutoTemplates.cs index db1c82ba..65d03364 100644 --- a/uSync.AutoTemplates/AutoTemplates.cs +++ b/uSync.AutoTemplates/AutoTemplates.cs @@ -1,11 +1,10 @@ -namespace uSync.AutoTemplates +namespace uSync.AutoTemplates; + +public class AutoTemplates { - public class AutoTemplates - { - /// - /// the regex to fine the layout file in a .cshtml file - /// - public const string LayoutRegEx = "Layout\\s*=\\s*(\"*[A-z.]+\"*)"; + /// + /// the regex to fine the layout file in a .cshtml file + /// + public const string LayoutRegEx = "Layout\\s*=\\s*(\"*[A-z.]+\"*)"; - } } diff --git a/uSync.AutoTemplates/AutoTemplatesBuilderExtensions.cs b/uSync.AutoTemplates/AutoTemplatesBuilderExtensions.cs index 406d6adc..9012db25 100644 --- a/uSync.AutoTemplates/AutoTemplatesBuilderExtensions.cs +++ b/uSync.AutoTemplates/AutoTemplatesBuilderExtensions.cs @@ -5,22 +5,21 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Extensions; -namespace uSync.AutoTemplates -{ - public static class AutoTemplatesBuilderExtensions +namespace uSync.AutoTemplates; + +public static class AutoTemplatesBuilderExtensions { - public static IUmbracoBuilder AdduSyncAutoTemplates(this IUmbracoBuilder builder) - { - // check to see if we've been registerd before. - if (builder.Services.FindIndex(x => x.ServiceType == typeof(TemplateWatcher)) != -1) - return builder; + public static IUmbracoBuilder AdduSyncAutoTemplates(this IUmbracoBuilder builder) + { + // check to see if we've been registerd before. + if (builder.Services.FindIndex(x => x.ServiceType == typeof(TemplateWatcher)) != -1) + return builder; - builder.Services.AddSingleton(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.Services.AddSingleton(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); - return builder; + return builder; - } } } diff --git a/uSync.AutoTemplates/TemplateWatcher.cs b/uSync.AutoTemplates/TemplateWatcher.cs index fb6a3d7e..cd7015f4 100644 --- a/uSync.AutoTemplates/TemplateWatcher.cs +++ b/uSync.AutoTemplates/TemplateWatcher.cs @@ -1,15 +1,14 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -using System; +using System; using System.Collections.Concurrent; using System.IO; -using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + using Umbraco.Cms.Core; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Hosting; @@ -19,253 +18,258 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace uSync.AutoTemplates +namespace uSync.AutoTemplates; + +public class TemplateWatcher : IRegisteredObject { - public class TemplateWatcher : IRegisteredObject + private readonly IApplicationShutdownRegistry _hostingLifetime; + private readonly FileSystemWatcher _watcher; + private readonly ILogger _logger; + + private readonly IFileService _fileService; + private readonly IShortStringHelper _shortStringHelper; + private readonly IHostEnvironment _hostEnvironment; + + private readonly IFileSystem? _templateFileSystem; + + private readonly string _viewsFolder; + + private bool _enabled; + private bool _deleteMissing; + private int _delay; + + public TemplateWatcher( + IConfiguration configuration, + IApplicationShutdownRegistry hostingLifetime, + ILogger logger, + IFileService fileService, + FileSystems fileSystems, + IShortStringHelper shortStringHelper, + IHostEnvironment hostEnvironment) { - private readonly IApplicationShutdownRegistry _hostingLifetime; - private readonly FileSystemWatcher _watcher; - private readonly ILogger _logger; - - private readonly IFileService _fileService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IHostEnvironment _hostEnvironment; - - private readonly IFileSystem _templateFileSystem; - - private readonly string _viewsFolder; - - private bool _enabled; - private bool _deleteMissing; - private int _delay; - - public TemplateWatcher( - IConfiguration configuration, - IApplicationShutdownRegistry hostingLifetime, - ILogger logger, - IFileService fileService, - FileSystems fileSystems, - IShortStringHelper shortStringHelper, - IHostEnvironment hostEnvironment) - { - _fileService = fileService; - _shortStringHelper = shortStringHelper; - _hostEnvironment = hostEnvironment; - _hostingLifetime = hostingLifetime; - - _logger = logger; - - _templateFileSystem = fileSystems.MvcViewsFileSystem; - - _viewsFolder = _hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews); - - - // - // use appsettings.json to turn on/off. - // - // "uSync" : { - // "AutoTemplates" : { - // "Enabled": true, - // "Delete": false - // } - // } - // - - _enabled = configuration.GetValue("uSync:AutoTemplates:Enabled", false); // by default this might be off. - _deleteMissing = configuration.GetValue("uSync:AutoTemplates:Delete", false); - _delay = configuration.GetValue("uSync:AutoTemplates:Delay", 1000); // wait. - - _hostingLifetime.RegisterObject(this); - _watcher = new FileSystemWatcher(_viewsFolder); - _watcher.Changed += Watcher_FileChanged; - _watcher.Deleted += Watcher_FileDeleted; - _watcher.Renamed += Watcher_Renamed; - } - public void WatchViewsFolder() + _fileService = fileService; + _shortStringHelper = shortStringHelper; + _hostEnvironment = hostEnvironment; + _hostingLifetime = hostingLifetime; + + _logger = logger; + + _templateFileSystem = fileSystems.MvcViewsFileSystem; + + _viewsFolder = _hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews); + + + // + // use appsettings.json to turn on/off. + // + // "uSync" : { + // "AutoTemplates" : { + // "Enabled": true, + // "Delete": false + // } + // } + // + + _enabled = configuration.GetValue("uSync:AutoTemplates:Enabled", false); // by default this might be off. + _deleteMissing = configuration.GetValue("uSync:AutoTemplates:Delete", false); + _delay = configuration.GetValue("uSync:AutoTemplates:Delay", 1000); // wait. + + _hostingLifetime.RegisterObject(this); + _watcher = new FileSystemWatcher(_viewsFolder); + _watcher.Changed += Watcher_FileChanged; + _watcher.Deleted += Watcher_FileDeleted; + _watcher.Renamed += Watcher_Renamed; + } + public void WatchViewsFolder() + { + if (!_enabled) return; + _watcher.EnableRaisingEvents = true; + } + + private void Watcher_Renamed(object sender, RenamedEventArgs e) + { + if (_enabled is false || e.OldName is null || e.Name is null) return; + Thread.Sleep(_delay); + + var oldAlias = GetAliasFromFileName(e.OldName); + var template = _fileService.GetTemplate(oldAlias); + if (template != null) { - if (!_enabled) return; - _watcher.EnableRaisingEvents = true; + template.Alias = GetAliasFromFileName(e.Name); + template.Name = Path.GetFileNameWithoutExtension(e.Name); + _fileService.SaveTemplate(template); } + } - private void Watcher_Renamed(object sender, RenamedEventArgs e) - { - if (!_enabled) return; - Thread.Sleep(_delay); + private void Watcher_FileDeleted(object sender, FileSystemEventArgs e) + { + if (_enabled is false || _deleteMissing is false || e.Name is null) + return; - var oldAlias = GetAliasFromFileName(e.OldName); - var template = _fileService.GetTemplate(oldAlias); - if (template != null) - { - template.Alias = GetAliasFromFileName(e.Name); - template.Name = Path.GetFileNameWithoutExtension(e.Name); - _fileService.SaveTemplate(template); - } - } + Thread.Sleep(_delay); + var alias = GetAliasFromFileName(e.Name); + SafeDeleteTemplate(alias); - private void Watcher_FileDeleted(object sender, FileSystemEventArgs e) - { - if (_enabled && _deleteMissing) - { - Thread.Sleep(_delay); - var alias = GetAliasFromFileName(e.Name); - SafeDeleteTemplate(alias); - } - } + } - private void Watcher_FileChanged(object sender, FileSystemEventArgs e) - { - if (!_enabled) return; - Thread.Sleep(_delay); + private void Watcher_FileChanged(object sender, FileSystemEventArgs e) + { + if (_enabled is false || e.Name is null) return; - CheckFile(e.Name); - } + Thread.Sleep(_delay); + CheckFile(e.Name); + } - public void CheckViewsFolder() - { - if (!_enabled) return; + public void CheckViewsFolder() + { + if (_enabled is false) return; - var views = _templateFileSystem.GetFiles(".", "*.cshtml"); + var views = _templateFileSystem?.GetFiles(".", "*.cshtml") ?? []; - foreach (var view in views) - { - CheckFile(view); - } + foreach (var view in views) + { + CheckFile(view); + } - if (_deleteMissing) - { - CheckTemplates(); - } + if (_deleteMissing) + { + CheckTemplates(); } + } - private void CheckTemplates() + private void CheckTemplates() + { + var templates = _fileService.GetTemplates(); + foreach (var template in templates) { - var templates = _fileService.GetTemplates(); - foreach (var template in templates) + if (!_templateFileSystem?.FileExists(template.Alias + ".cshtml") is true) { - if (!_templateFileSystem.FileExists(template.Alias + ".cshtml")) - { - SafeDeleteTemplate(template.Alias); - } + SafeDeleteTemplate(template.Alias); } } + } - private static object lockObject = new Object(); + private static object lockObject = new Object(); - private void CheckFile(string filename) + private void CheckFile(string filename) + { + try { - try - { - if (!_templateFileSystem.FileExists(filename)) return; + if (_templateFileSystem?.FileExists(filename) is false) return; - _logger.LogInformation("Checking {filename} template", filename); + _logger.LogInformation("Checking {filename} template", filename); - var fileAlias = GetAliasFromFileName(filename); + var fileAlias = GetAliasFromFileName(filename); - // is this from a save inside umbraco ? - // sometimes there can be a double trigger - - if (IsQueued(fileAlias)) return; + // is this from a save inside umbraco ? + // sometimes there can be a double trigger - + if (IsQueued(fileAlias)) return; - lock (lockObject) - { + lock (lockObject) + { - var text = GetFileContents(filename); - var match = Regex.Match(text, AutoTemplates.LayoutRegEx); + var text = GetFileContents(filename); + var match = Regex.Match(text, AutoTemplates.LayoutRegEx); - if (match == null || match.Groups.Count != 2) return; + if (match == null || match.Groups.Count != 2) return; - var layoutFile = match.Groups[1].Value.Trim('"'); + var layoutFile = match.Groups[1].Value.Trim('"'); - var fileMasterAlias = GetAliasFromFileName(layoutFile); + var fileMasterAlias = GetAliasFromFileName(layoutFile); - var template = _fileService.GetTemplate(fileAlias); + var template = _fileService.GetTemplate(fileAlias); - if (template != null) - { - var currentMaster = string.IsNullOrWhiteSpace(template.MasterTemplateAlias) ? "null" : template.MasterTemplateAlias; - if (fileMasterAlias != currentMaster) - { - template.SetMasterTemplate(GetMasterTemplate(fileMasterAlias)); - SafeSaveTemplate(template); - return; - } - } - else + if (template != null) + { + var currentMaster = string.IsNullOrWhiteSpace(template.MasterTemplateAlias) ? "null" : template.MasterTemplateAlias; + if (fileMasterAlias != currentMaster) { - // doesn't exist - template = new Template(_shortStringHelper, Path.GetFileNameWithoutExtension(filename), fileAlias); template.SetMasterTemplate(GetMasterTemplate(fileMasterAlias)); - template.Content = text; - SafeSaveTemplate(template); return; } } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Exeption while checking template file {filename}", filename); + else + { + // doesn't exist + template = new Template(_shortStringHelper, Path.GetFileNameWithoutExtension(filename), fileAlias); + template.SetMasterTemplate(GetMasterTemplate(fileMasterAlias)); + template.Content = text; + + SafeSaveTemplate(template); + return; + } } } - - private ITemplate GetMasterTemplate(string alias) + catch (Exception ex) { - if (alias == "null") return null; - return _fileService.GetTemplate(alias); + _logger.LogWarning(ex, "Exception while checking template file {filename}", filename); } + } - private void SafeDeleteTemplate(string alias) + private ITemplate? GetMasterTemplate(string alias) + { + if (alias == "null") return null; + return _fileService.GetTemplate(alias); + } + + private void SafeDeleteTemplate(string alias) + { + try { - try - { - _fileService.DeleteTemplate(alias); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Exeption while attempting to delete template {alias}", alias); - } + _fileService.DeleteTemplate(alias); } - - private void SafeSaveTemplate(ITemplate template) + catch (Exception ex) { - _watcher.EnableRaisingEvents = false; - _fileService.SaveTemplate(template); - _watcher.EnableRaisingEvents = true; + _logger.LogWarning(ex, "Exception while attempting to delete template {alias}", alias); } + } + private void SafeSaveTemplate(ITemplate template) + { + _watcher.EnableRaisingEvents = false; + _fileService.SaveTemplate(template); + _watcher.EnableRaisingEvents = true; + } - private string GetFileContents(string filename) + + private string GetFileContents(string filename) + { + using (Stream? stream = _templateFileSystem?.OpenFile(filename)) { - using (Stream stream = _templateFileSystem.OpenFile(filename)) + + if (stream is null) return string.Empty; + using (var reader = new StreamReader(stream, Encoding.UTF8, true)) { return reader.ReadToEnd(); } } + } - private string GetAliasFromFileName(string filename) - => Path.GetFileNameWithoutExtension(filename).ToSafeAlias(_shortStringHelper, false); + private string GetAliasFromFileName(string filename) + => Path.GetFileNameWithoutExtension(filename).ToSafeAlias(_shortStringHelper, false); - public void Stop(bool immediate) - { - _hostingLifetime.UnregisterObject(this); - } + public void Stop(bool immediate) + { + _hostingLifetime.UnregisterObject(this); + } - ConcurrentDictionary QueuedItems = new ConcurrentDictionary(); + ConcurrentDictionary QueuedItems = new ConcurrentDictionary(); - public void QueueChange(string alias) - { - QueuedItems.TryAdd(alias.ToLower(), true); - } + public void QueueChange(string alias) + { + QueuedItems.TryAdd(alias.ToLower(), true); + } - public bool IsQueued(string alias) - { - if (QueuedItems.TryRemove(alias.ToLower(), out bool flag)) - return flag; + public bool IsQueued(string alias) + { + if (QueuedItems.TryRemove(alias.ToLower(), out bool flag)) + return flag; - return false; - } + return false; } } diff --git a/uSync.AutoTemplates/packages.lock.json b/uSync.AutoTemplates/packages.lock.json index e257103f..0f7adce5 100644 --- a/uSync.AutoTemplates/packages.lock.json +++ b/uSync.AutoTemplates/packages.lock.json @@ -2,231 +2,25 @@ "version": 1, "dependencies": { "net8.0": { - "Umbraco.Cms.Web.BackOffice": { + "Umbraco.Cms.Core": { "type": "Direct", - "requested": "[14.0.0--preview006, )", - "resolved": "14.0.0--preview006", - "contentHash": "CNliwq77WLyh5D5B31qIf3TzEIkSXWGLEscTDw39SGIkzl1FMP9+Own9zrkhelzuG5qt/Z+iyRPppb/Tq7Tirw==", - "dependencies": { - "Newtonsoft.Json": "13.0.3", - "Serilog.AspNetCore": "8.0.1", - "Umbraco.Cms.Web.Common": "[14.0.0--preview006, 15.0.0)" - } - }, - "Asp.Versioning.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "YkuUcrqi862hw/p8dKCsfOQ6y2mWTfjKHuQoFUA9GOaoBGZsu/FzsoBm6z28WkQIlXCZR1SG7safgHj2WCO/lw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Asp.Versioning.Http": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "pxHp26pXoCCIFUxFZz4UWPuYDFJ3PB0QpTpC4QRoLhu90eE3FBs0zmDeRO6Sb3y6hMnk6efdQHGbyCzd7XQIrA==", - "dependencies": { - "Asp.Versioning.Abstractions": "8.0.0" - } - }, - "Asp.Versioning.Mvc": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "zxuA7J6HewdjFF/9Cfb6VWovBTj3MdmLg6PztInratlXGpJ+BZjQzoT8FEMOurzmCxbEFPlQmeMW7b1iJKfsdg==", - "dependencies": { - "Asp.Versioning.Http": "8.0.0" - } - }, - "Asp.Versioning.Mvc.ApiExplorer": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "jrNwSdQ/FNGbwnb6SpNRuAZpJ5qrlSrEwOWaXFLc8vCC14bo/2ihwJ3IJWRbSnijLUyB9j9eKFVcVi1l8ShzZw==", - "dependencies": { - "Asp.Versioning.Mvc": "8.0.0" - } - }, - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ==" - }, - "Dazinator.Extensions.FileProviders": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "Jb10uIvdGdaaOmEGUXeO1ssjp6YuvOuR87B5gLxGORFbroV1j7PDaVfEIgni7vV8KRcyAY5KvuMxgx6ADIEXNw==", - "dependencies": { - "DotNet.Glob": "3.1.0", - "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.2", - "Microsoft.AspNetCore.Http.Abstractions": "1.0.2", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "NETStandard.Library": "1.6.1" - } - }, - "DotNet.Glob": { - "type": "Transitive", - "resolved": "3.1.0", - "contentHash": "i6x0hDsFWg6Ke2isaNAcHQ9ChxBvTJu2cSmBY+Jtjiv2W4q6y9QlA3JKYuZqJ573TAZmpAn65Qf3sRpjvZ1gmw==" - }, - "Examine": { - "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "WL6VfLVO6It7kvwWANUux9LerwNr+xjxHHenNbxlOZE0dMcBKs0C3EYHEk6DHmDk0EtAPRcXT23NKlcJ7ZskWw==", - "dependencies": { - "Examine.Core": "3.2.0", - "Examine.Lucene": "3.2.0", - "Microsoft.AspNetCore.DataProtection": "5.0.5", - "Microsoft.Extensions.DependencyInjection.Abstractions": "5.0.0" - } - }, - "Examine.Core": { - "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "2f8pnvZf8COTyBcO3c3z8XR/sc6HqtE45922dwTEe7dCM1H5eoItUHpQ38SM3zX9sXKA2hHUJowggxyoYrPS0g==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "5.0.0", - "Microsoft.Extensions.Options": "5.0.0" - } - }, - "Examine.Lucene": { - "type": "Transitive", - "resolved": "3.2.0", - "contentHash": "Rm9WVnGlOBOyvkmjWB9+BhTJPNjHwA34Pk/Q6LMYDQujn6kFpBLK//5gEVqPGvU33du0oPTK1BN5rjuqJJq/JQ==", - "dependencies": { - "Examine.Core": "3.2.0", - "Lucene.Net.QueryParser": "4.8.0-beta00016", - "Lucene.Net.Replicator": "4.8.0-beta00016", - "System.Threading": "4.3.0", - "System.Threading.AccessControl": "4.7.0" - } - }, - "HtmlAgilityPack": { - "type": "Transitive", - "resolved": "1.11.57", - "contentHash": "zDxnHcAvi+qhZG602eKaPJKmzm0T8npKVML0RMwjrRBabpmTRtu2OVpfNkMUfYMgQ+5EllOkSeGxu0uFVV2zFw==" - }, - "J2N": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "M5bwDajAARZiyqupU+rHQJnsVLxNBOHJ8vKYHd8LcLIb1FgLfzzcJvc31Qo5Xz/GEHFjDF9ScjKL/ks/zRTXuA==" - }, - "K4os.Compression.LZ4": { - "type": "Transitive", - "resolved": "1.3.6", - "contentHash": "RxGhoJBjZCgGeZgDqOP4Krs1cR9PHInbz6d2N19Dic0Y6ZACzVKbR3uSpqfEZf4RiUbHk9aiog2eS22nQPTc2A==" - }, - "Lucene.Net": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "DCtUbE/NIrisNI7hRwU+UKS3Cr6S2vH1XB9wvEHHI3anu5OUpX1Fkr/PDC7oFCaol/QCvzVLbLZVizAT1aTLpA==", - "dependencies": { - "J2N": "2.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "2.0.0" - } - }, - "Lucene.Net.Analysis.Common": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "7pjEAIliWdih6E3I0hCE8hKcKKRx1LLzeQBslF1fhvzE1Sal4NyHd8RFJHV1Z+yHlBw4gCyyVIDZADiIoyqwxg==", - "dependencies": { - "Lucene.Net": "4.8.0-beta00016" - } - }, - "Lucene.Net.Facet": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "O1MrRfhb9BMfRQHooyEFrkgNwYbTEbK/AkKhz26sy+xO+zAldJ8YKS/IsydHsE+frklIAWT0jyv0c3Dh9qBXSA==", - "dependencies": { - "Lucene.Net.Join": "4.8.0-beta00016", - "Lucene.Net.Queries": "4.8.0-beta00016" - } - }, - "Lucene.Net.Grouping": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "y7QSEYfSnz7gEJS30xHsf8P0oMIreGGO08qC+UzKre29IAoUXdLLE2+vUfByGkcPuoGMIpZVBP51P6O647grBg==", + "requested": "[14.0.0-beta001, )", + "resolved": "14.0.0-beta001", + "contentHash": "W+GVz5RhAntIV40cQ6xifO/DWvfP1PopHoomqwgrI214aMajIWTEc64i+Lkd+lpdDCmk2bnDiyIt/sx7mI03IQ==", "dependencies": { - "Lucene.Net": "4.8.0-beta00016", - "Lucene.Net.Queries": "4.8.0-beta00016" - } - }, - "Lucene.Net.Join": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "trUiWhV3QPgW4TNPrEP29AsTXE29ACR5+Vz22xjbPtFTwyXMozl95NELVG5aUVMTqdwyMhJ9Lj82QeoHDnN0jw==", - "dependencies": { - "Lucene.Net.Grouping": "4.8.0-beta00016" - } - }, - "Lucene.Net.Queries": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "XBzdMDlan68V2ZlhAlP8Fd+Xx2Le8ec7cEN1kFF45Sipa3Q8L/tilJfwS9VHvMTvGkwPM/yj62eGbfGBgIMR8Q==", - "dependencies": { - "Lucene.Net": "4.8.0-beta00016" - } - }, - "Lucene.Net.QueryParser": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "5dVvjXmzPaK8GD/eblJopTJMQmO6c6fvVPfBIOw46+jyZR+yESkUnWF1LtLoLXZQNrl4Dx8LKdes5G1QAM7eGA==", - "dependencies": { - "Lucene.Net.Analysis.Common": "4.8.0-beta00016", - "Lucene.Net.Queries": "4.8.0-beta00016", - "Lucene.Net.Sandbox": "4.8.0-beta00016" - } - }, - "Lucene.Net.Replicator": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "BP007m7TtHfOFNGoipn1Y3kgHir0yvDfyCW9g7P6PQIo7nNkyyHuEK9slVEkPhLq+21Q2EnnHl7jMGeh0aK2eA==", - "dependencies": { - "J2N": "2.0.0", - "Lucene.Net": "4.8.0-beta00016", - "Lucene.Net.Facet": "4.8.0-beta00016", - "Newtonsoft.Json": "10.0.1" - } - }, - "Lucene.Net.Sandbox": { - "type": "Transitive", - "resolved": "4.8.0-beta00016", - "contentHash": "wMsRZtbNx0wvX3mtNjpOwQmKx3Ij4UGHWIYHbvnzMWlPUTgtOpYSj02REL4hOxI71WBZylpGB5EWfQ2eEld63g==", - "dependencies": { - "Lucene.Net": "4.8.0-beta00016" - } - }, - "MailKit": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "jVmB3Nr0JpqhyMiXOGWMin+QvRKpucGpSFBCav9dG6jEJPdBV+yp1RHVpKzxZPfT+0adaBuZlMFdbIciZo1EWA==", - "dependencies": { - "MimeKit": "4.3.0" - } - }, - "Markdown": { - "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "A6veXuFP1n50RbmFNtTgfHxnHmwMsgFLSCgS1xWbg5L8n5N6HFEksTlXocZ0LsmGW4leBzeLJd+BY7+g83zFJA==", - "dependencies": { - "System.Collections": "4.0.11", - "System.Runtime.Extensions": "4.1.0", - "System.Text.RegularExpressions": "4.1.0" - } - }, - "MessagePack": { - "type": "Transitive", - "resolved": "2.5.140", - "contentHash": "nkIsgy8BkIfv40bSz9XZb4q//scI1PF3AYeB5X66nSlIhBIqbdpLz8Qk3gHvnjV3RZglQLO/ityK3eNfLii2NA==", - "dependencies": { - "MessagePack.Annotations": "2.5.140", - "Microsoft.NET.StringTools": "17.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "Microsoft.Extensions.Caching.Abstractions": "8.0.0", + "Microsoft.Extensions.Caching.Memory": "8.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Embedded": "8.0.1", + "Microsoft.Extensions.FileProviders.Physical": "8.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", + "Microsoft.Extensions.Identity.Core": "8.0.1", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Options": "8.0.1", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", + "Microsoft.Extensions.Options.DataAnnotations": "8.0.0" } }, - "MessagePack.Annotations": { - "type": "Transitive", - "resolved": "2.5.140", - "contentHash": "JE3vwluOrsJ4t3hnfXzIxJUh6lhx6M/KR8Sark/HOUw1DJ5UKu5JsAnnuaQngg6poFkRx1lzHSLTkxHNJO7+uQ==" - }, "Microsoft.AspNetCore.Cryptography.Internal": { "type": "Transitive", "resolved": "8.0.1", @@ -240,152 +34,6 @@ "Microsoft.AspNetCore.Cryptography.Internal": "8.0.1" } }, - "Microsoft.AspNetCore.DataProtection": { - "type": "Transitive", - "resolved": "5.0.5", - "contentHash": "fYCIRLS3Q7eokBwzlcaKQnCBLDFXqjnyJO9lqOX0/V9zvy/JiOfvwKSkm6v5QJuNpXZywb/DnAq5Pdb3woc3MQ==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "5.0.5", - "Microsoft.AspNetCore.DataProtection.Abstractions": "5.0.5", - "Microsoft.Extensions.DependencyInjection.Abstractions": "5.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "5.0.0", - "Microsoft.Extensions.Logging.Abstractions": "5.0.0", - "Microsoft.Extensions.Options": "5.0.0", - "Microsoft.Win32.Registry": "5.0.0", - "System.Security.Cryptography.Xml": "5.0.0" - } - }, - "Microsoft.AspNetCore.DataProtection.Abstractions": { - "type": "Transitive", - "resolved": "5.0.5", - "contentHash": "k1DgnNSBG0lf9P+QDnU+FFeLI4b4hhw4iT+iw29XkcRaCGpcPwq7mLJUtz2Yqq/FRyEwlcteTJmdWEoJb0Fxag==" - }, - "Microsoft.AspNetCore.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "CSVd9h1TdWDT2lt62C4FcgaF285J4O3MaOqTVvc7xP+3bFiwXcdp6qEd+u1CQrdJ+xJuslR+tvDW7vWQ/OH5Qw==", - "dependencies": { - "Microsoft.AspNetCore.Hosting.Server.Abstractions": "1.0.2", - "Microsoft.AspNetCore.Http.Abstractions": "1.0.2", - "Microsoft.Extensions.Configuration.Abstractions": "1.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", - "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", - "Microsoft.Extensions.Logging.Abstractions": "1.0.2" - } - }, - "Microsoft.AspNetCore.Hosting.Server.Abstractions": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "6ZtFh0huTlrUl72u9Vic0icCVIQiEx7ULFDx3P7BpOI97wjb0GAXf8B4m9uSpSGf0vqLEKFlkPbvXF0MXXEzhw==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "1.0.2", - "Microsoft.Extensions.Configuration.Abstractions": "1.0.2" - } - }, - "Microsoft.AspNetCore.Http.Abstractions": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "peJqc7BgYwhTzOIfFHX3/esV6iOXf17Afekh6mCYuUD3aWyaBwQuWYaKLR+RnjBEWaSzpCDgfCMMp5Y3LUXsiA==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "1.0.2", - "System.Globalization.Extensions": "4.0.1", - "System.Linq.Expressions": "4.1.1", - "System.Reflection.TypeExtensions": "4.1.0", - "System.Runtime.InteropServices": "4.1.0", - "System.Text.Encodings.Web": "4.0.0" - } - }, - "Microsoft.AspNetCore.Http.Features": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "6sVnhFwtsjEVL09FsYpAttQ3Og6Jxg1dQFLF9XQUThi1myq64imjhj1swd92TXMLCp5wmt8szDixZXXdx64qhg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "5.0.0", - "System.IO.Pipelines": "5.0.0" - } - }, - "Microsoft.AspNetCore.JsonPatch": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "Zq13zrOOnDs6PZRlu3sXVEZ1QGbJj7Fw48UtC/ZYIWZ18T8Jkjo7OodzYXSaJgDAXAtDoakvo83N8Mjx7EI9Gg==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "YWNvdHGCHGWKILgEzUDe6soozYnknlSB3IY092zxjdgLoaCPRte2lnbRRS7Nt0lEFbsFjN/Eo2fCI5yusPK0iQ==", - "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "8.0.1", - "Newtonsoft.Json": "13.0.3", - "Newtonsoft.Json.Bson": "1.0.2" - } - }, - "Microsoft.AspNetCore.Mvc.Razor.Extensions": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "M0h+ChPgydX2xY17agiphnAVa/Qh05RAP8eeuqGGhQKT10claRBlLNO6d2/oSV8zy0RLHzwLnNZm5xuC/gckGA==", - "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.0", - "Microsoft.CodeAnalysis.Razor": "6.0.0" - } - }, - "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "4Gy4Vz4koUBroyu1J57XIQ8zT6oOzggjn/mWc3YTpB/rnaR27Y+msTM8+/ecZ/V26KARrFq2HFylmKFQQtiD+A==", - "dependencies": { - "Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.0", - "Microsoft.CodeAnalysis.Razor": "6.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.0" - } - }, - "Microsoft.AspNetCore.Razor.Language": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "yCtBr1GSGzJrrp1NJUb4ltwFYMKHw/tJLnIDvg9g/FnkGIEzmE19tbCQqXARIJv5kdtBgsoVIdGLL+zmjxvM/A==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.4", - "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.Razor": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "uqdzuQXxD7XrJCbIbbwpI/LOv0PBJ9VIR0gdvANTHOfK5pjTaCir+XcwvYvBZ5BIzd0KGzyiamzlEWw1cK1q0w==", - "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.0", - "Microsoft.CodeAnalysis.CSharp": "4.0.0", - "Microsoft.CodeAnalysis.Common": "4.0.0" - } - }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -406,15 +54,6 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -431,30 +70,6 @@ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==", - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Physical": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "System.Text.Json": "8.0.0" - } - }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "8.0.0", @@ -468,25 +83,6 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "NSmDw3K0ozNDgShSIpsZcbFIzBX4w28nDag+TfaQujkXGazBm+lid5onlWoCBy4VsLxqnnKjEBbGSJVWJMf43g==", - "dependencies": { - "System.Text.Encodings.Web": "8.0.0", - "System.Text.Json": "8.0.0" - } - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" - } - }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -505,15 +101,6 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, - "Microsoft.Extensions.FileProviders.Composite": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "0IoXXfkgKpYJB1t2lC0jPXAxuaywRNc9y2Mq96ZZNKBthL38vusa2UK73+Bm6Kq/9a5xNHJS6NhsSN+i5TEtkA==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "5.0.0", - "Microsoft.Extensions.Primitives": "5.0.0" - } - }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Transitive", "resolved": "8.0.1", @@ -549,19 +136,6 @@ "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, - "Microsoft.Extensions.Http": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" - } - }, "Microsoft.Extensions.Identity.Core": { "type": "Transitive", "resolved": "8.0.1", @@ -572,16 +146,6 @@ "Microsoft.Extensions.Options": "8.0.1" } }, - "Microsoft.Extensions.Identity.Stores": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "yFfKr8NSb178uc8hA2k1Pqr8QB+dUCTbhesO4ooskPKor0ulCWvv7v9kmdCEqloIlxlO+8fuGnzSSvfRx80aeA==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "8.0.0", - "Microsoft.Extensions.Identity.Core": "8.0.1", - "Microsoft.Extensions.Logging": "8.0.0" - } - }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "8.0.0", @@ -635,1463 +199,10 @@ "resolved": "8.0.0", "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "7iSWSRR72VKeonFdfDi43Lvkca98Y0F3TmmWhRSuHbkjk/IKUSO0Qd272LQFZpi5eDNQNbUXy3o4THXhOAU6cw==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "6I35Kt2/PQZAyUYLo3+QgT/LubZ5/4Ojelkbyo8KKdDgjMbVocAx2B3P5V7iMCz+rsAe/RLr6ql87QKnHtI+aw==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "7.0.0" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "dxYqmmFLsjBQZ6F6a4XDzrZ1CTxBRFVigJvWiNtXiIsT6UlYMxs9ONMaGx9XKzcxmcgEQ2ADuCqKZduz0LR9Hw==", - "dependencies": { - "Microsoft.IdentityModel.Logging": "7.0.0" - } - }, - "Microsoft.NET.StringTools": { - "type": "Transitive", - "resolved": "17.6.3", - "contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.3", - "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "MimeKit": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "39KDXuERDy5VmHIn7NnCWvIVp/Ar4qnxZWg9m06DfRqDbW1B6zFv9o3Tdoa4CCu71tE/0SRqRCN5Z+bbffw6uw==", - "dependencies": { - "BouncyCastle.Cryptography": "2.2.1", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Security.Cryptography.Pkcs": "7.0.3", - "System.Text.Encoding.CodePages": "7.0.0" - } - }, - "MiniProfiler.AspNetCore": { - "type": "Transitive", - "resolved": "4.3.8", - "contentHash": "dohMvXpjKDPv/edl7gwKhq80JBqRLLRSwVJB9bo0UYqsgEox7BZyYS/4vBty+UsZ59pYYYhMUpUKHVWLLj/PBw==", - "dependencies": { - "MiniProfiler.Shared": "4.3.8" - } - }, - "MiniProfiler.AspNetCore.Mvc": { - "type": "Transitive", - "resolved": "4.3.8", - "contentHash": "aJ6Kkw2zMy36cKDWTjQYo/pJ6bhPBRA8z4NO8REe+xDhv8+fk58P526Bi52gnvsDp4jIVk5AQ8nQDgPUS/K+7A==", - "dependencies": { - "MiniProfiler.AspNetCore": "4.3.8" - } - }, - "MiniProfiler.Shared": { - "type": "Transitive", - "resolved": "4.3.8", - "contentHash": "SfXNX90fmDm373YAla0z06plTCj6YbByQJOm6G8/9kE6Hf4UALJxySyiMB9O4KYeTc6Ha1EFQDs6jLhio+bBFA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "2.0.0", - "Newtonsoft.Json": "13.0.1", - "System.ComponentModel.Primitives": "4.3.0", - "System.Data.Common": "4.3.0", - "System.Diagnostics.DiagnosticSource": "4.4.1", - "System.Diagnostics.StackTrace": "4.3.0", - "System.Dynamic.Runtime": "4.3.0", - "System.Reflection.Emit.Lightweight": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Serialization.Primitives": "4.3.0", - "System.Threading.Tasks.Parallel": "4.3.0" - } - }, - "NCrontab": { - "type": "Transitive", - "resolved": "3.3.3", - "contentHash": "2yzZXZLI0YpxrNgWnW/4xoo7ErLgWJIwTljRVEJ3hyjc7Kw9eGdjbFZGP1AhBuTUEZQ443PgZifG1yox6Qo1/A==" - }, - "NETStandard.Library": { - "type": "Transitive", - "resolved": "1.6.1", - "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.AppContext": "4.3.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Console": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.Compression.ZipFile": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Linq": "4.3.0", - "System.Linq.Expressions": "4.3.0", - "System.Net.Http": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Net.Sockets": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Timer": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0", - "System.Xml.XDocument": "4.3.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "Newtonsoft.Json.Bson": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", - "dependencies": { - "Newtonsoft.Json": "12.0.1" - } - }, - "NPoco": { - "type": "Transitive", - "resolved": "5.7.1", - "contentHash": "6qjyBqqc0TSK/xHjXA6tSZhABSDQqXGrTOIdUIVazPsmN0OyTaBTEtwV2wTV0NyfkzcRPhLyO6bIW89ZFNvlWg==", - "dependencies": { - "System.Linq.Async": "5.0.0", - "System.Reflection.Emit.Lightweight": "4.7.0" - } - }, - "NUglify": { - "type": "Transitive", - "resolved": "1.20.2", - "contentHash": "vz/SjCdpxr0Jp09VzMeezid7rwbXimik2QO1dzxzDcN3bXGJloDGDVh0zoD6DA23y6yrRzxv1ZKJ3kKzV3rqyA==" - }, - "OpenIddict.Abstractions": { - "type": "Transitive", - "resolved": "4.10.1", - "contentHash": "r2oCgsk8hG82TmT5g7yTzGLrmZGTwn6/zISMXqqpM9rjQUH0/FZ7NgwJy0f1j+HZSyBNj3jjYQIjx6a7qYBYqA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0", - "Microsoft.IdentityModel.Tokens": "7.0.0" - } - }, - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==" - }, - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==" - }, - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==" - }, - "runtime.native.System": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", - "dependencies": { - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" - } - }, - "runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==", - "dependencies": { - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" - } - }, - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==" - }, - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==" - }, - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==" - }, - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==" - }, - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==" - }, - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" - }, - "Serilog": { - "type": "Transitive", - "resolved": "3.1.1", - "contentHash": "P6G4/4Kt9bT635bhuwdXlJ2SCqqn2nhh4gqFqQueCOr9bK/e7W9ll/IoX1Ter948cV2Z/5+5v8pAfJYUISY03A==" - }, - "Serilog.AspNetCore": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "B/X+wAfS7yWLVOTD83B+Ip9yl4MkhioaXj90JSoWi1Ayi8XHepEnsBdrkojg08eodCnmOKmShFUN2GgEc6c0CQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Serilog": "3.1.1", - "Serilog.Extensions.Hosting": "8.0.0", - "Serilog.Extensions.Logging": "8.0.0", - "Serilog.Formatting.Compact": "2.0.0", - "Serilog.Settings.Configuration": "8.0.0", - "Serilog.Sinks.Console": "5.0.0", - "Serilog.Sinks.Debug": "2.0.0", - "Serilog.Sinks.File": "5.0.0" - } - }, - "Serilog.Enrichers.Process": { - "type": "Transitive", - "resolved": "2.0.2", - "contentHash": "T9EjKKLsL6qC/3eOLUAKEPBLEqPDmt5BLXaQdPMaxJzuex+MeXA8DuAiPboUaftp3kbnCN4ZgZpDvs+Fa7OHuw==", - "dependencies": { - "Serilog": "2.3.0" - } - }, - "Serilog.Enrichers.Thread": { - "type": "Transitive", - "resolved": "3.1.0", - "contentHash": "85lWsGRJpRxvKT6j/H67no55SUBsBIvp556TKuBTGhjtoPeq+L7j/sDWbgAtvT0p7u7/phJyX6j35PQ4Vtqw0g==", - "dependencies": { - "Serilog": "2.3.0" - } - }, - "Serilog.Expressions": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "dsC8GtalMDXMzywA60fHeBvqAjQ1EM75zSrdA7j7TxJfmrfss6BOxzgoT5thqjY+icLNbovUsC5KTYRlXzCpXg==", - "dependencies": { - "Serilog": "3.1.0" - } - }, - "Serilog.Extensions.Hosting": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "db0OcbWeSCvYQkHWu6n0v40N4kKaTAXNjlM3BKvcbwvNzYphQFcBR+36eQ/7hMMwOkJvAyLC2a9/jNdUL5NjtQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Serilog": "3.1.1", - "Serilog.Extensions.Logging": "8.0.0" - } - }, - "Serilog.Extensions.Logging": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "YEAMWu1UnWgf1c1KP85l1SgXGfiVo0Rz6x08pCiPOIBt2Qe18tcZLvdBUuV5o1QHvrs8FAry9wTIhgBRtjIlEg==", - "dependencies": { - "Microsoft.Extensions.Logging": "8.0.0", - "Serilog": "3.1.1" - } - }, - "Serilog.Formatting.Compact": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "ob6z3ikzFM3D1xalhFuBIK1IOWf+XrQq+H4KeH4VqBcPpNcmUgZlRQ2h3Q7wvthpdZBBoY86qZOI2LCXNaLlNA==", - "dependencies": { - "Serilog": "3.1.0" - } - }, - "Serilog.Formatting.Compact.Reader": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "A4tBQ36969szfQMwnxaikNKxQs7lcGLPPcv45ghr3RrJK9hko71t8TNSdMSAWU25ZK6JSmH/RU14GwSo4v5E4Q==", - "dependencies": { - "Newtonsoft.Json": "13.0.3", - "Serilog": "3.1.0" - } - }, - "Serilog.Settings.Configuration": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "nR0iL5HwKj5v6ULo3/zpP8NMcq9E2pxYA6XKTSWCbugVs4YqPyvaqaKOY+OMpPivKp7zMEpax2UKHnDodbRB0Q==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.0", - "Serilog": "3.1.1" - } - }, - "Serilog.Sinks.Async": { - "type": "Transitive", - "resolved": "1.5.0", - "contentHash": "csHYIqAwI4Gy9oAhXYRwxGrQEAtBg3Ep7WaCzsnA1cZuBZjVAU0n7hWaJhItjO7hbLHh/9gRVxALCUB4Dv+gZw==", - "dependencies": { - "Serilog": "2.9.0" - } - }, - "Serilog.Sinks.Console": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "IZ6bn79k+3SRXOBpwSOClUHikSkp2toGPCZ0teUkscv4dpDg9E2R2xVsNkLmwddE4OpNVO3N0xiYsAH556vN8Q==", - "dependencies": { - "Serilog": "3.1.0" - } - }, - "Serilog.Sinks.Debug": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", - "dependencies": { - "Serilog": "2.10.0" - } - }, - "Serilog.Sinks.File": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", - "dependencies": { - "Serilog": "2.10.0" - } - }, - "Serilog.Sinks.Map": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "JbPBAeD5hxUQw8TZg3FlOnqVsSu1269nvqFm5DQ7hc+AmsB+hItl+zMSDphMbPJXjL8KdpMRSWNkGi7zTKRmCA==", - "dependencies": { - "Serilog": "2.8.0" - } - }, - "Smidge": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "AnRsxwg4Av7jxa0MkQMbLqdIrWbVZRVQ0KfnO4Mh19Old7lay179QvBnaOPFxAEWnIl4jHiZW8izesJp6TknVw==", - "dependencies": { - "Smidge.Core": "4.3.0" - } - }, - "Smidge.Core": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "B6m6uGpJrOKaJ68eE9clAzZUcURszTNHfoYa4razb3KUJtRXB5fmZvts8+0ffT0/tO09Vu2O/KFfiSZMp6X8Jw==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "5.0.0", - "Microsoft.Extensions.Configuration": "5.0.0", - "Microsoft.Extensions.Configuration.Json": "5.0.0", - "Microsoft.Extensions.FileProviders.Composite": "5.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "5.0.0", - "Microsoft.Extensions.Logging.Abstractions": "5.0.0", - "Microsoft.Extensions.Options": "5.0.0" - } - }, - "Smidge.InMemory": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fKyR6ICS0YoQLX0D4dIIYTwQEM1IZb8ChYhqLGpVyJ7GiOAawsXt4ZcVnH0XT+ggan2+JzQlLiXGcCdXnb16Xg==", - "dependencies": { - "Dazinator.Extensions.FileProviders": "2.0.0", - "Smidge.Core": "4.3.0", - "System.Text.Encodings.Web": "5.0.1" - } - }, - "Smidge.Nuglify": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kx5Ulh+o5zLI0Al0POs0nYPldUArErmrAxxccrrxl77MWWrDM3KS5IRWuKDtC42/sZKSzapmJIOwJ8r/1foMCg==", - "dependencies": { - "Nuglify": "1.20.2", - "Smidge": "4.3.0" - } - }, - "System.AppContext": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Collections.Concurrent": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" - }, - "System.ComponentModel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.ComponentModel.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", - "dependencies": { - "System.ComponentModel": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Console": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Data.Common": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "lm6E3T5u7BOuEH0u18JpbJHxBfOJPuCyl4Kg1RH10ktYLp5uEEE1xKrHW56/We4SnZpGAuCc9N0MJpSDhTHZGQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.DiagnosticSource": { + "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" - }, - "System.Diagnostics.StackTrace": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiHg0vgtd35/DM9jvtaC1eKRpWZxr0gcQd643ABG7GnvSlf5pOkY2uyd42mMOJoOmKvnpNj0F4tuoS1pacTwYw==", - "dependencies": { - "System.IO.FileSystem": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Metadata": "1.4.1", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Dynamic.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Linq": "4.3.0", - "System.Linq.Expressions": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Formats.Asn1": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AJukBuLoe3QeAF+mfaRKQb2dgyrvt340iMBHYv+VdBzCUM06IxGlvl0o/uPOS7lHnXPN6u8fFRHSHudx5aTi8w==" - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Buffers": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.IO.Compression": "4.3.0" - } - }, - "System.IO.Compression.ZipFile": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", - "dependencies": { - "System.Buffers": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.FileSystem.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q==" - }, - "System.Linq": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Linq.Async": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "cPtIuuH8TIjVHSi2ewwReWGW1PfChPE0LxPIDlfwVcLuTM9GANFTXiMB7k3aC4sk3f0cQU25LNKzx+jZMxijqw==" - }, - "System.Linq.Expressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Linq": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Emit.Lightweight": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.4", - "contentHash": "aOa2d51SEbmM+H+Csw7yJOuNZoHkrP2XnAurye5HWYgGVVU54YZDvsLUYRv6h18X3sPnjNCANmN7ZhIPiqMcjA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.DiagnosticSource": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" - } - }, - "System.Net.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Net.Sockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.ObjectModel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", - "dependencies": { - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit.ILGeneration": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "a4OLB4IITxAXJeV74MDx49Oq2+PsF6Sml54XAFv+2RyWwtDBcabzoxiiJRhdhx+gaohLh4hEGCLQyBozXoQPqA==" - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", - "dependencies": { - "System.Collections.Immutable": "7.0.0" - } - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Runtime.InteropServices.RuntimeInformation": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Runtime.Numerics": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", - "dependencies": { - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", - "dependencies": { - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.Apple": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Linq": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", - "dependencies": { - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ULmp3xoOwNYjOYp4JZ2NK/6NdTgiN1GQXzVVN1njQ7LOZ0d0B9vyMnhyqbIi9Qw4JXj1JgCsitkTShboHRx7Eg==", - "dependencies": { - "System.Formats.Asn1": "8.0.0" - } - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Cng": "4.3.0", - "System.Security.Cryptography.Csp": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Xml": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "HQSFbakswZ1OXFz2Bt3AJlC6ENDqWeVpgqhf213xqQUMDifzydOHIKVb1RV4prayobvR3ETIScMaQdDF2hwGZA==", - "dependencies": { - "System.Security.Cryptography.Pkcs": "8.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==" - }, - "System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==", - "dependencies": { - "System.Text.Encodings.Web": "8.0.0" - } - }, - "System.Text.RegularExpressions": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==", - "dependencies": { - "System.Runtime": "4.3.1" - } - }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", - "dependencies": { - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.AccessControl": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "/fmzEf1UYrdCzfOIHVJ2cx3v9DHLLLMkUrodpzJGW17N+K+SSmBD8OA/BGmtfN1Ae0Ex3rBjQVufnIi5zKefuQ==", - "dependencies": { - "System.Security.AccessControl": "4.7.0", - "System.Security.Principal.Windows": "4.7.0" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Tasks.Parallel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbjBNZHf/vQCfcdhzx7knsiygoCKgxL8mZOeocXZn5gWhCdzHIq6bYNKWX0LAJCWYP7bds4yBK8p06YkP0oa0g==", - "dependencies": { - "System.Collections.Concurrent": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Timer": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Xml.ReaderWriter": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Tasks.Extensions": "4.3.0" - } - }, - "System.Xml.XDocument": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0" - } - }, - "Umbraco.Cms.Core": { - "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "jNV8u0ZekZsmIuStkwLrhMxztT7eHhCHwyOpMMbNOdzCbqBQUuOb3MDcz4RUXJwS/BlXqfoTu4l7Pxdz7oDj/g==", - "dependencies": { - "Microsoft.Extensions.Caching.Memory": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Embedded": "8.0.1", - "Microsoft.Extensions.FileProviders.Physical": "8.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", - "Microsoft.Extensions.Identity.Core": "8.0.1", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Options": "8.0.1", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", - "Microsoft.Extensions.Options.DataAnnotations": "8.0.0" - } - }, - "Umbraco.Cms.Examine.Lucene": { - "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "VkctBavadScmuSwgzj8hxKJKhA7giJnpmQyiV9jxVvF9SW375nkjTtQk3itEZcotjY9AJJJkbGfZjiKbhlgNvg==", - "dependencies": { - "Examine": "3.2.0", - "System.Security.Cryptography.Xml": "8.0.0", - "Umbraco.Cms.Infrastructure": "[14.0.0--preview006, 15.0.0)" - } - }, - "Umbraco.Cms.Infrastructure": { - "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "zB+wjCSoMCN8jKUva1yoc19oeDpAhMNhjMLo98L0Agb+lJsMJNHyxHf95i97v4E654UfibSPsda0xw26HeJcSQ==", - "dependencies": { - "Examine.Core": "3.2.0", - "HtmlAgilityPack": "1.11.57", - "MailKit": "4.3.0", - "Markdown": "2.2.1", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Json": "8.0.0", - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Http": "8.0.0", - "Microsoft.Extensions.Identity.Stores": "8.0.1", - "MiniProfiler.Shared": "4.3.8", - "NPoco": "5.7.1", - "Newtonsoft.Json": "13.0.3", - "OpenIddict.Abstractions": "4.10.1", - "Serilog": "3.1.1", - "Serilog.Enrichers.Process": "2.0.2", - "Serilog.Enrichers.Thread": "3.1.0", - "Serilog.Expressions": "4.0.0", - "Serilog.Extensions.Hosting": "8.0.0", - "Serilog.Formatting.Compact": "2.0.0", - "Serilog.Formatting.Compact.Reader": "3.0.0", - "Serilog.Settings.Configuration": "8.0.0", - "Serilog.Sinks.Async": "1.5.0", - "Serilog.Sinks.File": "5.0.0", - "Serilog.Sinks.Map": "1.0.2", - "Umbraco.Cms.Core": "[14.0.0--preview006, 15.0.0)", - "ncrontab": "3.3.3" - } - }, - "Umbraco.Cms.PublishedCache.NuCache": { - "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "nkK6gGPysMsokfgLeir497YoVgb51qEKPxdtoaT75ENvoXoppo3i+3+WEUQjg73FcuH4lsbB+S8ncqONbPZm2A==", - "dependencies": { - "K4os.Compression.LZ4": "1.3.6", - "MessagePack": "2.5.140", - "Umbraco.CSharpTest.Net.Collections": "15.0.0", - "Umbraco.Cms.Infrastructure": "[14.0.0--preview006, 15.0.0)" - } - }, - "Umbraco.Cms.Web.Common": { - "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "hy15XuNvXKL9VkioLG8VWIwa7g2GT27oZ6uKZiXDPObSxBaqnjlKOQxTqWqIa6zEQ4wt3avWjlMK38sO6Dq0yg==", - "dependencies": { - "Asp.Versioning.Mvc": "8.0.0", - "Asp.Versioning.Mvc.ApiExplorer": "8.0.0", - "Dazinator.Extensions.FileProviders": "2.0.0", - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "8.0.1", - "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "8.0.1", - "MiniProfiler.AspNetCore.Mvc": "4.3.8", - "Serilog.AspNetCore": "8.0.1", - "Smidge.InMemory": "4.3.0", - "Smidge.Nuglify": "4.3.0", - "System.Net.Http": "4.3.4", - "System.Text.RegularExpressions": "4.3.1", - "Umbraco.Cms.Examine.Lucene": "[14.0.0--preview006, 15.0.0)", - "Umbraco.Cms.PublishedCache.NuCache": "[14.0.0--preview006, 15.0.0)" - } - }, - "Umbraco.CSharpTest.Net.Collections": { - "type": "Transitive", - "resolved": "15.0.0", - "contentHash": "YSDIkxq44VMy2N3jBTwJBJ/ZjGyuyb0GRyfQAUIma07dCHIbjXgKXjZaAxVa6ik3XTqgcyATvwYJL0EBtAClwA==" } } } diff --git a/uSync.AutoTemplates/uSync.AutoTemplates.csproj b/uSync.AutoTemplates/uSync.AutoTemplates.csproj index 8514107d..569f4e12 100644 --- a/uSync.AutoTemplates/uSync.AutoTemplates.csproj +++ b/uSync.AutoTemplates/uSync.AutoTemplates.csproj @@ -6,7 +6,7 @@ - + diff --git a/uSync.BackOffice/Authorization/SyncAuthorizationPolicies.cs b/uSync.BackOffice/Authorization/SyncAuthorizationPolicies.cs index b7a280b6..8ebfc8e5 100644 --- a/uSync.BackOffice/Authorization/SyncAuthorizationPolicies.cs +++ b/uSync.BackOffice/Authorization/SyncAuthorizationPolicies.cs @@ -1,13 +1,12 @@ -namespace uSync.BackOffice.Authorization +namespace uSync.BackOffice.Authorization; + +/// +/// Security policy constants used in Umbraco by uSync +/// +public static class SyncAuthorizationPolicies { /// - /// Security policy constants used in Umbraco by uSync + /// name of the uSyncTreeAccess policy. /// - public static class SyncAuthorizationPolicies - { - /// - /// name of the uSyncTreeAccess policy. - /// - public const string TreeAccessuSync = nameof(TreeAccessuSync); - } + public const string TreeAccessuSync = nameof(TreeAccessuSync); } diff --git a/uSync.BackOffice/BackOfficeConstants.cs b/uSync.BackOffice/BackOfficeConstants.cs index 55ca14fc..d8e1673e 100644 --- a/uSync.BackOffice/BackOfficeConstants.cs +++ b/uSync.BackOffice/BackOfficeConstants.cs @@ -1,245 +1,243 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Constant values for uSync +/// +public static partial class uSyncConstants { /// - /// Constant values for uSync + /// Information about the package name/files + /// + public static class Package + { + /// + /// Name of the Package + /// + public const string Name = "uSync"; + + /// + /// Virtual path to the plugin files + /// + public const string PluginPath = "/App_Plugins/uSync"; + } + + /// + /// Suffix to place on any release strings + /// + public const string ReleaseSuffix = ""; + + /// + /// ordering of the handler items (what gets done when) + /// + public static class Priorites + { + /// + /// Lower bound of uSync's reserved priority range + /// + public const int USYNC_RESERVED_LOWER = 1000; + + /// + /// Upper bound of uSync's reserved priority range + /// + public const int USYNC_RESERVED_UPPER = 2000; + + /// + /// DataTypes priority + /// + public const int DataTypes = USYNC_RESERVED_LOWER + 10; + + /// + /// Templates priority + /// + public const int Templates = USYNC_RESERVED_LOWER + 20; + + /// + /// ContentTypes priority + /// + public const int ContentTypes = USYNC_RESERVED_LOWER + 30; + + /// + /// MediaTypes priority + /// + public const int MediaTypes = USYNC_RESERVED_LOWER + 40; + + /// + /// MemberTypes priority + /// + public const int MemberTypes = USYNC_RESERVED_LOWER + 45; + + /// + /// Languages priority + /// + public const int Languages = USYNC_RESERVED_LOWER + 5; + + /// + /// DictionaryItems priority + /// + public const int DictionaryItems = USYNC_RESERVED_LOWER + 6; + + /// + /// Macros priority + /// + public const int Macros = USYNC_RESERVED_LOWER + 70; + + /// + /// Media priority + /// + public const int Media = USYNC_RESERVED_LOWER + 200; + + /// + /// Content priority + /// + public const int Content = USYNC_RESERVED_LOWER + 210; + + /// + /// ContentTemplate priority + /// + public const int ContentTemplate = USYNC_RESERVED_LOWER + 215; + + /// + /// DomainSettings priority + /// + public const int DomainSettings = USYNC_RESERVED_LOWER + 219; + + /// + /// DataTypeMappings priority + /// + public const int DataTypeMappings = USYNC_RESERVED_LOWER + 220; + + /// + /// RelationTypes priority + /// + public const int RelationTypes = USYNC_RESERVED_LOWER + 230; + } + + /// + /// Default group names /// - public static partial class uSyncConstants + public static class Groups { /// - /// Information about the package name/files - /// - public static class Package - { - /// - /// Name of the Package - /// - public const string Name = "uSync"; + /// Name for the settings group of handlers + /// + public const string Settings = "Settings"; + + /// + /// Name for the Content group of handlers + /// + public const string Content = "Content"; + + /// + /// Name for the Members group of handlers + /// + public const string Members = "Members"; + + /// + /// Name for the > group of handlers + /// + public const string Users = "Users"; + + /// + /// Name for the Forms group of handlers + /// + public const string Forms = "Forms"; + + /// + /// Name for the Files group of handlers + /// + public const string Files = "Files"; + + /// + /// Name for the default group (used for loading default global config) + /// + public const string Default = "__default__"; + + /// + /// Icons for the well known handler groups. + /// + public static Dictionary Icons = new Dictionary { + { Settings, "icon-settings-alt color-blue" }, + { Content, "icon-documents color-purple" }, + { Members, "icon-users" }, + { Users, "icon-users color-green"}, + { Default, "icon-settings" }, + { Forms, "icon-umb-contour" }, + { Files, "icon-script-alt" } + }; + } + + /// + /// names of the well know handlers + /// + public static class Handlers + { + /// + /// ContentHandler Name + /// + public const string ContentHandler = "ContentHandler"; + + /// + /// ContentTemplateHandler Name + /// + public const string ContentTemplateHandler = "ContentTemplateHandler"; + + /// + /// ContentTypeHandler Name + /// + public const string ContentTypeHandler = "ContentTypeHandler"; + + /// + /// DataTypeHandler Name + /// + public const string DataTypeHandler = "DataTypeHandler"; + + /// + /// DictionaryHandler Name + /// + public const string DictionaryHandler = "DictionaryHandler"; + + /// + /// DomainHandler Name + /// + public const string DomainHandler = "DomainHandler"; + + /// + /// LanguageHandler Name + /// + public const string LanguageHandler = "LanguageHandler"; + + /// + /// MacroHandler Name + /// + public const string MacroHandler = "MacroHandler"; + + /// + /// MediaHandler Name + /// + public const string MediaHandler = "MediaHandler"; + + /// + /// MediaTypeHandler Name + /// + public const string MediaTypeHandler = "MediaTypeHandler"; + + /// + /// MemberTypeHandler Name + /// + public const string MemberTypeHandler = "MemberTypeHandler"; + + /// + /// RelationTypeHandler Name + /// + public const string RelationTypeHandler = "RelationTypeHandler"; + + /// + /// TemplateHandler Name + /// + public const string TemplateHandler = "TemplateHandler"; + - /// - /// Virtual path to the plugin files - /// - public const string PluginPath = "/App_Plugins/uSync"; - } - - /// - /// Suffix to place on any release strings - /// - public const string ReleaseSuffix = ""; - - /// - /// ordering of the handler items (what gets done when) - /// - public static class Priorites - { - /// - /// Lower bound of uSync's reserved priority range - /// - public const int USYNC_RESERVED_LOWER = 1000; - - /// - /// Upper bound of uSync's reserved priority range - /// - public const int USYNC_RESERVED_UPPER = 2000; - - /// - /// DataTypes priority - /// - public const int DataTypes = USYNC_RESERVED_LOWER + 10; - - /// - /// Templates priority - /// - public const int Templates = USYNC_RESERVED_LOWER + 20; - - /// - /// ContentTypes priority - /// - public const int ContentTypes = USYNC_RESERVED_LOWER + 30; - - /// - /// MediaTypes priority - /// - public const int MediaTypes = USYNC_RESERVED_LOWER + 40; - - /// - /// MemberTypes priority - /// - public const int MemberTypes = USYNC_RESERVED_LOWER + 45; - - /// - /// Languages priority - /// - public const int Languages = USYNC_RESERVED_LOWER + 5; - - /// - /// DictionaryItems priority - /// - public const int DictionaryItems = USYNC_RESERVED_LOWER + 6; - - /// - /// Macros priority - /// - public const int Macros = USYNC_RESERVED_LOWER + 70; - - /// - /// Media priority - /// - public const int Media = USYNC_RESERVED_LOWER + 200; - - /// - /// Content priority - /// - public const int Content = USYNC_RESERVED_LOWER + 210; - - /// - /// ContentTemplate priority - /// - public const int ContentTemplate = USYNC_RESERVED_LOWER + 215; - - /// - /// DomainSettings priority - /// - public const int DomainSettings = USYNC_RESERVED_LOWER + 219; - - /// - /// DataTypeMappings priority - /// - public const int DataTypeMappings = USYNC_RESERVED_LOWER + 220; - - /// - /// RelationTypes priority - /// - public const int RelationTypes = USYNC_RESERVED_LOWER + 230; - } - - /// - /// Default group names - /// - public static class Groups - { - /// - /// Name for the settings group of handlers - /// - public const string Settings = "Settings"; - - /// - /// Name for the Content group of handlers - /// - public const string Content = "Content"; - - /// - /// Name for the Members group of handlers - /// - public const string Members = "Members"; - - /// - /// Name for the > group of handlers - /// - public const string Users = "Users"; - - /// - /// Name for the Forms group of handlers - /// - public const string Forms = "Forms"; - - /// - /// Name for the Files group of handlers - /// - public const string Files = "Files"; - - /// - /// Name for the default group (used for loading default global config) - /// - public const string Default = "__default__"; - - /// - /// Icons for the well known handler groups. - /// - public static Dictionary Icons = new Dictionary { - { Settings, "icon-settings-alt color-blue" }, - { Content, "icon-documents color-purple" }, - { Members, "icon-users" }, - { Users, "icon-users color-green"}, - { Default, "icon-settings" }, - { Forms, "icon-umb-contour" }, - { Files, "icon-script-alt" } - }; - } - - /// - /// names of the well know handlers - /// - public static class Handlers - { - /// - /// ContentHandler Name - /// - public const string ContentHandler = "ContentHandler"; - - /// - /// ContentTemplateHandler Name - /// - public const string ContentTemplateHandler = "ContentTemplateHandler"; - - /// - /// ContentTypeHandler Name - /// - public const string ContentTypeHandler = "ContentTypeHandler"; - - /// - /// DataTypeHandler Name - /// - public const string DataTypeHandler = "DataTypeHandler"; - - /// - /// DictionaryHandler Name - /// - public const string DictionaryHandler = "DictionaryHandler"; - - /// - /// DomainHandler Name - /// - public const string DomainHandler = "DomainHandler"; - - /// - /// LanguageHandler Name - /// - public const string LanguageHandler = "LanguageHandler"; - - /// - /// MacroHandler Name - /// - public const string MacroHandler = "MacroHandler"; - - /// - /// MediaHandler Name - /// - public const string MediaHandler = "MediaHandler"; - - /// - /// MediaTypeHandler Name - /// - public const string MediaTypeHandler = "MediaTypeHandler"; - - /// - /// MemberTypeHandler Name - /// - public const string MemberTypeHandler = "MemberTypeHandler"; - - /// - /// RelationTypeHandler Name - /// - public const string RelationTypeHandler = "RelationTypeHandler"; - - /// - /// TemplateHandler Name - /// - public const string TemplateHandler = "TemplateHandler"; - - - } } } diff --git a/uSync.BackOffice/Boot/FirstBootMigration.cs b/uSync.BackOffice/Boot/FirstBootMigration.cs index cbbd6f27..3b6471ef 100644 --- a/uSync.BackOffice/Boot/FirstBootMigration.cs +++ b/uSync.BackOffice/Boot/FirstBootMigration.cs @@ -1,7 +1,6 @@  using System; using System.Diagnostics; -using System.Linq; using Microsoft.Extensions.Logging; @@ -11,88 +10,87 @@ using uSync.BackOffice.Configuration; using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice.Boot +namespace uSync.BackOffice.Boot; + +/// +/// Migration plan to add FirstBoot feature +/// +public class FirstBootMigrationPlan : MigrationPlan { - /// - /// Migration plan to add FirstBoot feature - /// - public class FirstBootMigrationPlan : MigrationPlan + /// + public FirstBootMigrationPlan() + : base("uSync_FirstBoot") { - /// - public FirstBootMigrationPlan() - : base("uSync_FirstBoot") - { - From(string.Empty) - .To("FirstBoot-Migration") - .To("Logviewer-Migration"); - } + From(string.Empty) + .To("FirstBoot-Migration"); + // .To("Logviewer-Migration"); } +} - /// - /// First boot Feature migration - /// - public class FirstBootMigration : MigrationBase +/// +/// First boot Feature migration +/// +public class FirstBootMigration : MigrationBase +{ + private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly uSyncConfigService _uSyncConfig; + private readonly uSyncService _uSyncService; + private readonly ILogger _logger; + + /// + public FirstBootMigration( + IMigrationContext context, + IUmbracoContextFactory umbracoContextFactory, + uSyncConfigService uSyncConfig, + uSyncService uSyncService, + ILogger logger) : base(context) { - private readonly IUmbracoContextFactory _umbracoContextFactory; - private readonly uSyncConfigService _uSyncConfig; - private readonly uSyncService _uSyncService; - private readonly ILogger _logger; - - /// - public FirstBootMigration( - IMigrationContext context, - IUmbracoContextFactory umbracoContextFactory, - uSyncConfigService uSyncConfig, - uSyncService uSyncService, - ILogger logger) : base(context) - { - _umbracoContextFactory = umbracoContextFactory; - _uSyncConfig = uSyncConfig; - _uSyncService = uSyncService; - _logger = logger; - } + _umbracoContextFactory = umbracoContextFactory; + _uSyncConfig = uSyncConfig; + _uSyncService = uSyncService; + _logger = logger; + } - /// - protected override void Migrate() + /// + protected override void Migrate() + { + // first boot migration. + try { - // first boot migration. - try - { - if (!_uSyncConfig.Settings.ImportOnFirstBoot) - return; + if (!_uSyncConfig.Settings.ImportOnFirstBoot) + return; - var sw = Stopwatch.StartNew(); - var changes = 0; + var sw = Stopwatch.StartNew(); + var changes = 0; - _logger.LogInformation("Import on First-boot Set - will import {group} handler groups", - _uSyncConfig.Settings.FirstBootGroup); + _logger.LogInformation("Import on First-boot Set - will import {group} handler groups", + _uSyncConfig.Settings.FirstBootGroup); - // if config service is set to import on first boot then this - // will let uSync do a first boot import + // if config service is set to import on first boot then this + // will let uSync do a first boot import - // not sure about context on migrations so will need to test - // or maybe we fire something into a notification (or use a static) + // not sure about context on migrations so will need to test + // or maybe we fire something into a notification (or use a static) - using (var reference = _umbracoContextFactory.EnsureUmbracoContext()) - { - var results = _uSyncService.Import(_uSyncConfig.GetFolders(), false, new SyncHandlerOptions - { - Group = _uSyncConfig.Settings.FirstBootGroup - }, (uSyncCallbacks)null); - - changes = results.CountChanges(); - }; - - sw.Stop(); - _logger.LogInformation("uSync First boot complete {changes} changes in ({time}ms)", - changes, sw.ElapsedMilliseconds); - } - catch(Exception ex) + using (var reference = _umbracoContextFactory.EnsureUmbracoContext()) { - _logger.LogError(ex, "uSync First boot failed {message}", ex.Message); - throw; - } + var results = _uSyncService.Import(_uSyncConfig.GetFolders(), false, new SyncHandlerOptions + { + Group = _uSyncConfig.Settings.FirstBootGroup + }, null); + + changes = results.CountChanges(); + }; + + sw.Stop(); + _logger.LogInformation("uSync First boot complete {changes} changes in ({time}ms)", + changes, sw.ElapsedMilliseconds); + } + catch (Exception ex) + { + _logger.LogError(ex, "uSync First boot failed {message}", ex.Message); + throw; } } } diff --git a/uSync.BackOffice/Boot/LogViewerMigration.cs b/uSync.BackOffice/Boot/LogViewerMigration.cs index 8cce2300..a0a55413 100644 --- a/uSync.BackOffice/Boot/LogViewerMigration.cs +++ b/uSync.BackOffice/Boot/LogViewerMigration.cs @@ -7,25 +7,25 @@ namespace uSync.BackOffice.Boot; internal class LogViewerMigration : MigrationBase { - private static string uSyncLogQuery = "StartsWith(SourceContext, 'uSync')"; + private static string uSyncLogQuery = "StartsWith(SourceContext, 'uSync')"; - private readonly ILogViewerConfig _logViewerConfig; + private readonly ILogViewerConfig _logViewerConfig; - public LogViewerMigration( - ILogViewerConfig config, - IMigrationContext context) : base(context) - { - _logViewerConfig = config; - } + public LogViewerMigration( + ILogViewerConfig config, + IMigrationContext context) : base(context) + { + _logViewerConfig = config; + } - protected override void Migrate() - { - var existing = _logViewerConfig.GetSavedSearches() - .FirstOrDefault(x => x.Query.StartsWith(uSyncLogQuery, StringComparison.OrdinalIgnoreCase)); + protected override void Migrate() + { + var existing = _logViewerConfig.GetSavedSearches() + .FirstOrDefault(x => x.Query.StartsWith(uSyncLogQuery, StringComparison.OrdinalIgnoreCase)); - if (existing == null) - { - _logViewerConfig.AddSavedSearch("Find all uSync Log Entries", uSyncLogQuery); - } - } + if (existing == null) + { + _logViewerConfig.AddSavedSearch("Find all uSync Log Entries", uSyncLogQuery); + } + } } diff --git a/uSync.BackOffice/Boot/uSyncBootExtension.cs b/uSync.BackOffice/Boot/uSyncBootExtension.cs index 87d1edfd..56b3ccf7 100644 --- a/uSync.BackOffice/Boot/uSyncBootExtension.cs +++ b/uSync.BackOffice/Boot/uSyncBootExtension.cs @@ -1,9 +1,4 @@ - -using System; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; @@ -15,76 +10,75 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Extensions; -namespace uSync.BackOffice.Boot +namespace uSync.BackOffice.Boot; + +/// +/// replaces the default 'NoNodes' page with a uSync one, which will allow us to +/// tell the dev that the new site they have has got uSync files they can import +/// +internal static class uSyncBootExtension { - /// - /// replaces the default 'NoNodes' page with a uSync one, which will allow us to - /// tell the dev that the new site they have has got uSync files they can import - /// - internal static class uSyncBootExtension - { - internal static string defaultNoNodesPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; - internal static string noNodesPath = "~/App_Plugins/uSync/boot/NoNodes.cshtml"; + internal static string _defaultNoNodesPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; + internal static string _noNodesPath = "~/App_Plugins/uSync/boot/NoNodes.cshtml"; - public static IUmbracoBuilder AdduSyncFirstBoot(this IUmbracoBuilder builder) + public static IUmbracoBuilder AdduSyncFirstBoot(this IUmbracoBuilder builder) + { + builder.Services.PostConfigure(settings => { - builder.Services.PostConfigure(settings => + if (settings.NoNodesViewPath.InvariantEquals(_defaultNoNodesPath)) { - if (settings.NoNodesViewPath.InvariantEquals(defaultNoNodesPath)) - { - // if the default hasn't changed, put in the uSync version - settings.NoNodesViewPath = noNodesPath; - } - }); + // if the default hasn't changed, put in the uSync version + settings.NoNodesViewPath = _noNodesPath; + } + }); - // add notification handler to do the actual first boot run. - builder.AddNotificationHandler(); - - return builder; - } + // add notification handler to do the actual first boot run. + builder.AddNotificationHandler(); + + return builder; } +} - /// - /// Handler to mange app starting for first boot migrations - /// - internal class FirstBootAppStartingHandler - : INotificationHandler - { +/// +/// Handler to mange app starting for first boot migrations +/// +internal class FirstBootAppStartingHandler + : INotificationHandler +{ - private readonly ICoreScopeProvider _scopeProvider; - private readonly IKeyValueService _keyValueService; - private readonly IMigrationPlanExecutor _migrationPlanExecutor; - private readonly IRuntimeState _runtimeState; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IKeyValueService _keyValueService; + private readonly IMigrationPlanExecutor _migrationPlanExecutor; + private readonly IRuntimeState _runtimeState; - /// - public FirstBootAppStartingHandler( - ICoreScopeProvider scopeProvider, - IKeyValueService keyValueService, - IMigrationPlanExecutor migrationPlanExecutor, - IRuntimeState runtimeState) - { - _scopeProvider = scopeProvider; - _keyValueService = keyValueService; - _migrationPlanExecutor = migrationPlanExecutor; - _runtimeState = runtimeState; - } + /// + public FirstBootAppStartingHandler( + ICoreScopeProvider scopeProvider, + IKeyValueService keyValueService, + IMigrationPlanExecutor migrationPlanExecutor, + IRuntimeState runtimeState) + { + _scopeProvider = scopeProvider; + _keyValueService = keyValueService; + _migrationPlanExecutor = migrationPlanExecutor; + _runtimeState = runtimeState; + } - /// - public void Handle(UmbracoApplicationStartedNotification notification) + /// + public void Handle(UmbracoApplicationStartedNotification notification) + { + if (_runtimeState.Level == Umbraco.Cms.Core.RuntimeLevel.Run) { - if (_runtimeState.Level == Umbraco.Cms.Core.RuntimeLevel.Run) - { - var firstBootMigration = new FirstBootMigrationPlan(); - var upgrader = new Upgrader(firstBootMigration); + var firstBootMigration = new FirstBootMigrationPlan(); + var upgrader = new Upgrader(firstBootMigration); - // this bit is done inside the upgrader.Execute method too, - // but we don't want the extra three log messages during startup, - // so we also check before we start - var currentState = _keyValueService.GetValue(upgrader.StateValueKey); - if (currentState == null || currentState != firstBootMigration.FinalState) - upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); - } + // this bit is done inside the upgrader.Execute method too, + // but we don't want the extra three log messages during startup, + // so we also check before we start + var currentState = _keyValueService.GetValue(upgrader.StateValueKey); + if (currentState == null || currentState != firstBootMigration.FinalState) + upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); } } } diff --git a/uSync.BackOffice/Cache/CacheLifecycleManager.cs b/uSync.BackOffice/Cache/CacheLifecycleManager.cs index a680ed21..cd441a5b 100644 --- a/uSync.BackOffice/Cache/CacheLifecycleManager.cs +++ b/uSync.BackOffice/Cache/CacheLifecycleManager.cs @@ -1,130 +1,128 @@  -using Microsoft.Extensions.Logging; - using System; +using Microsoft.Extensions.Logging; + using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using uSync.BackOffice.Services; using uSync.Core.Cache; -namespace uSync.BackOffice.Cache +namespace uSync.BackOffice.Cache; + +/// +/// Cleans up the entity cache at start and end of the sync. +/// +public class CacheLifecycleManager : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler + + { + private readonly SyncEntityCache _entityCache; + private readonly ILogger _logger; + private readonly uSyncEventService _eventService; + /// - /// Cleans up the entity cache at start and end of the sync. + /// Constructor /// - public class CacheLifecycleManager : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + public CacheLifecycleManager( + ILogger logger, + SyncEntityCache entityCache, + uSyncEventService eventService) + { + _logger = logger; + _entityCache = entityCache; + _eventService = eventService; + } - { - private readonly SyncEntityCache _entityCache; - private readonly ILogger _logger; - private readonly uSyncEventService _eventService; - - /// - /// Constructor - /// - public CacheLifecycleManager( - ILogger logger, - SyncEntityCache entityCache, - uSyncEventService eventService) - { - _logger = logger; - _entityCache = entityCache; - _eventService = eventService; - } + /// + /// Handle the uSync import starting notification + /// + public void Handle(uSyncImportStartingNotification notification) => OnBulkActionComplete(); + /// + /// Handle the uSync uSync Export Completed Notification + /// + public void Handle(uSyncExportCompletedNotification notification) => OnBulkActionComplete(); - /// - /// Handle the uSync import starting notification - /// - public void Handle(uSyncImportStartingNotification notification) => OnBulkActionComplete(); - - /// - /// Handle the uSync uSync Export Completed Notification - /// - public void Handle(uSyncExportCompletedNotification notification) => OnBulkActionComplete(); - - /// - /// Handle the uSync uSync Import Completed Notification - /// - public void Handle(uSyncImportCompletedNotification notification) => OnBulkActionComplete(); - - /// - /// Handle the uSync uSync Export Starting Notification - /// - public void Handle(uSyncExportStartingNotification notification) => OnBulkActionComplete(); - - /// - /// Handle the uSync uSync Report Completed Notification - /// - public void Handle(uSyncReportStartingNotification notification) => OnBulkActionComplete(); - - /// - /// Handle the uSync uSync Report Completed Notification - /// - public void Handle(uSyncReportCompletedNotification notification) => OnBulkActionComplete(); - - /// - /// Clear the cache on the Umbraco Content Saving notification - /// - public void Handle(ContentSavingNotification notification) => ClearOnEvents(); - - /// - /// Clear the cache on the Umbraco Content Deleting notification - /// - public void Handle(ContentDeletingNotification notification) => ClearOnEvents(); - - /// - /// Clear the cache on the Umbraco Content Moving notification - /// - public void Handle(ContentMovingNotification notification) => ClearOnEvents(); - - /// - /// Clear the cache on the Umbraco Media Saving notification - /// - public void Handle(MediaSavingNotification notification) => ClearOnEvents(); - - /// - /// Clear the cache on the Umbraco Media Saved notification - /// - public void Handle(MediaSavedNotification notification) => ClearOnEvents(); - - /// - /// Clear the cache on the Umbraco Media deleted notification - /// - public void Handle(MediaDeletedNotification notification) => ClearOnEvents(); - - private void OnBulkActionComplete() + /// + /// Handle the uSync uSync Import Completed Notification + /// + public void Handle(uSyncImportCompletedNotification notification) => OnBulkActionComplete(); + + /// + /// Handle the uSync uSync Export Starting Notification + /// + public void Handle(uSyncExportStartingNotification notification) => OnBulkActionComplete(); + + /// + /// Handle the uSync uSync Report Completed Notification + /// + public void Handle(uSyncReportStartingNotification notification) => OnBulkActionComplete(); + + /// + /// Handle the uSync uSync Report Completed Notification + /// + public void Handle(uSyncReportCompletedNotification notification) => OnBulkActionComplete(); + + /// + /// Clear the cache on the Umbraco Content Saving notification + /// + public void Handle(ContentSavingNotification notification) => ClearOnEvents(); + + /// + /// Clear the cache on the Umbraco Content Deleting notification + /// + public void Handle(ContentDeletingNotification notification) => ClearOnEvents(); + + /// + /// Clear the cache on the Umbraco Content Moving notification + /// + public void Handle(ContentMovingNotification notification) => ClearOnEvents(); + + /// + /// Clear the cache on the Umbraco Media Saving notification + /// + public void Handle(MediaSavingNotification notification) => ClearOnEvents(); + + /// + /// Clear the cache on the Umbraco Media Saved notification + /// + public void Handle(MediaSavedNotification notification) => ClearOnEvents(); + + /// + /// Clear the cache on the Umbraco Media deleted notification + /// + public void Handle(MediaDeletedNotification notification) => ClearOnEvents(); + + private void OnBulkActionComplete() + { + _entityCache.Clear(); + } + + private void ClearOnEvents() + { + try { + if (_eventService.IsPaused) return; _entityCache.Clear(); } - - private void ClearOnEvents() + catch (Exception ex) { - try - { - if (_eventService.IsPaused) return; - _entityCache.Clear(); - } - catch(Exception ex) - { - _logger.LogWarning(ex, "Failed to clean the entity name cache"); - } + _logger.LogWarning(ex, "Failed to clean the entity name cache"); } } } diff --git a/uSync.BackOffice/Configuration/uSyncConfigExtensions.cs b/uSync.BackOffice/Configuration/uSyncConfigExtensions.cs index 048eeb06..1da0e2ea 100644 --- a/uSync.BackOffice/Configuration/uSyncConfigExtensions.cs +++ b/uSync.BackOffice/Configuration/uSyncConfigExtensions.cs @@ -4,48 +4,47 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -namespace uSync +namespace uSync; + +/// +/// Extensions to help reading/writing uSync settings +/// +public static class uSyncConfigExtensions { /// - /// Extensions to help reading/writing uSync settings + /// Add the suite of usync.json files to the config tree. /// - public static class uSyncConfigExtensions + /// + /// lets you have usync.json, usync.environment.json and usync.machinename.json files + /// that are loaded along side the other config sources, so you can move uSync config + /// out of the appsettings.json files if you want. + /// + public static void AdduSyncConfigs(this IConfigurationBuilder configurationBuilder, HostBuilderContext builderContext, string filename) { - /// - /// Add the suite of usync.json files to the config tree. - /// - /// - /// lets you have usync.json, usync.environment.json and usync.machinename.json files - /// that are loaded along side the other config sources, so you can move uSync config - /// out of the appsettings.json files if you want. - /// - public static void AdduSyncConfigs(this IConfigurationBuilder configurationBuilder, HostBuilderContext builderContext, string filename) - { - var env = builderContext.HostingEnvironment; - var fileRootName = - Path.Combine(Path.GetDirectoryName(filename), Path.GetFileNameWithoutExtension(filename)); + var env = builderContext.HostingEnvironment; + var fileRootName = + Path.Combine(Path.GetDirectoryName(filename) ?? string.Empty, Path.GetFileNameWithoutExtension(filename)); - configurationBuilder.AddJsonFile($"{fileRootName}.json", optional: true, reloadOnChange: true); - configurationBuilder.AddJsonFile($"{fileRootName}.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - configurationBuilder.AddJsonFile($"{fileRootName}.{Environment.MachineName}.json", optional: true, reloadOnChange: true); - } + configurationBuilder.AddJsonFile($"{fileRootName}.json", optional: true, reloadOnChange: true); + configurationBuilder.AddJsonFile($"{fileRootName}.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + configurationBuilder.AddJsonFile($"{fileRootName}.{Environment.MachineName}.json", optional: true, reloadOnChange: true); + } - /// - /// add usync.json files sources to the applications configuration - /// - /// - /// allows you to move the uSync config out of the main appsettings.config file, by default will add - /// usync.json, usync.[environment].json and usync.[machinename].json to the list of configuration - /// locations - /// - /// Host builder object from Configure method - /// name of json file to add (default usync.json) - public static IHostBuilder ConfigureuSyncConfig(this IHostBuilder hostBuilder, string filename = "usync.json") + /// + /// add usync.json files sources to the applications configuration + /// + /// + /// allows you to move the uSync config out of the main appsettings.config file, by default will add + /// usync.json, usync.[environment].json and usync.[machinename].json to the list of configuration + /// locations + /// + /// Host builder object from Configure method + /// name of json file to add (default usync.json) + public static IHostBuilder ConfigureuSyncConfig(this IHostBuilder hostBuilder, string filename = "usync.json") + { + return hostBuilder.ConfigureAppConfiguration((hostingContext, config) => { - return hostBuilder.ConfigureAppConfiguration((hostingContext, config) => - { - config.AdduSyncConfigs(hostingContext, filename); - }); - } + config.AdduSyncConfigs(hostingContext, filename); + }); } } diff --git a/uSync.BackOffice/Configuration/uSyncConfigService.cs b/uSync.BackOffice/Configuration/uSyncConfigService.cs index 95948579..b135de57 100644 --- a/uSync.BackOffice/Configuration/uSyncConfigService.cs +++ b/uSync.BackOffice/Configuration/uSyncConfigService.cs @@ -4,67 +4,66 @@ using Microsoft.Extensions.Options; -namespace uSync.BackOffice.Configuration +namespace uSync.BackOffice.Configuration; + +/// +/// Manages the configuration settings for uSync, +/// +public class uSyncConfigService { + private IOptionsMonitor _setOptionsMonitor; + /// - /// Manages the configuration settings for uSync, + /// uSync settings loaded from configuration /// - public class uSyncConfigService - { - private IOptionsMonitor _setOptionsMonitor; + public uSyncSettings Settings { get; set; } - /// - /// uSync settings loaded from configuration - /// - public uSyncSettings Settings { get; set; } - - /// - /// The unmapped root folder for uSync. - /// - [Obsolete("we should be using the array of folders, will be removed in v15")] - public string GetRootFolder() - => Settings.IsRootSite - ? Settings.Folders[0].TrimStart('/') - : Settings.RootFolder.TrimStart('/'); + /// + /// The unmapped root folder for uSync. + /// + [Obsolete("we should be using the array of folders, will be removed in v15")] + public string GetRootFolder() + => Settings.IsRootSite + ? Settings.Folders[0].TrimStart('/') + : Settings.RootFolder.TrimStart('/'); - /// - /// Get the root folders that uSync is using. - /// - /// - public string[] GetFolders() - => Settings.IsRootSite - ? [Settings.Folders[0].TrimStart('/')] - : Settings.Folders.Select(x => x.TrimStart('/')).ToArray(); - - - /// - /// Constructor for config service - /// - public uSyncConfigService( - IOptionsMonitor settingsOptionsMonitor, - IOptionsMonitor setOptionsMonitor) - { - Settings = settingsOptionsMonitor.CurrentValue; + /// + /// Get the root folders that uSync is using. + /// + /// + public string[] GetFolders() + => Settings.IsRootSite + ? [Settings.Folders[0].TrimStart('/')] + : Settings.Folders.Select(x => x.TrimStart('/')).ToArray(); - settingsOptionsMonitor.OnChange(options => - { - Settings = options; - }); - _setOptionsMonitor = setOptionsMonitor; + /// + /// Constructor for config service + /// + public uSyncConfigService( + IOptionsMonitor settingsOptionsMonitor, + IOptionsMonitor setOptionsMonitor) + { + Settings = settingsOptionsMonitor.CurrentValue; - } + settingsOptionsMonitor.OnChange(options => + { + Settings = options; + }); - /// - /// get the settings for a named handler set. - /// - public uSyncHandlerSetSettings GetSetSettings(string setName) - => _setOptionsMonitor.Get(setName); + _setOptionsMonitor = setOptionsMonitor; - /// - /// get the default handler settings for handlers - /// - public uSyncHandlerSetSettings GetDefaultSetSettings() - => GetSetSettings(Settings.DefaultSet); } + + /// + /// get the settings for a named handler set. + /// + public uSyncHandlerSetSettings GetSetSettings(string setName) + => _setOptionsMonitor.Get(setName); + + /// + /// get the default handler settings for handlers + /// + public uSyncHandlerSetSettings GetDefaultSetSettings() + => GetSetSettings(Settings.DefaultSet); } diff --git a/uSync.BackOffice/Configuration/uSyncHandlerSetSettings.cs b/uSync.BackOffice/Configuration/uSyncHandlerSetSettings.cs index 17df2103..ac483f20 100644 --- a/uSync.BackOffice/Configuration/uSyncHandlerSetSettings.cs +++ b/uSync.BackOffice/Configuration/uSyncHandlerSetSettings.cs @@ -2,62 +2,60 @@ using System.Collections.Generic; using System.ComponentModel; -namespace uSync.BackOffice.Configuration +namespace uSync.BackOffice.Configuration; + +/// +/// Settings for a handler set (group of handlers) +/// +public class uSyncHandlerSetSettings { /// - /// Settings for a handler set (group of handlers) + /// Is this handler set enabled /// - public class uSyncHandlerSetSettings - { - /// - /// Is this handler set enabled - /// - [DefaultValue(true)] - public bool Enabled { get; set; } = true; - - /// - /// List of groups handlers can belong to. - /// - public string[] HandlerGroups { get; set; } = Array.Empty(); - - /// - /// List of disabled handlers - /// - public string[] DisabledHandlers { get; set; } = Array.Empty(); - - /// - /// Default settings for all handlers - /// - public HandlerSettings HandlerDefaults { get; set; } = new HandlerSettings(); - - /// - /// Settings for named handlers - /// - public IDictionary Handlers { get; set; } = new Dictionary(); - - /// - /// for handlers to appear in the drop down on the dashboard they have to be selectable - /// - public bool IsSelectable { get; set; } = false; + [DefaultValue(true)] + public bool Enabled { get; set; } = true; + /// + /// List of groups handlers can belong to. + /// + public string[] HandlerGroups { get; set; } = Array.Empty(); - } + /// + /// List of disabled handlers + /// + public string[] DisabledHandlers { get; set; } = Array.Empty(); + + /// + /// Default settings for all handlers + /// + public HandlerSettings HandlerDefaults { get; set; } = new HandlerSettings(); + + /// + /// Settings for named handlers + /// + public IDictionary Handlers { get; set; } = new Dictionary(); /// - /// Extensions to make handling the settings easier. + /// for handlers to appear in the drop down on the dashboard they have to be selectable /// - public static class HandlerSetSettingsExtensions + public bool IsSelectable { get; set; } = false; + + +} + +/// +/// Extensions to make handling the settings easier. +/// +public static class HandlerSetSettingsExtensions +{ + /// + /// Get the handler settings for the named handler - (will load defaults if no speicifc handler settings are found) + /// + public static HandlerSettings GetHandlerSettings(this uSyncHandlerSetSettings handlerSet, string alias) { - /// - /// Get the handler settings for the named handler - (will load defaults if no speicifc handler settings are found) - /// - public static HandlerSettings GetHandlerSettings(this uSyncHandlerSetSettings handlerSet, string alias) - { - if (handlerSet.Handlers.ContainsKey(alias)) - return handlerSet.Handlers[alias].Clone(); - - return handlerSet.HandlerDefaults.Clone(); - } - } + if (handlerSet.Handlers.ContainsKey(alias)) + return handlerSet.Handlers[alias].Clone(); + return handlerSet.HandlerDefaults.Clone(); + } } diff --git a/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs b/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs index ef481fb0..1468f1d2 100644 --- a/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs +++ b/uSync.BackOffice/Configuration/uSyncHandlerSettings.cs @@ -4,122 +4,120 @@ using Umbraco.Extensions; -namespace uSync.BackOffice.Configuration +namespace uSync.BackOffice.Configuration; + +/// +/// Settings to control who a handler works +/// +public class HandlerSettings { /// - /// Settings to control who a handler works + /// Is handler enabled or disabled /// - public class HandlerSettings - { - /// - /// Is handler enabled or disabled - /// - [DefaultValue(true)] - public bool Enabled { get; set; } = true; - - /// - /// List of actions the handler is configured for. - /// - public string[] Actions { get; set; } = Array.Empty(); - - /// - /// Should use a flat folder structure when exporting items - /// - [DefaultValue(true)] - public bool UseFlatStructure { get; set; } = true; - - /// - /// Items should be saved with their guid/key value as the filename - /// - [DefaultValue(false)] - public bool GuidNames { get; set; } = false; - - /// - /// Imports should fail if the parent item is missing (if false, item be importated go a close as possible to location) - /// - [DefaultValue(false)] - public bool FailOnMissingParent { get; set; } = false; - - /// - /// Override the group the handler belongs too. - /// - [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 - /// - - // TODO: v13 - change this to string, object settings collection. - // makes for better intellisense from schema. - public Dictionary Settings { get; set; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - } + [DefaultValue(true)] + public bool Enabled { get; set; } = true; /// - /// Extensions to the handler settings + /// List of actions the handler is configured for. /// - public static class HandlerSettingsExtensions - { - /// - /// get a setting from the settings dictionary. - /// - /// - /// - /// - /// - /// - public static TResult GetSetting(this HandlerSettings settings, string key, TResult defaultValue) - { - if (settings.Settings != null && settings.Settings.ContainsKey(key)) - { - var attempt = settings.Settings[key].TryConvertTo(); - if (attempt) return attempt.Result; - } + public string[] Actions { get; set; } = Array.Empty(); - return defaultValue; - } + /// + /// Should use a flat folder structure when exporting items + /// + [DefaultValue(true)] + public bool UseFlatStructure { get; set; } = true; - /// - /// Add a setting to the settings Dictionary (creating the dictionary if its missing) - /// - /// - /// - /// - public static void AddSetting(this HandlerSettings settings, string key, TObject value) - { - if (settings.Settings == null) - settings.Settings = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + /// + /// Items should be saved with their guid/key value as the filename + /// + [DefaultValue(false)] + public bool GuidNames { get; set; } = false; - settings.Settings[key] = value.ToString(); - } + /// + /// Imports should fail if the parent item is missing (if false, item be importated go a close as possible to location) + /// + [DefaultValue(false)] + public bool FailOnMissingParent { get; set; } = false; + + /// + /// Override the group the handler belongs too. + /// + [DefaultValue("")] + public string Group { get; set; } = string.Empty; - /// - /// create a copy of this handlers settings - /// - /// - /// - public static HandlerSettings Clone(this HandlerSettings settings) + /// + /// 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 + /// + + // TODO: v13 - change this to string, object settings collection. + // makes for better intellisense from schema. + public Dictionary Settings { get; set; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); +} + +/// +/// Extensions to the handler settings +/// +public static class HandlerSettingsExtensions +{ + /// + /// get a setting from the settings dictionary. + /// + /// + /// + /// + /// + /// + public static TResult GetSetting(this HandlerSettings settings, string key, TResult defaultValue) + { + if (settings.Settings != null && settings.Settings.ContainsKey(key)) { - return new HandlerSettings - { - Actions = settings.Actions, - Enabled = settings.Enabled, - FailOnMissingParent = settings.FailOnMissingParent, - UseFlatStructure = settings.UseFlatStructure, - Group = settings.Group, - GuidNames = settings.GuidNames, - Settings = new Dictionary(settings.Settings, StringComparer.InvariantCultureIgnoreCase) - }; + var attempt = settings.Settings[key].TryConvertTo(); + if (attempt) return attempt.Result ?? defaultValue; } + return defaultValue; + } + + /// + /// Add a setting to the settings Dictionary (creating the dictionary if its missing) + /// + /// + /// + /// + public static void AddSetting(this HandlerSettings settings, string key, TObject value) + { + if (settings.Settings == null) + settings.Settings = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + settings.Settings.TryAdd(key, value?.ToString() ?? string.Empty); + } + + /// + /// create a copy of this handlers settings + /// + /// + /// + public static HandlerSettings Clone(this HandlerSettings settings) + { + return new HandlerSettings + { + Actions = settings.Actions, + Enabled = settings.Enabled, + FailOnMissingParent = settings.FailOnMissingParent, + UseFlatStructure = settings.UseFlatStructure, + Group = settings.Group, + GuidNames = settings.GuidNames, + Settings = new Dictionary(settings.Settings, StringComparer.InvariantCultureIgnoreCase) + }; } } diff --git a/uSync.BackOffice/Configuration/uSyncSettings.cs b/uSync.BackOffice/Configuration/uSyncSettings.cs index f20b80a3..52a8551b 100644 --- a/uSync.BackOffice/Configuration/uSyncSettings.cs +++ b/uSync.BackOffice/Configuration/uSyncSettings.cs @@ -2,208 +2,207 @@ using System.Collections.Generic; using System.ComponentModel; -namespace uSync.BackOffice.Configuration +namespace uSync.BackOffice.Configuration; + +/// +/// uSync Settings +/// +public class uSyncSettings { /// - /// uSync Settings - /// - public class uSyncSettings - { - /// - /// Location where all uSync files are saved by default - /// - [DefaultValue("uSync/v14/")] - public string RootFolder { get; set; } = "uSync/v14/"; - - /// - /// collection of folders uSync looks in when performing imports. - /// - [DefaultValue("uSync/Root/, uSync/v14/")] - public string[] Folders { get; set; } = [ ]; - - /// - /// folder that has old things in. - /// - [DefaultValue("uSync/v9")] - public string LegacyFolder { get; set; } = "uSync/v9"; - - /// - /// Sets this site to be the root site (so it will save into "uSync/root/") - /// - [DefaultValue(false)] - public bool IsRootSite { get; set; } = false; - - /// - /// when locked you can't make changes to anything that is in the root - /// - [DefaultValue (true)] - public bool LockRoot { get; set; } = true; - - /// - /// lock specific types at root so they can't be changed in child sites. - /// - /// - /// document, media, member, dictionary-item, macro, template, document-type, - /// media-type, data-type, member-type, member-group, relation-type, forms-form, - /// forms-prevalue, forms-datasource, language - /// - public string[] LockRootTypes { get; set; } = []; - - /// - /// The default handler set to use on all notification triggered events - /// - [DefaultValue(uSync.Sets.DefaultSet)] - public string DefaultSet { get; set; } = uSync.Sets.DefaultSet; - - /// - /// Import when Umbraco boots (can be group name or 'All' so everything is done, blank or 'none' == off) - /// - [DefaultValue("None")] - public string ImportAtStartup { get; set; } = "None"; - - /// - /// Export when Umbraco boots - /// - [DefaultValue("None")] - public string ExportAtStartup { get; set; } = "None"; - - /// - /// Export when an item is saved in Umbraco - /// - [DefaultValue("All")] - public string ExportOnSave { get; set; } = "All"; - - - /// - /// The handler groups that are enabled in the UI. - /// - [DefaultValue("All")] - public string UIEnabledGroups { get; set; } = "All"; - - /// - /// Debug reports (creates an export into a temp folder for comparison) - /// - [DefaultValue(false)] - public bool ReportDebug { get; set; } = false; - - /// - /// Ping the AddOnUrl to get the json used to show the addons dashboard - /// - [DefaultValue(true)] - public bool AddOnPing { get; set; } = true; - - /// - /// Pre Umbraco 8.4 - rebuild the cache was needed after content was imported - /// - [DefaultValue(false)] - public bool RebuildCacheOnCompletion { get; set; } = false; - - /// - /// Fail if the items parent is not in umbraco or part of the batch being imported - /// - [DefaultValue(false)] - public bool FailOnMissingParent { get; set; } = false; - - /// - /// fail if a duplicate file of same type and key is detected during the import process. - /// - [DefaultValue(false)] - public bool FailOnDuplicates { get; set; } = false; - - /// - /// Should folder keys be cached (for speed) - /// - [DefaultValue(true)] - public bool CacheFolderKeys { get; set; } = true; - - - /// - /// Show a version check warning to the user if the folder version is less than the version expected by uSync. - /// - [DefaultValue(true)] - public bool ShowVersionCheckWarning { get; set; } = true; - - /// - /// Custom mapping keys, allows users to add a simple config mapping to make one property type to behave like an existing one - /// - public IDictionary CustomMappings { get; set; } = new Dictionary(); - - /// - /// location of SignalR hub script - /// - [DefaultValue("")] - [Obsolete("Will remove the option to move the route in v13")] - public string SignalRRoot { get; set; } = string.Empty; - - /// - /// Should the history view be on of off ? - /// - [DefaultValue(true)] - public bool EnableHistory { get; set; } = true; - - /// - /// Default file extension for the uSync files. - /// - [DefaultValue("config")] - public string DefaultExtension { get; set; } = "config"; - - /// - /// Import the uSync folder on the first boot. - /// - [DefaultValue(false)] - public bool ImportOnFirstBoot { get; set; } = false ; - - /// - /// Handler group(s) to run on first boot, default is All (so full import) - /// - [DefaultValue("All")] - public string FirstBootGroup { get; set; } = "All"; - - /// - /// Disable the default dashboard (so people can't accedently press the buttons). - /// - [DefaultValue("false")] - public bool DisableDashboard { get; set; } = false; - - /// - /// summerize results (for when there are loads and loads of items) - /// - [DefaultValue("false")] - public bool SummaryDashboard { get; set; } = false; - - /// - /// limit of items to display before flicking to summary view. (this is per handler) - /// - [DefaultValue(1000)] - public int SummaryLimit { get; set; } = 1000; - - /// - /// list of addon (tabs) you don't want to show inside uSync dashboard. - /// - public string HideAddOns { get; set; } = "licence"; - - /// - /// turns of use of the Notifications.Supress 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. - /// - /// until we have had time to look at this , we will leave this as - /// disabled by default so all notification messages fire. - /// - [DefaultValue("false")] - public bool DisableNotificationSuppression { get; set; } = false; - - /// - /// trigger all the notifications in a background thread, - /// - /// - /// uSync will process imports faster, but any notification events will - /// fire off afterward in the background. - /// - [DefaultValue(false)] - public bool BackgroundNotifications { get; set; } = false; - } + /// Location where all uSync files are saved by default + /// + [DefaultValue("uSync/v14/")] + public string RootFolder { get; set; } = "uSync/v14/"; + + /// + /// collection of folders uSync looks in when performing imports. + /// + [DefaultValue("uSync/Root/, uSync/v14/")] + public string[] Folders { get; set; } = []; + + /// + /// folder that has old things in. + /// + [DefaultValue("uSync/v9")] + public string LegacyFolder { get; set; } = "uSync/v9"; + + /// + /// Sets this site to be the root site (so it will save into "uSync/root/") + /// + [DefaultValue(false)] + public bool IsRootSite { get; set; } = false; + + /// + /// when locked you can't make changes to anything that is in the root + /// + [DefaultValue(true)] + public bool LockRoot { get; set; } = true; + + /// + /// lock specific types at root so they can't be changed in child sites. + /// + /// + /// document, media, member, dictionary-item, macro, template, document-type, + /// media-type, data-type, member-type, member-group, relation-type, forms-form, + /// forms-prevalue, forms-datasource, language + /// + public string[] LockRootTypes { get; set; } = []; + + /// + /// The default handler set to use on all notification triggered events + /// + [DefaultValue(uSync.Sets.DefaultSet)] + public string DefaultSet { get; set; } = uSync.Sets.DefaultSet; + + /// + /// Import when Umbraco boots (can be group name or 'All' so everything is done, blank or 'none' == off) + /// + [DefaultValue("None")] + public string ImportAtStartup { get; set; } = "None"; + + /// + /// Export when Umbraco boots + /// + [DefaultValue("None")] + public string ExportAtStartup { get; set; } = "None"; + + /// + /// Export when an item is saved in Umbraco + /// + [DefaultValue("All")] + public string ExportOnSave { get; set; } = "All"; + + + /// + /// The handler groups that are enabled in the UI. + /// + [DefaultValue("All")] + public string UIEnabledGroups { get; set; } = "All"; + + /// + /// Debug reports (creates an export into a temp folder for comparison) + /// + [DefaultValue(false)] + public bool ReportDebug { get; set; } = false; + + /// + /// Ping the AddOnUrl to get the json used to show the addons dashboard + /// + [DefaultValue(true)] + public bool AddOnPing { get; set; } = true; + + /// + /// Pre Umbraco 8.4 - rebuild the cache was needed after content was imported + /// + [DefaultValue(false)] + public bool RebuildCacheOnCompletion { get; set; } = false; + + /// + /// Fail if the items parent is not in umbraco or part of the batch being imported + /// + [DefaultValue(false)] + public bool FailOnMissingParent { get; set; } = false; + + /// + /// fail if a duplicate file of same type and key is detected during the import process. + /// + [DefaultValue(false)] + public bool FailOnDuplicates { get; set; } = false; + + /// + /// Should folder keys be cached (for speed) + /// + [DefaultValue(true)] + public bool CacheFolderKeys { get; set; } = true; + + + /// + /// Show a version check warning to the user if the folder version is less than the version expected by uSync. + /// + [DefaultValue(true)] + public bool ShowVersionCheckWarning { get; set; } = true; + + /// + /// Custom mapping keys, allows users to add a simple config mapping to make one property type to behave like an existing one + /// + public IDictionary CustomMappings { get; set; } = new Dictionary(); + + /// + /// location of SignalR hub script + /// + [DefaultValue("")] + [Obsolete("Will remove the option to move the route in v13")] + public string SignalRRoot { get; set; } = string.Empty; + + /// + /// Should the history view be on of off ? + /// + [DefaultValue(true)] + public bool EnableHistory { get; set; } = true; + + /// + /// Default file extension for the uSync files. + /// + [DefaultValue("config")] + public string DefaultExtension { get; set; } = "config"; + + /// + /// Import the uSync folder on the first boot. + /// + [DefaultValue(false)] + public bool ImportOnFirstBoot { get; set; } = false; + + /// + /// Handler group(s) to run on first boot, default is All (so full import) + /// + [DefaultValue("All")] + public string FirstBootGroup { get; set; } = "All"; + + /// + /// Disable the default dashboard (so people can't accedently press the buttons). + /// + [DefaultValue("false")] + public bool DisableDashboard { get; set; } = false; + + /// + /// summerize results (for when there are loads and loads of items) + /// + [DefaultValue("false")] + public bool SummaryDashboard { get; set; } = false; + + /// + /// limit of items to display before flicking to summary view. (this is per handler) + /// + [DefaultValue(1000)] + public int SummaryLimit { get; set; } = 1000; + + /// + /// list of addon (tabs) you don't want to show inside uSync dashboard. + /// + public string HideAddOns { get; set; } = "licence"; + + /// + /// turns of use of the Notifications.Supress 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. + /// + /// until we have had time to look at this , we will leave this as + /// disabled by default so all notification messages fire. + /// + [DefaultValue("false")] + public bool DisableNotificationSuppression { get; set; } = false; + + /// + /// trigger all the notifications in a background thread, + /// + /// + /// uSync will process imports faster, but any notification events will + /// fire off afterward in the background. + /// + [DefaultValue(false)] + public bool BackgroundNotifications { get; set; } = false; } diff --git a/uSync.BackOffice/Expansions/ISyncTreeNode.cs b/uSync.BackOffice/Expansions/ISyncTreeNode.cs index c917397c..c0c5540a 100644 --- a/uSync.BackOffice/Expansions/ISyncTreeNode.cs +++ b/uSync.BackOffice/Expansions/ISyncTreeNode.cs @@ -5,92 +5,91 @@ using Umbraco.Cms.Core.Trees; -namespace uSync.BackOffice.Expansions +namespace uSync.BackOffice.Expansions; + +/// +/// add on for uSync that allows you to render a node +/// under the uSync tree. +/// +public interface ISyncTreeNode +{ + /// + /// if the tree node is in-fact enabled. + /// + public bool Disabled { get; } + + /// + /// position in the tree, (higher is lower down the tree) + /// + public int Weight { get; } + + /// + /// Id this will be passed to the controller. + /// + public string Id { get; } + + /// + /// alias for the tree + /// + public string TreeAlias { get; } + + /// + /// alias for the node item + /// + public string Alias { get; } + + /// + /// title of the tree item + /// + public string Title { get; } + + /// + /// icon for the tree item. + /// + public string Icon { get; } + + + /// + /// method to return any additional child nodes under the parent node + /// + public IEnumerable GetChildNodes(string id, FormCollection queryStrings); + + /// + /// to display any context menu. + /// + /// + /// + /// + public ActionResult GetMenuItems(string id, FormCollection queryStrings); +} + +/// +/// Representation of a single tree node +/// +public class uSyncTreeNode { /// - /// add on for uSync that allows you to render a node - /// under the uSync tree. + /// Id for this tree node + /// + public string? Id { get; set; } + + /// + /// Alias of the tree item + /// + public string? Alias { get; set; } + + /// + /// title (shown to user) for tree item + /// + public string? Title { get; set; } + + /// + /// Icon to display. /// - public interface ISyncTreeNode - { - /// - /// if the tree node is in-fact enabled. - /// - public bool Disabled { get; } - - /// - /// position in the tree, (higher is lower down the tree) - /// - public int Weight { get; } - - /// - /// Id this will be passed to the controller. - /// - public string Id { get; } - - /// - /// alias for the tree - /// - public string TreeAlias { get; } - - /// - /// alias for the node item - /// - public string Alias { get; } - - /// - /// title of the tree item - /// - public string Title { get; } - - /// - /// icon for the tree item. - /// - public string Icon { get; } - - - /// - /// method to return any additional child nodes under the parent node - /// - public IEnumerable GetChildNodes(string id, FormCollection queryStrings); - - /// - /// to display any context menu. - /// - /// - /// - /// - public ActionResult GetMenuItems(string id, FormCollection queryStrings); - } + public string? Icon { get; set; } /// - /// Representation of a single tree node + /// segment path to this item. /// - public class uSyncTreeNode - { - /// - /// Id for this tree node - /// - public string Id { get; set; } - - /// - /// Alias of the tree item - /// - public string Alias { get; set; } - - /// - /// title (shown to user) for tree item - /// - public string Title { get; set; } - - /// - /// Icon to display. - /// - public string Icon { get; set; } - - /// - /// segment path to this item. - /// - public string Path { get; set; } - } + public string? Path { get; set; } } diff --git a/uSync.BackOffice/Expansions/SyncTreeNodeCollection.cs b/uSync.BackOffice/Expansions/SyncTreeNodeCollection.cs index dec5f81a..a04797a8 100644 --- a/uSync.BackOffice/Expansions/SyncTreeNodeCollection.cs +++ b/uSync.BackOffice/Expansions/SyncTreeNodeCollection.cs @@ -1,29 +1,29 @@ -using System; -using System.Collections.Generic; +//using System; +//using System.Collections.Generic; -using Umbraco.Cms.Core.Composing; +//using Umbraco.Cms.Core.Composing; -namespace uSync.BackOffice.Expansions; +//namespace uSync.BackOffice.Expansions; -/// -/// collection of UI tree nodes, allows us to dynamically extend the uSync tree -/// -public class SyncTreeNodeCollection - : BuilderCollectionBase -{ - /// - public SyncTreeNodeCollection(Func> items) - : base(items) - { } -} +///// +///// collection of UI tree nodes, allows us to dynamically extend the uSync tree +///// +//public class SyncTreeNodeCollection +// : BuilderCollectionBase +//{ +// /// +// public SyncTreeNodeCollection(Func> items) +// : base(items) +// { } +//} -/// -/// collection builder for UI tree nodes under uSync tree.(subtrees) -/// -public class SyncTreeNodeCollectionBuilder - : LazyCollectionBuilderBase -{ - /// - protected override SyncTreeNodeCollectionBuilder This => this; -} +///// +///// collection builder for UI tree nodes under uSync tree.(subtrees) +///// +//public class SyncTreeNodeCollectionBuilder +// : LazyCollectionBuilderBase +//{ +// /// +// protected override SyncTreeNodeCollectionBuilder This => this; +//} diff --git a/uSync.BackOffice/Expansions/uSyncTreeController.cs b/uSync.BackOffice/Expansions/uSyncTreeController.cs index 36bdccfe..d7eb351d 100644 --- a/uSync.BackOffice/Expansions/uSyncTreeController.cs +++ b/uSync.BackOffice/Expansions/uSyncTreeController.cs @@ -1,130 +1,130 @@ -using System.Linq; - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Trees; -using Umbraco.Cms.Web.BackOffice.Trees; -using Umbraco.Cms.Web.Common.Attributes; -using Umbraco.Cms.Web.Common.ModelBinders; -using Umbraco.Extensions; - -namespace uSync.BackOffice.Expansions -{ - /// - /// Tree controller for the 'uSync' tree - /// - [Tree(Constants.Applications.Settings, uSync.Trees.uSync, - TreeGroup = uSync.Trees.Group, - TreeTitle = uSync.Name, SortOrder = 35)] - [PluginController(uSync.Name)] - public class uSyncTreeController : TreeController - { - /// - /// Collection of nodes underneath uSync tree item - /// - public SyncTreeNodeCollection _treeNodes; - - private readonly IMenuItemCollectionFactory _menuItemsFactory; - - /// - public uSyncTreeController( - ILocalizedTextService localizedTextService, - UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, - IEventAggregator eventAggregator, - SyncTreeNodeCollection treeNodes, - IMenuItemCollectionFactory menuItemsFactory) - : base(localizedTextService, umbracoApiControllerTypeCollection, eventAggregator) - { - _treeNodes = treeNodes; - _menuItemsFactory = menuItemsFactory; - } - - /// - protected override ActionResult CreateRootNode(FormCollection queryStrings) - { - var result = base.CreateRootNode(queryStrings); - - result.Value.RoutePath = $"{SectionAlias}/{uSync.Trees.uSync}/dashboard"; - result.Value.Icon = "icon-infinity"; - result.Value.HasChildren = _treeNodes.Count > 0; - result.Value.MenuUrl = null; - - return result.Value; - } - - private string getParentId(string id) - => id.IndexOf('_') < 0 ? id : id.Substring(0, id.IndexOf("_")); - - /// - protected override ActionResult GetMenuForNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) - { - var defaultMenu = _menuItemsFactory.Create(); - - if (_treeNodes.Count == 0) return defaultMenu; - if (id == Constants.System.RootString) return defaultMenu; - - var parentId = getParentId(id); - var current = _treeNodes.FirstOrDefault(x => x.Id == parentId); - return current?.GetMenuItems(id, queryStrings) ?? defaultMenu; - } - - /// - protected override ActionResult GetTreeNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) - { - if (_treeNodes.Count == 0) return new TreeNodeCollection(); - - var collection = new TreeNodeCollection(); - - if (id == Constants.System.RootString) - { - foreach (var node in _treeNodes.Where(x => !x.Disabled).OrderBy(x => x.Weight)) - { - var treeNode = CreateTreeNode( - node.Id, - id, - queryStrings, - node.Title, - node.Icon, - $"{SectionAlias}/{node.TreeAlias}/{node.Alias}"); - - var children = node.GetChildNodes(id, queryStrings); - if (children?.Any() == true) - treeNode.HasChildren = true; - - collection.Add(treeNode); - - } - - return collection; - } - else - { - var treeNode = _treeNodes.FirstOrDefault(x => x.Id == getParentId(id)); - if (treeNode != null) - { - var children = treeNode.GetChildNodes(id, queryStrings); - if (children != null) - { - foreach (var child in children) - { - collection.Add(CreateTreeNode( - $"{id}_{child.Id}", - id, - queryStrings, - child.Title, - child.Icon, - $"{SectionAlias}/{treeNode.TreeAlias}/{child.Path}")); - } - } - } - } - - return collection; - - } - } -} +//using System.Linq; + +//using Microsoft.AspNetCore.Http; +//using Microsoft.AspNetCore.Mvc; + +//using Umbraco.Cms.Core; +//using Umbraco.Cms.Core.Events; +//using Umbraco.Cms.Core.Services; +//using Umbraco.Cms.Core.Trees; +//using Umbraco.Cms.Web.BackOffice.Trees; +//using Umbraco.Cms.Web.Common.Attributes; +//using Umbraco.Cms.Web.Common.ModelBinders; +//using Umbraco.Extensions; + +//namespace uSync.BackOffice.Expansions +//{ +// /// +// /// Tree controller for the 'uSync' tree +// /// +// [Tree(Constants.Applications.Settings, uSync.Trees.uSync, +// TreeGroup = uSync.Trees.Group, +// TreeTitle = uSync.Name, SortOrder = 35)] +// [PluginController(uSync.Name)] +// public class uSyncTreeController : TreeController +// { +// /// +// /// Collection of nodes underneath uSync tree item +// /// +// public SyncTreeNodeCollection _treeNodes; + +// private readonly IMenuItemCollectionFactory _menuItemsFactory; + +// /// +// public uSyncTreeController( +// ILocalizedTextService localizedTextService, +// UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, +// IEventAggregator eventAggregator, +// SyncTreeNodeCollection treeNodes, +// IMenuItemCollectionFactory menuItemsFactory) +// : base(localizedTextService, umbracoApiControllerTypeCollection, eventAggregator) +// { +// _treeNodes = treeNodes; +// _menuItemsFactory = menuItemsFactory; +// } + +// /// +// protected override ActionResult CreateRootNode(FormCollection queryStrings) +// { +// var result = base.CreateRootNode(queryStrings); + +// result.Value.RoutePath = $"{SectionAlias}/{uSync.Trees.uSync}/dashboard"; +// result.Value.Icon = "icon-infinity"; +// result.Value.HasChildren = _treeNodes.Count > 0; +// result.Value.MenuUrl = null; + +// return result.Value; +// } + +// private string getParentId(string id) +// => id.IndexOf('_') < 0 ? id : id.Substring(0, id.IndexOf("_")); + +// /// +// protected override ActionResult GetMenuForNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) +// { +// var defaultMenu = _menuItemsFactory.Create(); + +// if (_treeNodes.Count == 0) return defaultMenu; +// if (id == Constants.System.RootString) return defaultMenu; + +// var parentId = getParentId(id); +// var current = _treeNodes.FirstOrDefault(x => x.Id == parentId); +// return current?.GetMenuItems(id, queryStrings) ?? defaultMenu; +// } + +// /// +// protected override ActionResult GetTreeNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) +// { +// if (_treeNodes.Count == 0) return new TreeNodeCollection(); + +// var collection = new TreeNodeCollection(); + +// if (id == Constants.System.RootString) +// { +// foreach (var node in _treeNodes.Where(x => !x.Disabled).OrderBy(x => x.Weight)) +// { +// var treeNode = CreateTreeNode( +// node.Id, +// id, +// queryStrings, +// node.Title, +// node.Icon, +// $"{SectionAlias}/{node.TreeAlias}/{node.Alias}"); + +// var children = node.GetChildNodes(id, queryStrings); +// if (children?.Any() == true) +// treeNode.HasChildren = true; + +// collection.Add(treeNode); + +// } + +// return collection; +// } +// else +// { +// var treeNode = _treeNodes.FirstOrDefault(x => x.Id == getParentId(id)); +// if (treeNode != null) +// { +// var children = treeNode.GetChildNodes(id, queryStrings); +// if (children != null) +// { +// foreach (var child in children) +// { +// collection.Add(CreateTreeNode( +// $"{id}_{child.Id}", +// id, +// queryStrings, +// child.Title, +// child.Icon, +// $"{SectionAlias}/{treeNode.TreeAlias}/{child.Path}")); +// } +// } +// } +// } + +// return collection; + +// } +// } +//} diff --git a/uSync.BackOffice/Extensions/ScopeExtensions.cs b/uSync.BackOffice/Extensions/ScopeExtensions.cs index 17356139..fbe5983c 100644 --- a/uSync.BackOffice/Extensions/ScopeExtensions.cs +++ b/uSync.BackOffice/Extensions/ScopeExtensions.cs @@ -26,20 +26,20 @@ public static ICoreScope CreateNotificationScope( ILoggerFactory loggerFactory, uSyncConfigService syncConfigService, uSyncEventService syncEventService, - IBackgroundTaskQueue backgroundTaskQueue, - SyncUpdateCallback callback) + IBackgroundTaskQueue? backgroundTaskQueue, + SyncUpdateCallback? callback) { - + var notificationPublisher = new SyncScopedNotificationPublisher( eventAggregator, loggerFactory.CreateLogger(), - callback, + callback, syncConfigService, backgroundTaskQueue, syncEventService); return scopeProvider.CreateCoreScope( - scopedNotificationPublisher: notificationPublisher, + scopedNotificationPublisher: notificationPublisher, autoComplete: true); } } diff --git a/uSync.BackOffice/Extensions/uSyncActionExtensions.cs b/uSync.BackOffice/Extensions/uSyncActionExtensions.cs index 9aba66b7..4024b5c3 100644 --- a/uSync.BackOffice/Extensions/uSyncActionExtensions.cs +++ b/uSync.BackOffice/Extensions/uSyncActionExtensions.cs @@ -5,59 +5,60 @@ using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Extensions for working with uSyncActions +/// +public static class uSyncActionExtensions { /// - /// Extensions for working with uSyncActions + /// does this list of actions have any that in an error state? + /// + public static bool ContainsErrors(this IEnumerable actions) + => actions.Any(x => x.Change >= Core.ChangeType.Fail || !x.Success); + + /// + /// count how many actions in this list are for changes + /// + public static int CountChanges(this IEnumerable actions) + => actions.Count(x => x.Change > Core.ChangeType.NoChange && x.Change < Core.ChangeType.Hidden); + + /// + /// checks to see if the requested action is valid for the configured list of actions. + /// + public static bool IsValidAction(this HandlerActions requestedAction, IEnumerable actions) + => requestedAction == HandlerActions.None || + actions.Count() == 0 || + actions.InvariantContains("all") || + actions.InvariantContains(requestedAction.ToString()); + + /// + /// Convert a list of actions into a summary list of actions, uses less cpu when people sync massive amounts of content. /// - public static class uSyncActionExtensions + public static IEnumerable ConvertToSummary(this IEnumerable actions, bool strict) { - /// - /// does this list of actions have any that in an error state? - /// - public static bool ContainsErrors(this IEnumerable actions) - => actions.Any(x => x.Change >= Core.ChangeType.Fail || !x.Success); - - /// - /// count how many actions in this list are for changes - /// - public static int CountChanges(this IEnumerable actions) - => actions.Count(x => x.Change > Core.ChangeType.NoChange && x.Change < Core.ChangeType.Hidden); - - /// - /// checks to see if the requested action is valid for the configured list of actions. - /// - public static bool IsValidAction(this HandlerActions requestedAction, IEnumerable actions) - => requestedAction == HandlerActions.None || - actions.Count() == 0 || - actions.InvariantContains("all") || - actions.InvariantContains(requestedAction.ToString()); - - /// - /// Convert a list of actions into a summary list of actions, uses less cpu when people sync massive amounts of content. - /// - public static IEnumerable ConvertToSummary(this IEnumerable actions, bool strict) + var summary = new List(); + + foreach (var items in actions.GroupBy(x => x.HandlerAlias)) { - var summary = new List(); + if (items.Key is null) continue; - foreach(var items in actions.GroupBy(x => x.HandlerAlias)) - { - var fails = items.Where(x => !x.Success).ToList(); + var fails = items.Where(x => !x.Success).ToList(); - summary.Add(uSyncAction.SetAction( - success: true, - name: items.Key, - type: items.Key, - change: Core.ChangeType.Information, - message: $"({items.CountChanges()}/{items.Count()} Changes) ({fails.Count} failures)") - ); + summary.Add(uSyncAction.SetAction( + success: true, + name: items.Key, + type: items.Key, + change: Core.ChangeType.Information, + message: $"({items.CountChanges()}/{items.Count()} Changes) ({fails.Count} failures)") + ); - if (!strict) summary.AddRange(fails); - - } + if (!strict) summary.AddRange(fails); - return summary; } + return summary; } + } diff --git a/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs b/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs index 7c0ff015..03e3e588 100644 --- a/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs +++ b/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs @@ -2,11 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using System.Linq; using System.Xml.Linq; using Umbraco.Cms.Core.HealthChecks; -using Umbraco.Extensions; using uSync.BackOffice.Configuration; using uSync.BackOffice.Services; @@ -49,7 +47,7 @@ public override Task> GetStatus() CheckConfigFolderValidity() }; - + return Task.FromResult((IEnumerable)items); } @@ -95,13 +93,14 @@ private List CheckFolder(string folder) var files = Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories); - foreach(var file in files) + foreach (var file in files) { try { var node = XElement.Load(file); - if (!node.IsEmptyItem()) { + if (!node.IsEmptyItem()) + { var key = node.GetKey(); var folderName = Path.GetFileName(Path.GetDirectoryName(file)); @@ -143,13 +142,13 @@ private HealthCheckStatus CheckConfigFolderValidity() List errors = new List(); - foreach(var file in Directory.GetFiles(root, "*.config", SearchOption.AllDirectories)) + foreach (var file in Directory.GetFiles(root, "*.config", SearchOption.AllDirectories)) { try { var node = XElement.Load(file); } - catch(Exception ex) + catch (Exception ex) { errors.Add($"
  • {Path.GetFileName(Path.GetDirectoryName(file))}\\{Path.GetFileName(file)} is invalid {ex.Message}
  • "); } diff --git a/uSync.BackOffice/Hubs/HubClientService.cs b/uSync.BackOffice/Hubs/HubClientService.cs index 4ea9d99b..0a02e6fd 100644 --- a/uSync.BackOffice/Hubs/HubClientService.cs +++ b/uSync.BackOffice/Hubs/HubClientService.cs @@ -4,86 +4,85 @@ using uSync.BackOffice.Models; -namespace uSync.BackOffice.Hubs +namespace uSync.BackOffice.Hubs; + +/// +/// Service to mange SignalR comms for uSync +/// +public class HubClientService { + private readonly IHubContext hubContext; + private readonly string clientId; + /// - /// Service to mange SignalR comms for uSync + /// Construct an new HubClientService (via DI) /// - public class HubClientService + public HubClientService(IHubContext hubContext, string clientId) { - private readonly IHubContext hubContext; - private readonly string clientId; - - /// - /// Construct an new HubClientService (via DI) - /// - public HubClientService(IHubContext hubContext, string clientId) - { - this.hubContext = hubContext; - this.clientId = clientId; - } + this.hubContext = hubContext; + this.clientId = clientId; + } - /// - /// Send an 'add' message to the client - /// - public void SendMessage(TObject item) + /// + /// Send an 'add' message to the client + /// + public void SendMessage(TObject item) + { + if (hubContext != null && !string.IsNullOrWhiteSpace(clientId)) { - if (hubContext != null && !string.IsNullOrWhiteSpace(clientId)) + var client = hubContext.Clients.Client(clientId); + if (client != null) { - var client = hubContext.Clients.Client(clientId); - if (client != null) - { - client.SendAsync("Add", item).Wait(); - return; - } - - hubContext.Clients.All.SendAsync("Add", item).Wait(); + client.SendAsync("Add", item).Wait(); + return; } + + hubContext.Clients.All.SendAsync("Add", item).Wait(); } + } - /// - /// Send an 'update' message to the client - /// - public void SendUpdate(Object message) + /// + /// Send an 'update' message to the client + /// + public void SendUpdate(Object message) + { + if (hubContext != null && !string.IsNullOrWhiteSpace(clientId)) { - if (hubContext != null && !string.IsNullOrWhiteSpace(clientId)) + var client = hubContext.Clients.Client(clientId); + if (client != null) { - var client = hubContext.Clients.Client(clientId); - if (client != null) - { - client.SendAsync("Update", message).Wait(); - return; - } - hubContext.Clients.All.SendAsync("Update", message).Wait(); + client.SendAsync("Update", message).Wait(); + return; } + hubContext.Clients.All.SendAsync("Update", message).Wait(); } + } - /// - /// post a summary 'add' message to the client - /// - public void PostSummary(SyncProgressSummary summary) - { - this.SendMessage(summary); - } + /// + /// post a summary 'add' message to the client + /// + public void PostSummary(SyncProgressSummary summary) + { + this.SendMessage(summary); + } - /// - /// post a progress 'update' message to the client - /// - public void PostUpdate(string message, int count, int total) + /// + /// post a progress 'update' message to the client + /// + public void PostUpdate(string message, int count, int total) + { + this.SendUpdate(new uSyncUpdateMessage() { - this.SendUpdate(new uSyncUpdateMessage() - { - Message = message, - Count = count, - Total = total - }); - } - - /// - /// get the uSync callbacks for this connection - /// - /// - public uSyncCallbacks Callbacks() => - new uSyncCallbacks(this.PostSummary, this.PostUpdate); + Message = message, + Count = count, + Total = total + }); } + + /// + /// get the uSync callbacks for this connection + /// + /// + public uSyncCallbacks Callbacks() => + new uSyncCallbacks(this.PostSummary, this.PostUpdate); } diff --git a/uSync.BackOffice/Hubs/SyncHub.cs b/uSync.BackOffice/Hubs/SyncHub.cs index bf3adc9e..5d13e879 100644 --- a/uSync.BackOffice/Hubs/SyncHub.cs +++ b/uSync.BackOffice/Hubs/SyncHub.cs @@ -4,33 +4,32 @@ using Microsoft.AspNetCore.SignalR; -namespace uSync.BackOffice.Hubs +namespace uSync.BackOffice.Hubs; + +/// +/// SignalR Hub +/// +public class SyncHub : Hub { /// - /// SignalR Hub + /// Get the current time /// - public class SyncHub : Hub - { - /// - /// Get the current time - /// - /// - /// Used to give the hub a purpose - not called - /// - public string GetTime() - => DateTime.Now.ToString(CultureInfo.InvariantCulture); - } + /// + /// Used to give the hub a purpose - not called + /// + public string GetTime() + => DateTime.Now.ToString(CultureInfo.InvariantCulture); +} +/// +/// Iterface for the ISyncHub +/// +public interface ISyncHub +{ /// - /// Iterface for the ISyncHub + /// refresh the hub /// - public interface ISyncHub - { - /// - /// refresh the hub - /// - /// - /// - Task refreshed(int id); - } + /// + /// + Task refreshed(int id); } diff --git a/uSync.BackOffice/Hubs/uSyncCallbacks.cs b/uSync.BackOffice/Hubs/uSyncCallbacks.cs index b4a964d9..0be86412 100644 --- a/uSync.BackOffice/Hubs/uSyncCallbacks.cs +++ b/uSync.BackOffice/Hubs/uSyncCallbacks.cs @@ -2,30 +2,29 @@ using static uSync.BackOffice.uSyncService; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Callback objects used to communicate via SignalR +/// +public class uSyncCallbacks { /// - /// Callback objects used to communicate via SignalR + /// Add event callback /// - public class uSyncCallbacks - { - /// - /// Add event callback - /// - public SyncEventCallback Callback { get; private set; } + public SyncEventCallback? Callback { get; private set; } - /// - /// Update event callback - /// - public SyncUpdateCallback Update { get; private set; } + /// + /// Update event callback + /// + public SyncUpdateCallback? Update { get; private set; } - /// - /// generate a new callback object - /// - public uSyncCallbacks(SyncEventCallback callback, SyncUpdateCallback update) - { - this.Callback = callback; - this.Update = update; - } + /// + /// generate a new callback object + /// + public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update) + { + this.Callback = callback; + this.Update = update; } } diff --git a/uSync.BackOffice/Hubs/uSyncHubRoutes.cs b/uSync.BackOffice/Hubs/uSyncHubRoutes.cs index b68aa5a8..463faaae 100644 --- a/uSync.BackOffice/Hubs/uSyncHubRoutes.cs +++ b/uSync.BackOffice/Hubs/uSyncHubRoutes.cs @@ -12,59 +12,58 @@ using uSync.BackOffice.Configuration; -namespace uSync.BackOffice.Hubs +namespace uSync.BackOffice.Hubs; + +/// +/// Handles SignalR routes for uSync +/// +public class uSyncHubRoutes : IAreaRoutes { + private readonly IRuntimeState _runtimeState; + private readonly string _umbracoPathSegment; + /// - /// Handles SignalR routes for uSync + /// Constructor (called via DI) /// - public class uSyncHubRoutes : IAreaRoutes + public uSyncHubRoutes( + IOptions uSyncSettings, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState) { - private readonly IRuntimeState _runtimeState; - private readonly string _umbracoPathSegment; - - /// - /// Constructor (called via DI) - /// - public uSyncHubRoutes( - IOptions uSyncSettings, - IOptions globalSettings, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState) - { - _runtimeState = runtimeState; + _runtimeState = runtimeState; #pragma warning disable CS0618 // Type or member is obsolete - if (!string.IsNullOrWhiteSpace(uSyncSettings.Value?.SignalRRoot)) - { - _umbracoPathSegment = uSyncSettings.Value.SignalRRoot; - } - else - { - _umbracoPathSegment = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); - } -#pragma warning restore CS0618 // Type or member is obsolete + if (!string.IsNullOrWhiteSpace(uSyncSettings.Value?.SignalRRoot)) + { + _umbracoPathSegment = uSyncSettings.Value.SignalRRoot; + } + else + { + _umbracoPathSegment = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); } +#pragma warning restore CS0618 // Type or member is obsolete + } - /// - /// Create the signalR routes for uSync - /// - public void CreateRoutes(IEndpointRouteBuilder endpoints) + /// + /// Create the signalR routes for uSync + /// + public void CreateRoutes(IEndpointRouteBuilder endpoints) + { + switch (_runtimeState.Level) { - switch (_runtimeState.Level) - { - case RuntimeLevel.Install: - case RuntimeLevel.Upgrade: - case RuntimeLevel.Run: - endpoints.MapHub(GetuSyncHubRoute()); - break; + case RuntimeLevel.Install: + case RuntimeLevel.Upgrade: + case RuntimeLevel.Run: + endpoints.MapHub(GetuSyncHubRoute()); + break; - } } - - /// - /// Get the path to the uSync SignalR route - /// - public string GetuSyncHubRoute() - => $"/{_umbracoPathSegment}/{nameof(SyncHub)}"; } + + /// + /// Get the path to the uSync SignalR route + /// + public string GetuSyncHubRoute() + => $"/{_umbracoPathSegment}/{nameof(SyncHub)}"; } diff --git a/uSync.BackOffice/Hubs/uSyncUpdateMessage.cs b/uSync.BackOffice/Hubs/uSyncUpdateMessage.cs index c7cfea81..0420f918 100644 --- a/uSync.BackOffice/Hubs/uSyncUpdateMessage.cs +++ b/uSync.BackOffice/Hubs/uSyncUpdateMessage.cs @@ -1,23 +1,22 @@ -namespace uSync.BackOffice.Hubs +namespace uSync.BackOffice.Hubs; + +/// +/// update message sent via uSync to client +/// +public class uSyncUpdateMessage { /// - /// update message sent via uSync to client + /// string message to display /// - public class uSyncUpdateMessage - { - /// - /// string message to display - /// - public string Message { get; set; } + public string Message { get; set; } = string.Empty; - /// - /// nubmer of items processed - /// - public int Count { get; set; } + /// + /// number of items processed + /// + public int Count { get; set; } - /// - /// total number of items we expect to process - /// - public int Total { get; set; } - } + /// + /// total number of items we expect to process + /// + public int Total { get; set; } } diff --git a/uSync.BackOffice/Legacy/ISyncLegacyService.cs b/uSync.BackOffice/Legacy/ISyncLegacyService.cs new file mode 100644 index 00000000..b2eb7e06 --- /dev/null +++ b/uSync.BackOffice/Legacy/ISyncLegacyService.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace uSync.BackOffice.Legacy; + +/// +/// legacy usync file detection +/// +public interface ISyncLegacyService +{ + /// + /// find any legacy data types in the uSync folder + /// + List FindLegacyDataTypes(string folder); + + /// + /// find the latest uSync legacy folder. + /// + bool TryGetLatestLegacyFolder([MaybeNullWhen(false)] out string? folder); +} \ No newline at end of file diff --git a/uSync.BackOffice/Legacy/SyncLegacyService.cs b/uSync.BackOffice/Legacy/SyncLegacyService.cs new file mode 100644 index 00000000..287dd4f6 --- /dev/null +++ b/uSync.BackOffice/Legacy/SyncLegacyService.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +using uSync.BackOffice.Services; +using uSync.Core; + +namespace uSync.BackOffice.Legacy; + +/// +/// checks for legacy datatypes,and helps convert them. +/// +internal class SyncLegacyService : ISyncLegacyService +{ + private static string[] _legacyFolders = [ + "~/uSync/v13", + "~/uSync/v12", + "~/uSync/v11", + "~/uSync/v10", + "~/uSync/v9" + ]; + + private static Dictionary _legacyTypes = new(StringComparer.OrdinalIgnoreCase) + { + { SyncLegacyTypes.NestedContent, "Nested Content" }, + { SyncLegacyTypes.OurNestedContent, "Nested Content (Community version)" }, + { SyncLegacyTypes.Grid, "Grid" }, + { SyncLegacyTypes.MediaPicker, "Media Picker" } + }; + + private readonly SyncFileService _syncFileService; + + public SyncLegacyService(SyncFileService syncFileService) + { + _syncFileService = syncFileService; + } + + + public bool TryGetLatestLegacyFolder([MaybeNullWhen(false)] out string? folder) + { + folder = null; + + foreach (var legacyFolder in _legacyFolders) + { + if (_syncFileService.DirectoryExists(legacyFolder)) + { + folder = legacyFolder; + return true; + } + } + + return false; + } + + public List FindLegacyDataTypes(string folder) + { + + var dataTypesFolder = Path.Combine(folder, "DataTypes"); + if (_syncFileService.DirectoryExists(dataTypesFolder) is false) + return []; + + var discoveredLegacyTypes = new List(); + + foreach (var file in _syncFileService.GetFiles(dataTypesFolder, "*.config")) + { + var node = _syncFileService.LoadXElement(file); + if (node.Name.LocalName.Equals(Core.uSyncConstants.Serialization.DataType) is false) + continue; + + var type = node.Element(Core.uSyncConstants.Xml.Info) + ?.Element("EditorAlias").ValueOrDefault(string.Empty); + if (string.IsNullOrEmpty(type)) continue; + + if (_legacyTypes.ContainsKey(type)) + { + discoveredLegacyTypes.Add(_legacyTypes[type]); + } + } + + return discoveredLegacyTypes; + } +} diff --git a/uSync.BackOffice/Models/AddOnInfo.cs b/uSync.BackOffice/Models/AddOnInfo.cs index 45c9c89e..57c0384d 100644 --- a/uSync.BackOffice/Models/AddOnInfo.cs +++ b/uSync.BackOffice/Models/AddOnInfo.cs @@ -1,25 +1,24 @@ using System.Collections.Generic; -namespace uSync.BackOffice.Models +namespace uSync.BackOffice.Models; + +/// +/// Information about uSync AddOns (displayed in version string) +/// +public class AddOnInfo { /// - /// Information about uSync AddOns (displayed in version string) + /// Version of uSync we are running /// - public class AddOnInfo - { - /// - /// Version of uSync we are running - /// - public string Version { get; set; } + public string? Version { get; set; } - /// - /// Display string for all the add ons installed - /// - public string AddOnString { get; set; } + /// + /// Display string for all the add ons installed + /// + public string? AddOnString { get; set; } - /// - /// List of all the uSync AddOns installed - /// - public List AddOns { get; set; } = new List(); - } + /// + /// List of all the uSync AddOns installed + /// + public List AddOns { get; set; } = []; } diff --git a/uSync.BackOffice/Models/ISyncAddOn.cs b/uSync.BackOffice/Models/ISyncAddOn.cs index d8a5e705..07c81daa 100644 --- a/uSync.BackOffice/Models/ISyncAddOn.cs +++ b/uSync.BackOffice/Models/ISyncAddOn.cs @@ -1,45 +1,44 @@ -namespace uSync.BackOffice.Models +namespace uSync.BackOffice.Models; + +/// +/// An add on to uSync, which allows you to inject a view onto the uSync page +/// just like a content app. +/// +public interface ISyncAddOn { /// - /// An add on to uSync, which allows you to inject a view onto the uSync page - /// just like a content app. + /// Name used listing what is installed + /// + string Name { get; } + + + /// + /// your own version + /// + string Version { get; } + + /// + /// icon to use for app + /// + string Icon { get; } + + /// + /// view (if this is blank, the add on will not show in the top right) + /// + string View { get; } + + /// + /// alias for the 'app' + /// + string Alias { get; } + + /// + /// name shown under the icon. + /// + string DisplayName { get; } + + /// + /// sort order - the lower the further up the list you will be. /// - public interface ISyncAddOn - { - /// - /// Name used listing what is installed - /// - string Name { get; } - - - /// - /// your own version - /// - string Version { get; } - - /// - /// icon to use for app - /// - string Icon { get; } - - /// - /// view (if this is blank, the add on will not show in the top right) - /// - string View { get; } - - /// - /// alias for the 'app' - /// - string Alias { get; } - - /// - /// name shown under the icon. - /// - string DisplayName { get; } - - /// - /// sort order - the lower the further up the list you will be. - /// - int SortOrder { get; } - } + int SortOrder { get; } } diff --git a/uSync.BackOffice/Models/OrderedNodeInfo.cs b/uSync.BackOffice/Models/OrderedNodeInfo.cs index 23018968..4da2284f 100644 --- a/uSync.BackOffice/Models/OrderedNodeInfo.cs +++ b/uSync.BackOffice/Models/OrderedNodeInfo.cs @@ -5,96 +5,90 @@ using uSync.Core; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// object representing a file and its level +/// +public class OrderedNodeInfo { /// - /// object representing a file and its level + /// construct an OrderedNode /// - public class OrderedNodeInfo + public OrderedNodeInfo(string filename, XElement node) { - /// - /// construct an OrderedNode - /// - public OrderedNodeInfo() { } - - /// - /// construct an OrderedNode - /// - public OrderedNodeInfo(string filename, XElement node) - { - FileName = filename; - Node = node; - Key = $"{node.Name.LocalName}_{node.GetKey()}".ToGuid(); - Alias = node.GetAlias(); - } - - /// - /// set all the values of an ordered node. - /// - public OrderedNodeInfo(string filename, XElement node, int level, string path, bool isRoot) - : this(filename, node) - { - Level = level; - Path = path; - IsRoot = isRoot; - } - - /// - /// the key for the item. - /// - public Guid Key - { - get; - [Obsolete("Setter will be removed v15")] - set; - } + FileName = filename; + Node = node; + Key = $"{node.Name.LocalName}_{node.GetKey()}".ToGuid(); + Alias = node.GetAlias(); + Path = string.Empty; + } - /// - /// umbraco alias of the item - /// - public string Alias { get; } + /// + /// set all the values of an ordered node. + /// + public OrderedNodeInfo(string filename, XElement node, int level, string path, bool isRoot) + : this(filename, node) + { + Level = level; + Path = path; + IsRoot = isRoot; + } - /// - /// relative path of the item (so same in all 'folders') - /// - public string Path { get; } + /// + /// the key for the item. + /// + public Guid Key + { + get; + [Obsolete("Setter will be removed v15")] + set; + } - /// - /// level (e.g 0 is root) of file - /// - public int Level { get; } + /// + /// umbraco alias of the item + /// + public string Alias { get; } - /// - /// path to the actual file. - /// - public string FileName - { - get; - [Obsolete("Setter will be removed v15")] - set; - } + /// + /// relative path of the item (so same in all 'folders') + /// + public string Path { get; } - /// - /// the xml for this item. - /// - public XElement Node - { - get; - [Obsolete("Setter will be private from v15")] - set; - } + /// + /// level (e.g 0 is root) of file + /// + public int Level { get; } - /// - /// overwrites the node value for this ordered node element. - /// - /// - public void SetNode(XElement node) - => Node = node; + /// + /// path to the actual file. + /// + public string FileName + { + get; + [Obsolete("Setter will be removed v15")] + set; + } - /// - /// is this element from a root folder ? - /// - public bool IsRoot { get; } + /// + /// the xml for this item. + /// + public XElement Node + { + get; + [Obsolete("Setter will be private from v15")] + set; } + /// + /// overwrites the node value for this ordered node element. + /// + /// + public void SetNode(XElement node) + => Node = node; + + /// + /// is this element from a root folder ? + /// + public bool IsRoot { get; } } diff --git a/uSync.BackOffice/Models/SyncActionOptions.cs b/uSync.BackOffice/Models/SyncActionOptions.cs index dd96512f..4a8e3c7f 100644 --- a/uSync.BackOffice/Models/SyncActionOptions.cs +++ b/uSync.BackOffice/Models/SyncActionOptions.cs @@ -1,48 +1,47 @@ using System; using System.Collections.Generic; -namespace uSync.BackOffice.Models +namespace uSync.BackOffice.Models; + +/// +/// Options to tell uSync how to process an action +/// +public class SyncActionOptions { /// - /// Options to tell uSync how to process an action + /// SignalR client id + /// + public string? ClientId { get; set; } + + /// + /// Name of the handler to use for the action + /// + public string? Handler { get; set; } + + /// + /// Should the action be forced + /// + public bool Force { get; set; } + + /// + /// Set to use when processing the action /// - public class SyncActionOptions - { - /// - /// SignalR client id - /// - public string ClientId { get; set; } - - /// - /// Name of the handler to use for the action - /// - public string Handler { get; set; } - - /// - /// Should the action be forced - /// - public bool Force { get; set; } - - /// - /// Set to use when processing the action - /// - public string Set { get; set; } - - /// - /// SyncActions to use as the source for all individual actions - /// - public IEnumerable Actions { get; set; } - - /// - /// the folder (has to be in the uSync folder) you want to import - /// - [Obsolete("Pass array of folders for merging, will be removed in v15")] - public string Folder { get; set; } - - /// - /// array of usync folders you want to import - files will be merged as part of the process. - /// - public string[] Folders { get; set; } - - } + public string? Set { get; set; } + + /// + /// SyncActions to use as the source for all individual actions + /// + public IEnumerable Actions { get; set; } = []; + + /// + /// the folder (has to be in the uSync folder) you want to import + /// + [Obsolete("Pass array of folders for merging, will be removed in v15")] + public string? Folder { get; set; } + + /// + /// array of usync folders you want to import - files will be merged as part of the process. + /// + public string[] Folders { get; set; } = []; + } diff --git a/uSync.BackOffice/Models/SyncActionResult.cs b/uSync.BackOffice/Models/SyncActionResult.cs index aedd5aa0..2b4e3108 100644 --- a/uSync.BackOffice/Models/SyncActionResult.cs +++ b/uSync.BackOffice/Models/SyncActionResult.cs @@ -1,29 +1,28 @@ using System.Collections.Generic; -namespace uSync.BackOffice.Models +namespace uSync.BackOffice.Models; + +/// +/// Result of a series of actions performed by a controller +/// +public class SyncActionResult { /// - /// Result of a series of actions performed by a controller + /// Construct a new SyncActionResult object /// - public class SyncActionResult - { - /// - /// Construct a new SyncActionResult object - /// - public SyncActionResult() { } - - /// - /// Construct a new SyncActionResult object - /// - /// list of actions to include - public SyncActionResult(IEnumerable actions) - { - this.Actions = actions; - } + public SyncActionResult() { } - /// - /// List of actions performed by process - /// - public IEnumerable Actions { get; set; } + /// + /// Construct a new SyncActionResult object + /// + /// list of actions to include + public SyncActionResult(IEnumerable actions) + { + this.Actions = actions; } + + /// + /// List of actions performed by process + /// + public IEnumerable Actions { get; set; } = []; } diff --git a/uSync.BackOffice/Models/SyncHandlerView.cs b/uSync.BackOffice/Models/SyncHandlerView.cs index da6da546..b0a534b8 100644 --- a/uSync.BackOffice/Models/SyncHandlerView.cs +++ b/uSync.BackOffice/Models/SyncHandlerView.cs @@ -1,43 +1,42 @@ -namespace uSync.BackOffice.Models +namespace uSync.BackOffice.Models; + +/// +/// view model of a handler, sent to the UI to draw the handler boxes. +/// +public class SyncHandlerView { /// - /// view model of a handler, sent to the UI to draw the handler boxes. + /// Is Handler enabled /// - public class SyncHandlerView - { - /// - /// Is Handler enabled - /// - public bool Enabled { get; set; } + public bool Enabled { get; set; } - /// - /// current status of this handler - /// - public int Status { get; set; } + /// + /// current status of this handler + /// + public int Status { get; set; } - /// - /// Display name of the handler - /// - public string Name { get; set; } - - /// - /// Alias for the handler - /// - public string Alias { get; set; } + /// + /// Display name of the handler + /// + public required string Name { get; set; } + + /// + /// Alias for the handler + /// + public required string Alias { get; set; } - /// - /// Icon to show for handler - /// - public string Icon { get; set; } + /// + /// Icon to show for handler + /// + public string Icon { get; set; } = "icon-bug"; - /// - /// Group handler belongs too - /// - public string Group { get; set; } + /// + /// Group handler belongs too + /// + public string Group { get; set; } = string.Empty; - /// - /// Set handler is currently in - /// - public string Set { get; set; } - } + /// + /// Set handler is currently in + /// + public string Set { get; set; } = string.Empty; } diff --git a/uSync.BackOffice/Models/SyncProgressModels.cs b/uSync.BackOffice/Models/SyncProgressModels.cs index b6bbb2af..7880fce5 100644 --- a/uSync.BackOffice/Models/SyncProgressModels.cs +++ b/uSync.BackOffice/Models/SyncProgressModels.cs @@ -3,188 +3,187 @@ using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice.Models +namespace uSync.BackOffice.Models; + +/// +/// Progress summary - object that tells the UI to draw the handler icons while uSync works. +/// +public class SyncProgressSummary { /// - /// Progress summary - object that tells the UI to draw the handler icons while uSync works. + /// current count (progress) of where we are upto. /// - public class SyncProgressSummary - { - /// - /// current count (progress) of where we are upto. - /// - public int Count { get; set; } - - /// - /// How many steps we think we are going to take. - /// - public int Total { get; set; } - - /// - /// Message to display to user. - /// - public string Message { get; set; } - - /// - /// Summary (icons, state) of the handlers - /// - public List Handlers { get; set; } - - private SyncProgressSummary(string message, int totalSteps) - { - this.Message = message; - this.Total = totalSteps; - } + public int Count { get; set; } - /// - /// Create a new SyncProcessSummary object - /// - public SyncProgressSummary(IEnumerable summaries, - string message, int totalSteps) - : this(message, totalSteps) - { - this.Handlers = summaries.ToList(); - } + /// + /// How many steps we think we are going to take. + /// + public int Total { get; set; } - /// - /// Create a new SyncProcessSummary object - /// - public SyncProgressSummary( - IEnumerable handlers, - string message, - int totalSteps) - : this(message, totalSteps) - { - if (handlers != null) - { - this.Handlers = handlers.Select(x => new SyncHandlerSummary() - { - Icon = x.Icon, - Name = x.Name, - Status = HandlerStatus.Pending - }).ToList(); - } - else - { - this.Handlers = new List(); - } - } + /// + /// Message to display to user. + /// + public string Message { get; set; } - /// - /// Updated the change status of a single handler in the list - /// - /// Name of handler - /// current handler status - /// number of changes - public void UpdateHandler(string name, HandlerStatus status, int changeCount) - { - UpdateHandler(name, status, changeCount, false); - } + /// + /// Summary (icons, state) of the handlers + /// + public List Handlers { get; set; } = []; + + private SyncProgressSummary(string message, int totalSteps) + { + this.Message = message; + this.Total = totalSteps; + } + + /// + /// Create a new SyncProcessSummary object + /// + public SyncProgressSummary(IEnumerable summaries, + string message, int totalSteps) + : this(message, totalSteps) + { + this.Handlers = summaries.ToList(); + } - /// - /// Updated the change status of a single handler in the list - /// - /// Name of handler - /// current handler status - /// number of changes - /// there are actions that have failed in the set - public void UpdateHandler(string name, HandlerStatus status, int changeCount, bool hasErrors) + /// + /// Create a new SyncProcessSummary object + /// + public SyncProgressSummary( + IEnumerable handlers, + string message, + int totalSteps) + : this(message, totalSteps) + { + if (handlers != null) { - var item = this.Handlers.FirstOrDefault(x => x.Name == name); - if (item != null) + this.Handlers = handlers.Select(x => new SyncHandlerSummary() { - item.Status = status; - item.Changes = changeCount; - item.InError = hasErrors; - } + Icon = x.Icon, + Name = x.Name, + Status = HandlerStatus.Pending + }).ToList(); } - - /// - /// Updated the change status of a single handler in the list - /// - /// Name of handler - /// current handler status - /// Update the main progress message for the UI - /// number of changes - public void UpdateHandler(string name, HandlerStatus status, string message, int changeCount) + else { - UpdateHandler(name, status, changeCount); - UpdateMessage(message); + this.Handlers = new List(); } + } - /// - /// update the progress message in the object - /// - /// - public void UpdateMessage(string message) - { - this.Message = message; - } + /// + /// Updated the change status of a single handler in the list + /// + /// Name of handler + /// current handler status + /// number of changes + public void UpdateHandler(string name, HandlerStatus status, int changeCount) + { + UpdateHandler(name, status, changeCount, false); + } - /// - /// increment the item count in the progress message - /// - public void Increment() + /// + /// Updated the change status of a single handler in the list + /// + /// Name of handler + /// current handler status + /// number of changes + /// there are actions that have failed in the set + public void UpdateHandler(string name, HandlerStatus status, int changeCount, bool hasErrors) + { + var item = this.Handlers.FirstOrDefault(x => x.Name == name); + if (item != null) { - this.Count++; + item.Status = status; + item.Changes = changeCount; + item.InError = hasErrors; } + } + /// + /// Updated the change status of a single handler in the list + /// + /// Name of handler + /// current handler status + /// Update the main progress message for the UI + /// number of changes + public void UpdateHandler(string name, HandlerStatus status, string message, int changeCount) + { + UpdateHandler(name, status, changeCount); + UpdateMessage(message); } /// - /// Summary object used to display a summary of progress via the UI + /// update the progress message in the object /// - public class SyncHandlerSummary + /// + public void UpdateMessage(string message) { - /// - /// The icon the user sees for this handler. - /// - public string Icon { get; set; } - - /// - /// Name shown under the handler - /// - public string Name { get; set; } - - /// - /// Current status of the handler - /// - public HandlerStatus Status { get; set; } - - /// - /// number of changes that have been processed - /// - public int Changes { get; set; } - - /// - /// reports if the handler has errors - /// - public bool InError { get; set; } + this.Message = message; } /// - /// current status of a handler. + /// increment the item count in the progress message /// - public enum HandlerStatus + public void Increment() { - /// - /// Pending - handler has yet to start work - /// - Pending, - - /// - /// Processing - handler is doing stuff now - /// - Processing, - - /// - /// Complete - handler has completed its work - /// - Complete, - - /// - /// error - the handler encountered one or more errors - /// - Error + this.Count++; } + +} + +/// +/// Summary object used to display a summary of progress via the UI +/// +public class SyncHandlerSummary +{ + /// + /// The icon the user sees for this handler. + /// + public required string Icon { get; set; } + + /// + /// Name shown under the handler + /// + public required string Name { get; set; } + + /// + /// Current status of the handler + /// + public HandlerStatus Status { get; set; } + + /// + /// number of changes that have been processed + /// + public int Changes { get; set; } + + /// + /// reports if the handler has errors + /// + public bool InError { get; set; } +} + +/// +/// current status of a handler. +/// +public enum HandlerStatus +{ + /// + /// Pending - handler has yet to start work + /// + Pending, + + /// + /// Processing - handler is doing stuff now + /// + Processing, + + /// + /// Complete - handler has completed its work + /// + Complete, + + /// + /// error - the handler encountered one or more errors + /// + Error } diff --git a/uSync.BackOffice/Models/uSyncImportOptions.cs b/uSync.BackOffice/Models/uSyncImportOptions.cs index bff3986e..00f17fb1 100644 --- a/uSync.BackOffice/Models/uSyncImportOptions.cs +++ b/uSync.BackOffice/Models/uSyncImportOptions.cs @@ -3,96 +3,95 @@ using uSync.Core.Serialization; -namespace uSync.BackOffice.Models +namespace uSync.BackOffice.Models; + +/// +/// options passed to an import, report or export of an item. +/// +public class uSyncImportOptions +{ + /// + /// Unique ID for this series of import operations + /// + public Guid ImportId { get; set; } + + /// + /// Handler set to load for operation + /// + public string HandlerSet { get; set; } = string.Empty; + + /// + /// Flags to pass to the serializers + /// + public SerializerFlags Flags { get; set; } + + /// + /// Additional settings on the handlers/serializers for this operation + /// + public Dictionary Settings { get; set; } = []; + + /// + /// SignalR callbacks to use for UI communication + /// + public uSyncCallbacks? Callbacks { get; set; } + + /// + /// Root folder for all uSync operations + /// + [Obsolete("Pass array of folders, will be removed in v15")] + public string RootFolder { get; set; } = string.Empty; + + /// + /// collection of root folders, that are merged for the action + /// + public string[] Folders { get; set; } = []; + + /// + /// (reserved for future use) + /// + public bool EnableRollback { get; set; } + + + /// + /// should we pause the uSync export events during the import ? + /// + public bool PauseDuringImport { get; set; } = true; + + /// + /// the user doing the import. + /// + public int UserId { get; set; } = -1; +} + +/// +/// Import options when paging any import operations +/// +public class uSyncPagedImportOptions : uSyncImportOptions { /// - /// options passed to an import, report or export of an item. + /// Page number + /// + public int PageNumber { get; set; } + + /// + /// Page size - number of items to process per page /// - public class uSyncImportOptions - { - /// - /// Unique ID for this series of import operations - /// - public Guid ImportId { get; set; } - - /// - /// Handler set to load for operation - /// - public string HandlerSet { get; set; } - - /// - /// Flags to pass to the serializers - /// - public SerializerFlags Flags { get; set; } - - /// - /// Additional settings on the handlers/serializers for this operation - /// - public Dictionary Settings { get; set; } - - /// - /// SignalR callbacks to use for UI communication - /// - public uSyncCallbacks Callbacks { get; set; } - - /// - /// Root folder for all uSync operations - /// - [Obsolete("Pass array of folders, will be removed in v15")] - public string RootFolder { get; set; } - - /// - /// collection of root folders, that are merged for the action - /// - public string[] Folders { get; set; } - - /// - /// (reserved for future use) - /// - public bool EnableRollback { get; set; } - - - /// - /// should we pause the uSync export events during the import ? - /// - public bool PauseDuringImport { get; set; } = true; - - /// - /// the user doing the import. - /// - public int UserId { get; set; } = -1; - } + public int PageSize { get; set; } /// - /// Import options when paging any import operations + /// progress bar lower bound value /// - public class uSyncPagedImportOptions : uSyncImportOptions - { - /// - /// Page number - /// - public int PageNumber { get; set; } - - /// - /// Page size - number of items to process per page - /// - public int PageSize { get; set; } - - /// - /// progress bar lower bound value - /// - public int ProgressMin { get; set; } - - /// - /// progress bar upper bound value - /// - public int ProgressMax { get; set; } - - /// - /// should include what is a normally disabled handler when looking for - /// something to process the folder. - /// - public bool IncludeDisabledHandlers { get; set; } - - } + public int ProgressMin { get; set; } + + /// + /// progress bar upper bound value + /// + public int ProgressMax { get; set; } + + /// + /// should include what is a normally disabled handler when looking for + /// something to process the folder. + /// + public bool IncludeDisabledHandlers { get; set; } + } diff --git a/uSync.BackOffice/Models/uSyncOptions.cs b/uSync.BackOffice/Models/uSyncOptions.cs index f457c1c8..b622bddd 100644 --- a/uSync.BackOffice/Models/uSyncOptions.cs +++ b/uSync.BackOffice/Models/uSyncOptions.cs @@ -1,40 +1,39 @@ using System.Runtime.Serialization; -namespace uSync.BackOffice.Models +namespace uSync.BackOffice.Models; + +/// +/// Options passed to Import/Export methods by JS calls +/// +public class uSyncOptions { /// - /// Options passed to Import/Export methods by JS calls + /// SignalR Hub client id /// - public class uSyncOptions - { - /// - /// SignalR Hub client id - /// - [DataMember(Name = "clientId")] - public string ClientId { get; set; } + [DataMember(Name = "clientId")] + public string ClientId { get; set; } = string.Empty; - /// - /// Force the import (even if no changes detected) - /// - [DataMember(Name = "force")] - public bool Force { get; set; } + /// + /// Force the import (even if no changes detected) + /// + [DataMember(Name = "force")] + public bool Force { get; set; } - /// - /// Make the export clean the folder before it starts - /// - [DataMember(Name = "clean")] - public bool Clean { get; set; } + /// + /// Make the export clean the folder before it starts + /// + [DataMember(Name = "clean")] + public bool Clean { get; set; } - /// - /// Name of the handler group to perfom the actions on - /// - [DataMember(Name = "group")] - public string Group { get; set; } + /// + /// Name of the handler group to perfom the actions on + /// + [DataMember(Name = "group")] + public string Group { get; set; } = string.Empty; - /// - /// The set in which the handler lives - /// - [DataMember(Name = "set")] - public string Set { get; set; } - } + /// + /// The set in which the handler lives + /// + [DataMember(Name = "set")] + public string Set { get; set; } = string.Empty; } diff --git a/uSync.BackOffice/Notifications/SyncScopedNotificationPublisher.cs b/uSync.BackOffice/Notifications/SyncScopedNotificationPublisher.cs index 5fcc6f78..ec2e5d38 100644 --- a/uSync.BackOffice/Notifications/SyncScopedNotificationPublisher.cs +++ b/uSync.BackOffice/Notifications/SyncScopedNotificationPublisher.cs @@ -20,18 +20,18 @@ internal class SyncScopedNotificationPublisher { private readonly ILogger _logger; private readonly IEventAggregator _eventAggregator; - private readonly SyncUpdateCallback _updateCallback; + private readonly SyncUpdateCallback? _updateCallback; private readonly uSyncConfigService _uSyncConfig; private readonly uSyncEventService _uSyncEventService; - private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IBackgroundTaskQueue? _backgroundTaskQueue; public SyncScopedNotificationPublisher( IEventAggregator eventAggregator, ILogger logger, - SyncUpdateCallback callback, + SyncUpdateCallback? callback, uSyncConfigService uSyncConfig, - IBackgroundTaskQueue backgroundTaskQueue, + IBackgroundTaskQueue? backgroundTaskQueue, uSyncEventService uSyncEventService) : base(eventAggregator, false) { diff --git a/uSync.BackOffice/Notifications/uSyncApplicationStartingHandler.cs b/uSync.BackOffice/Notifications/uSyncApplicationStartingHandler.cs index 51ca7669..a82cc064 100644 --- a/uSync.BackOffice/Notifications/uSyncApplicationStartingHandler.cs +++ b/uSync.BackOffice/Notifications/uSyncApplicationStartingHandler.cs @@ -15,175 +15,174 @@ using uSync.BackOffice.Services; using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice.Notifications +namespace uSync.BackOffice.Notifications; + +/// +/// Run uSync tasks when the site has started up. +/// +internal class uSyncApplicationStartingHandler : INotificationHandler { + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IServerRoleAccessor _serverRegistrar; + private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly uSyncConfigService _uSyncConfig; + private readonly SyncFileService _syncFileService; + private readonly uSyncService _uSyncService; + /// - /// Run uSync tasks when the site has started up. + /// Generate a new uSyncApplicationStartingHandler object /// - internal class uSyncApplicationStartingHandler : INotificationHandler + public uSyncApplicationStartingHandler( + ILogger logger, + IRuntimeState runtimeState, + IServerRoleAccessor serverRegistrar, + IUmbracoContextFactory umbracoContextFactory, + uSyncConfigService uSyncConfigService, + SyncFileService syncFileService, + uSyncService uSyncService) { - private readonly ILogger _logger; - private readonly IRuntimeState _runtimeState; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IUmbracoContextFactory _umbracoContextFactory; - private readonly uSyncConfigService _uSyncConfig; - private readonly SyncFileService _syncFileService; - private readonly uSyncService _uSyncService; - - /// - /// Generate a new uSyncApplicationStartingHandler object - /// - public uSyncApplicationStartingHandler( - ILogger logger, - IRuntimeState runtimeState, - IServerRoleAccessor serverRegistrar, - IUmbracoContextFactory umbracoContextFactory, - uSyncConfigService uSyncConfigService, - SyncFileService syncFileService, - uSyncService uSyncService) - { - _runtimeState = runtimeState; - _serverRegistrar = serverRegistrar; + _runtimeState = runtimeState; + _serverRegistrar = serverRegistrar; + + _umbracoContextFactory = umbracoContextFactory; - _umbracoContextFactory = umbracoContextFactory; + _logger = logger; - _logger = logger; + _uSyncConfig = uSyncConfigService; - _uSyncConfig = uSyncConfigService; + _syncFileService = syncFileService; + _uSyncService = uSyncService; + } - _syncFileService = syncFileService; - _uSyncService = uSyncService; + /// + /// Handle the application starting notification event. + /// + public void Handle(UmbracoApplicationStartedNotification notification) + { + // we only run uSync when the site is running, and we + // are not running on a replica. + if (_runtimeState.Level < RuntimeLevel.Run) + { + _logger.LogInformation("Umbraco is in {mode} mode, so uSync will not run this time.", _runtimeState.Level); + return; } - /// - /// Handle the application starting notification event. - /// - public void Handle(UmbracoApplicationStartedNotification notification) + if (_serverRegistrar.CurrentServerRole == ServerRole.Subscriber) { - // we only run uSync when the site is running, and we - // are not running on a replica. - if (_runtimeState.Level < RuntimeLevel.Run) - { - _logger.LogInformation("Umbraco is in {mode} mode, so uSync will not run this time.", _runtimeState.Level); - return; - } + _logger.LogInformation("This is a replicate server in a load balanced setup - uSync will not run {serverRole}", _serverRegistrar.CurrentServerRole); + return; + } - if (_serverRegistrar.CurrentServerRole == ServerRole.Subscriber) - { - _logger.LogInformation("This is a replicate server in a load balanced setup - uSync will not run {serverRole}", _serverRegistrar.CurrentServerRole); - return; - } + InituSync(); + } - InituSync(); - } + /// + /// Initialize uSync elements (run start up import etc). + /// + private void InituSync() + { + var sw = Stopwatch.StartNew(); - /// - /// Initialize uSync elements (run start up import etc). - /// - private void InituSync() + try { - var sw = Stopwatch.StartNew(); - - try + using (var reference = _umbracoContextFactory.EnsureUmbracoContext()) { - using (var reference = _umbracoContextFactory.EnsureUmbracoContext()) + if (IsExportAtStartupEnabled() || (IsExportOnSaveOn() && !HasSyncFolders())) { - if (IsExportAtStartupEnabled() || (IsExportOnSaveOn() && !HasSyncFolders())) + + var options = new SyncHandlerOptions { + Group = _uSyncConfig.Settings.ExportOnSave + }; - var options = new SyncHandlerOptions - { - Group = _uSyncConfig.Settings.ExportOnSave - }; - - _logger.LogInformation("uSync: Running export at startup"); - _uSyncService.Export(_uSyncConfig.GetRootFolder(), options); - } + _logger.LogInformation("uSync: Running export at startup"); + _uSyncService.Export(_uSyncConfig.GetRootFolder(), options); + } - if (IsImportAtStartupEnabled()) - { - _logger.LogInformation("uSync: Running Import at startup {group}", _uSyncConfig.Settings.ImportAtStartup); + if (IsImportAtStartupEnabled()) + { + _logger.LogInformation("uSync: Running Import at startup {group}", _uSyncConfig.Settings.ImportAtStartup); - if (!HasStopFile(_uSyncConfig.GetRootFolder())) - { - _uSyncService.Import(_uSyncConfig.GetFolders(), false, new SyncHandlerOptions - { - Group = _uSyncConfig.Settings.ImportAtStartup - }); - - ProcessOnceFile(_uSyncConfig.GetRootFolder()); - } - else + if (!HasStopFile(_uSyncConfig.GetRootFolder())) + { + _uSyncService.Import(_uSyncConfig.GetFolders(), false, new SyncHandlerOptions { - _logger.LogInformation("Startup Import blocked by usync.stop file"); - } + Group = _uSyncConfig.Settings.ImportAtStartup + }); + + ProcessOnceFile(_uSyncConfig.GetRootFolder()); + } + else + { + _logger.LogInformation("Startup Import blocked by usync.stop file"); } } } - catch (Exception ex) - { - _logger.LogWarning(ex, "uSync: Error during startup {message}", ex.Message); - } - finally - { - sw.Stop(); - _logger.LogInformation("uSync: Startup Complete {elapsed}ms", sw.ElapsedMilliseconds); - } - } - - /// - /// checks if there are any of the sync folders (including root). - /// - /// - private bool HasSyncFolders() + catch (Exception ex) { - foreach (var folder in _uSyncConfig.GetFolders()) - { - if (_syncFileService.DirectoryExists(folder)) return true; - } + _logger.LogWarning(ex, "uSync: Error during startup {message}", ex.Message); + } + finally + { + sw.Stop(); + _logger.LogInformation("uSync: Startup Complete {elapsed}ms", sw.ElapsedMilliseconds); + } - return false; + } + + /// + /// checks if there are any of the sync folders (including root). + /// + /// + private bool HasSyncFolders() + { + foreach (var folder in _uSyncConfig.GetFolders()) + { + if (_syncFileService.DirectoryExists(folder)) return true; } + return false; + } + - /// - /// does the uSync folder contain a uSync.stop file (which would mean we would not process anything at startup) - /// - private bool HasStopFile(string folder) - => _syncFileService.FileExists($"{folder}/usync.stop"); + /// + /// does the uSync folder contain a uSync.stop file (which would mean we would not process anything at startup) + /// + private bool HasStopFile(string folder) + => _syncFileService.FileExists($"{folder}/usync.stop"); - /// - /// Process the once file (if it exists we rename it to usync.stop). - /// - private void ProcessOnceFile(string folder) + /// + /// Process the once file (if it exists we rename it to usync.stop). + /// + private void ProcessOnceFile(string folder) + { + if (_syncFileService.FileExists($"{folder}/usync.once")) { - if (_syncFileService.FileExists($"{folder}/usync.once")) - { - _syncFileService.DeleteFile($"{folder}/usync.once"); - _syncFileService.SaveFile($"{folder}/usync.stop", "uSync Stop file, prevents startup import"); - _logger.LogInformation("usync.once file replaced by usync.stop file"); - } + _syncFileService.DeleteFile($"{folder}/usync.once"); + _syncFileService.SaveFile($"{folder}/usync.stop", "uSync Stop file, prevents startup import"); + _logger.LogInformation("usync.once file replaced by usync.stop file"); } + } - /// - /// is the export on save feature on (not blank or none) - /// - private bool IsExportOnSaveOn() - => IsGroupSettingEnabled(_uSyncConfig.Settings.ExportOnSave); + /// + /// is the export on save feature on (not blank or none) + /// + private bool IsExportOnSaveOn() + => IsGroupSettingEnabled(_uSyncConfig.Settings.ExportOnSave); - private bool IsImportAtStartupEnabled() - => IsGroupSettingEnabled(_uSyncConfig.Settings.ImportAtStartup); + private bool IsImportAtStartupEnabled() + => IsGroupSettingEnabled(_uSyncConfig.Settings.ImportAtStartup); - private bool IsExportAtStartupEnabled() - => IsGroupSettingEnabled(_uSyncConfig.Settings.ExportAtStartup); + private bool IsExportAtStartupEnabled() + => IsGroupSettingEnabled(_uSyncConfig.Settings.ExportAtStartup); - private bool IsGroupSettingEnabled(string value) - => !string.IsNullOrWhiteSpace(value) - && !value.InvariantEquals("none") - && !value.InvariantEquals("off") - && !value.InvariantEquals("false"); + private bool IsGroupSettingEnabled(string value) + => !string.IsNullOrWhiteSpace(value) + && !value.InvariantEquals("none") + && !value.InvariantEquals("off") + && !value.InvariantEquals("false"); - } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/CancelableuSyncBulkNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/CancelableuSyncBulkNotification.cs index 2fea69f5..039d0c1a 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/CancelableuSyncBulkNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/CancelableuSyncBulkNotification.cs @@ -2,28 +2,25 @@ using Umbraco.Cms.Core.Notifications; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// uSync cancelable bulk notification event (fired for "starting" events) +/// +public class CancelableuSyncBulkNotification : uSyncBulkNotification, ICancelableNotification { /// - /// uSync cancelable bulk notification event (fired for "starting" events) + /// Notification constructor /// - public class CancelableuSyncBulkNotification : uSyncBulkNotification, ICancelableNotification - { - /// - /// Notification constructor - /// - public CancelableuSyncBulkNotification() - : base(Enumerable.Empty()) - { } - - /// - /// Cancel this process - /// - /// - /// if this value is set to true then uSync will cancel the process it is currently running - /// - public bool Cancel { get; set; } - } - + public CancelableuSyncBulkNotification() + : base(Enumerable.Empty()) + { } + /// + /// Cancel this process + /// + /// + /// if this value is set to true then uSync will cancel the process it is currently running + /// + public bool Cancel { get; set; } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/CancelableuSyncItemNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/CancelableuSyncItemNotification.cs index 7ccc9aad..2d986dd7 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/CancelableuSyncItemNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/CancelableuSyncItemNotification.cs @@ -2,32 +2,29 @@ using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Cancelable uSync event +/// +public class CancelableuSyncItemNotification : uSyncItemNotification, ICancelableNotification { /// - /// Cancelable uSync event + /// Construct a new cancelable event of type item /// - public class CancelableuSyncItemNotification : uSyncItemNotification, ICancelableNotification - { - /// - /// Construct a new cancelable event of type item - /// - public CancelableuSyncItemNotification(TObject item) - : base(item) - { } - - /// - /// Construct a new cancelable event of type item for a specific handler - /// - public CancelableuSyncItemNotification(TObject item, ISyncHandler handler) - : base(item, handler) - { } - - /// - /// Cancel the current process - /// - public bool Cancel { get; set; } - } + public CancelableuSyncItemNotification(TObject item) + : base(item) + { } + /// + /// Construct a new cancelable event of type item for a specific handler + /// + public CancelableuSyncItemNotification(TObject item, ISyncHandler handler) + : base(item, handler) + { } + /// + /// Cancel the current process + /// + public bool Cancel { get; set; } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncBulkNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncBulkNotification.cs index 0343c4de..813a9677 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncBulkNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncBulkNotification.cs @@ -2,27 +2,24 @@ using Umbraco.Cms.Core.Notifications; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Notifications of bulk (starting/completed) events +/// +public class uSyncBulkNotification : INotification { /// - /// Notifications of bulk (starting/completed) events + /// generate new BulkNotificationObject /// - public class uSyncBulkNotification : INotification + /// + public uSyncBulkNotification(IEnumerable actions) { - /// - /// generate new BulkNotificationObject - /// - /// - public uSyncBulkNotification(IEnumerable actions) - { - this.Actions = actions; - } - - /// - /// actions that have occured during the bulk operation - /// - public IEnumerable Actions { get; set; } + this.Actions = actions; } - + /// + /// actions that have occured during the bulk operation + /// + public IEnumerable Actions { get; set; } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportCompletedNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportCompletedNotification.cs index 1c41c412..c2d6b3c1 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportCompletedNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportCompletedNotification.cs @@ -1,16 +1,14 @@ using System.Collections.Generic; -namespace uSync.BackOffice -{ - /// - /// Notification object used when an bulk export has been completed - /// - public class uSyncExportCompletedNotification : uSyncBulkNotification { - - /// - public uSyncExportCompletedNotification(IEnumerable actions) - : base(actions) { } - } +namespace uSync.BackOffice; +/// +/// Notification object used when an bulk export has been completed +/// +public class uSyncExportCompletedNotification : uSyncBulkNotification +{ + /// + public uSyncExportCompletedNotification(IEnumerable actions) + : base(actions) { } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportStartingNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportStartingNotification.cs index 595a7044..34d9c96c 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportStartingNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportStartingNotification.cs @@ -1,9 +1,6 @@ -namespace uSync.BackOffice -{ - /// - /// bulk notification created when Export process is starting - /// - public class uSyncExportStartingNotification : CancelableuSyncBulkNotification { } +namespace uSync.BackOffice; - -} +/// +/// bulk notification created when Export process is starting +/// +public class uSyncExportStartingNotification : CancelableuSyncBulkNotification { } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportedItemNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportedItemNotification.cs index e2afde8e..93285071 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportedItemNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportedItemNotification.cs @@ -2,16 +2,14 @@ using uSync.Core; -namespace uSync.BackOffice -{ - /// - /// Notification object created after an item has been exported - /// - public class uSyncExportedItemNotification : uSyncItemNotification { - /// - public uSyncExportedItemNotification(XElement item, ChangeType change) - : base(item, change) { } - } - +namespace uSync.BackOffice; +/// +/// Notification object created after an item has been exported +/// +public class uSyncExportedItemNotification : uSyncItemNotification +{ + /// + public uSyncExportedItemNotification(XElement? item, ChangeType change) + : base(item, change) { } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportingItemNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportingItemNotification.cs index 045a5e58..5bba8570 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportingItemNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncExportingItemNotification.cs @@ -1,24 +1,21 @@ using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Cancelable notification called before an item is Exported +/// +public class uSyncExportingItemNotification : CancelableuSyncItemNotification { /// - /// Cancelable notification called before an item is Exported + /// generate a new uSyncExportingItemNotification object /// - public class uSyncExportingItemNotification : CancelableuSyncItemNotification - { - /// - /// generate a new uSyncExportingItemNotification object - /// - public uSyncExportingItemNotification(TObject item) - : base(item) { } - - /// - /// generate a new uSyncExportingItemNotification object - /// - public uSyncExportingItemNotification(TObject item, ISyncHandler syncHandler) - : base(item, syncHandler) { } - } - + public uSyncExportingItemNotification(TObject item) + : base(item) { } + /// + /// generate a new uSyncExportingItemNotification object + /// + public uSyncExportingItemNotification(TObject item, ISyncHandler syncHandler) + : base(item, syncHandler) { } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportCompletedNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportCompletedNotification.cs index 38cf865b..9adf64b1 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportCompletedNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportCompletedNotification.cs @@ -1,16 +1,13 @@ using System.Collections.Generic; -namespace uSync.BackOffice -{ - /// - /// bulk notification fired when import process is completed - /// - public class uSyncImportCompletedNotification : uSyncBulkNotification - { - /// - public uSyncImportCompletedNotification(IEnumerable actions) - : base(actions) { } - } - +namespace uSync.BackOffice; +/// +/// bulk notification fired when import process is completed +/// +public class uSyncImportCompletedNotification : uSyncBulkNotification +{ + /// + public uSyncImportCompletedNotification(IEnumerable actions) + : base(actions) { } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportStartingNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportStartingNotification.cs index 2d8cde9d..55a1462d 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportStartingNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportStartingNotification.cs @@ -1,7 +1,6 @@ -namespace uSync.BackOffice -{ - /// - /// Cancelable noification called before import starts - /// - public class uSyncImportStartingNotification : CancelableuSyncBulkNotification { } -} +namespace uSync.BackOffice; + +/// +/// Cancelable noification called before import starts +/// +public class uSyncImportStartingNotification : CancelableuSyncBulkNotification { } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportedItemNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportedItemNotification.cs index 03e8808c..cbced409 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportedItemNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportedItemNotification.cs @@ -2,17 +2,14 @@ using uSync.Core; -namespace uSync.BackOffice -{ - /// - /// Notification fired when a single item has been imported - /// - public class uSyncImportedItemNotification : uSyncItemNotification - { - /// - public uSyncImportedItemNotification(XElement item, ChangeType change) - : base(item, change) { } - } - +namespace uSync.BackOffice; +/// +/// Notification fired when a single item has been imported +/// +public class uSyncImportedItemNotification : uSyncItemNotification +{ + /// + public uSyncImportedItemNotification(XElement item, ChangeType change) + : base(item, change) { } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportingItemNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportingItemNotification.cs index b1dc193e..2f17d40d 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportingItemNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncImportingItemNotification.cs @@ -2,24 +2,23 @@ using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Cancelable nofiration called before an item is imported +/// +public class uSyncImportingItemNotification : CancelableuSyncItemNotification { /// - /// Cancelable nofiration called before an item is imported + /// generate a new uSyncImportingItemNotification object /// - public class uSyncImportingItemNotification : CancelableuSyncItemNotification - { - /// - /// generate a new uSyncImportingItemNotification object - /// - public uSyncImportingItemNotification(XElement item) - : base(item) { } + public uSyncImportingItemNotification(XElement item) + : base(item) { } - /// - /// generate a new uSyncImportingItemNotification object - /// - public uSyncImportingItemNotification(XElement item, ISyncHandler handler) - : base(item, handler) { } + /// + /// generate a new uSyncImportingItemNotification object + /// + public uSyncImportingItemNotification(XElement item, ISyncHandler handler) + : base(item, handler) { } - } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncItemNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncItemNotification.cs index 2b4be50d..f5ab5d49 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncItemNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncItemNotification.cs @@ -3,45 +3,44 @@ using uSync.BackOffice.SyncHandlers; using uSync.Core; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// An item notification object. +/// +public class uSyncItemNotification : INotification { - /// - /// An item notification object. - /// - public class uSyncItemNotification : INotification + /// + public uSyncItemNotification(TObject? item) + { + this.Item = item; + } + /// + public uSyncItemNotification(TObject? item, ChangeType change) + : this(item) { - /// - public uSyncItemNotification(TObject item) - { - this.Item = item; - } - /// - public uSyncItemNotification(TObject item, ChangeType change) - : this(item) - { - this.Change = change; - } + this.Change = change; + } - /// - public uSyncItemNotification(TObject item, ISyncHandler handler) - : this(item) - { - this.Handler = handler; - } + /// + public uSyncItemNotification(TObject? item, ISyncHandler handler) + : this(item) + { + this.Handler = handler; + } - /// - /// The type of Change being notified - /// - public ChangeType Change { get; set; } + /// + /// The type of Change being notified + /// + public ChangeType Change { get; set; } = ChangeType.NoChange; - /// - /// The item the notification is for - /// - public TObject Item { get; set; } + /// + /// The item the notification is for + /// + public TObject? Item { get; set; } - /// - /// The handler performing the notification - /// - public ISyncHandler Handler { get; set; } - } + /// + /// The handler performing the notification + /// + public ISyncHandler? Handler { get; set; } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportCompletedNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportCompletedNotification.cs index 2e364309..a361483e 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportCompletedNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportCompletedNotification.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Bulk notification when Reporting is complete +/// +public class uSyncReportCompletedNotification : uSyncBulkNotification { - /// - /// Bulk notification when Reporting is complete - /// - public class uSyncReportCompletedNotification : uSyncBulkNotification - { - /// - public uSyncReportCompletedNotification(IEnumerable actions) - : base(actions) { } - } + /// + public uSyncReportCompletedNotification(IEnumerable actions) + : base(actions) { } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportStartingNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportStartingNotification.cs index 7436d03e..6f5dd64a 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportStartingNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportStartingNotification.cs @@ -1,7 +1,6 @@ -namespace uSync.BackOffice -{ - /// - /// bulk notification object called before we report anything - /// - public class uSyncReportStartingNotification : CancelableuSyncBulkNotification { } -} +namespace uSync.BackOffice; + +/// +/// bulk notification object called before we report anything +/// +public class uSyncReportStartingNotification : CancelableuSyncBulkNotification { } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportedItemNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportedItemNotification.cs index 949f50ee..4e0177ca 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportedItemNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportedItemNotification.cs @@ -2,15 +2,14 @@ using uSync.Core; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Notification object when an item has been reported +/// +public class uSyncReportedItemNotification : uSyncItemNotification { - /// - /// Notification object when an item has been reported - /// - public class uSyncReportedItemNotification : uSyncItemNotification - { - /// - public uSyncReportedItemNotification(XElement item, ChangeType change) - : base(item, change) { } - } + /// + public uSyncReportedItemNotification(XElement item, ChangeType change) + : base(item, change) { } } diff --git a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportingItemNotification.cs b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportingItemNotification.cs index 38ebeb73..8b2d51ca 100644 --- a/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportingItemNotification.cs +++ b/uSync.BackOffice/Notifications/uSyncNotifications/uSyncReportingItemNotification.cs @@ -1,14 +1,13 @@ using System.Xml.Linq; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// Notification object for when an item is about to be reported on +/// +public class uSyncReportingItemNotification : CancelableuSyncItemNotification { - /// - /// Notification object for when an item is about to be reported on - /// - public class uSyncReportingItemNotification : CancelableuSyncItemNotification - { - /// - public uSyncReportingItemNotification(XElement item) - : base(item) { } - } + /// + public uSyncReportingItemNotification(XElement item) + : base(item) { } } diff --git a/uSync.BackOffice/Services/ISyncActionService.cs b/uSync.BackOffice/Services/ISyncActionService.cs index 0a4048b2..7e97eb83 100644 --- a/uSync.BackOffice/Services/ISyncActionService.cs +++ b/uSync.BackOffice/Services/ISyncActionService.cs @@ -18,27 +18,27 @@ public interface ISyncActionService /// /// run an export based on the options provided /// - SyncActionResult ExportHandler(SyncActionOptions options, uSyncCallbacks callbacks); + SyncActionResult ExportHandler(SyncActionOptions options, uSyncCallbacks? callbacks); /// /// get a list of the handlers for a given action /// - IEnumerable GetActionHandlers(HandlerActions action, uSyncOptions options); + IEnumerable GetActionHandlers(HandlerActions action, uSyncOptions? options); /// /// run an import against a handler based on the options provided. /// - SyncActionResult ImportHandler(SyncActionOptions options, uSyncCallbacks callbacks); + SyncActionResult ImportHandler(SyncActionOptions options, uSyncCallbacks? callbacks); /// /// run the post import step at the end of an import /// - SyncActionResult ImportPost(SyncActionOptions options, uSyncCallbacks callbacks); + SyncActionResult ImportPost(SyncActionOptions options, uSyncCallbacks? callbacks); /// /// run a report for a given handler based on the options provided. /// - SyncActionResult ReportHandler(SyncActionOptions options, uSyncCallbacks callbacks); + SyncActionResult ReportHandler(SyncActionOptions options, uSyncCallbacks? callbacks); /// /// start the bulk process diff --git a/uSync.BackOffice/Services/SyncActionService.cs b/uSync.BackOffice/Services/SyncActionService.cs index 0648f9f2..545700eb 100644 --- a/uSync.BackOffice/Services/SyncActionService.cs +++ b/uSync.BackOffice/Services/SyncActionService.cs @@ -39,13 +39,13 @@ public SyncActionService( _logger = logger; } - public IEnumerable GetActionHandlers(HandlerActions action, uSyncOptions options) + public IEnumerable GetActionHandlers(HandlerActions action, uSyncOptions? options) { - var handlerGroup = string.IsNullOrWhiteSpace(options.Group) + var handlerGroup = string.IsNullOrWhiteSpace(options?.Group) ? _uSyncConfig.Settings.UIEnabledGroups : options.Group; - var handlerSet = string.IsNullOrWhiteSpace(options.Set) + var handlerSet = string.IsNullOrWhiteSpace(options?.Set) ? _uSyncConfig.Settings.DefaultSet : options.Set; @@ -65,8 +65,10 @@ public IEnumerable GetActionHandlers(HandlerActions action, uSy }); } - public SyncActionResult ReportHandler(SyncActionOptions options, uSyncCallbacks callbacks) + public SyncActionResult ReportHandler(SyncActionOptions options, uSyncCallbacks? callbacks) { + if (options.Handler is null) return new(); + var handlerSet = !string.IsNullOrWhiteSpace(options.Set) ? options.Set : _uSyncConfig.Settings.DefaultSet; @@ -87,8 +89,10 @@ public SyncActionResult ReportHandler(SyncActionOptions options, uSyncCallbacks } - public SyncActionResult ImportHandler(SyncActionOptions options, uSyncCallbacks callbacks) + public SyncActionResult ImportHandler(SyncActionOptions options, uSyncCallbacks? callbacks) { + if (options.Handler is null) return new(); + var handlerSet = !string.IsNullOrWhiteSpace(options.Set) ? options.Set : _uSyncConfig.Settings.DefaultSet; @@ -109,7 +113,7 @@ public SyncActionResult ImportHandler(SyncActionOptions options, uSyncCallbacks return new SyncActionResult(actions); } - public SyncActionResult ImportPost(SyncActionOptions options, uSyncCallbacks callbacks) + public SyncActionResult ImportPost(SyncActionOptions options, uSyncCallbacks? callbacks) { var handlerSet = !string.IsNullOrWhiteSpace(options.Set) @@ -122,13 +126,15 @@ public SyncActionResult ImportPost(SyncActionOptions options, uSyncCallbacks cal handlerSet, options.Actions); - callbacks?.Update("Import Complete", 1, 1); + callbacks?.Update?.Invoke("Import Complete", 1, 1); return new SyncActionResult(actions); } - public SyncActionResult ExportHandler(SyncActionOptions options, uSyncCallbacks callbacks) + public SyncActionResult ExportHandler(SyncActionOptions options, uSyncCallbacks? callbacks) { + if (options.Handler is null) return new(); + var handlerSet = !string.IsNullOrWhiteSpace(options.Set) ? options.Set : _uSyncConfig.Settings.DefaultSet; @@ -170,7 +176,7 @@ private string MakeValidImportFolder(string folder) var rootParent = Path.GetDirectoryName(fullRoot.TrimEnd(new char[] { '/', '\\' })); _logger.LogDebug("Import Folder: {fullPath} {rootPath} {fullRoot}", fullPath, rootParent, fullRoot); - if (fullPath.StartsWith(rootParent)) + if (rootParent is not null && fullPath.StartsWith(rootParent)) { _logger.LogDebug("Using Custom Folder: {fullPath}", folder); return folder; diff --git a/uSync.BackOffice/Services/SyncFileService.cs b/uSync.BackOffice/Services/SyncFileService.cs index 58d75ac0..087edd5f 100644 --- a/uSync.BackOffice/Services/SyncFileService.cs +++ b/uSync.BackOffice/Services/SyncFileService.cs @@ -11,609 +11,615 @@ using Umbraco.Cms.Core.Extensions; -using uSync.BackOffice.Models; using uSync.Core; using uSync.Core.Tracking; -namespace uSync.BackOffice.Services +namespace uSync.BackOffice.Services; + +/// +/// putting all file actions in a service, +/// so if we want to abstract later we can. +/// +public class SyncFileService { + private readonly ILogger logger; + private readonly IHostEnvironment _hostEnvironment; + + private static char[] _trimChars = [' ', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]; + /// - /// putting all file actions in a service, - /// so if we want to abstract later we can. + /// Constructor for File service (via DI) /// - public class SyncFileService + /// + /// + public SyncFileService(ILogger logger, + IHostEnvironment hostEnvironment) { - private readonly ILogger logger; - private readonly IHostEnvironment _hostEnvironment; - - private static char[] _trimChars = [ ' ', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar ]; - - /// - /// Constructor for File service (via DI) - /// - /// - /// - public SyncFileService(ILogger logger, - IHostEnvironment hostEnvironment) - { - this.logger = logger; - _hostEnvironment = hostEnvironment; - } + this.logger = logger; + _hostEnvironment = hostEnvironment; + } - /// - /// return the absolute path for any given path. - /// - public string GetAbsPath(string path) - { - if (Path.IsPathFullyQualified(path)) return CleanLocalPath(path); - return CleanLocalPath(_hostEnvironment.MapPathContentRoot(path.TrimStart(_trimChars))); - } + /// + /// return the absolute path for any given path. + /// + public string GetAbsPath(string path) + { + if (Path.IsPathFullyQualified(path)) return CleanLocalPath(path); + return CleanLocalPath(_hostEnvironment.MapPathContentRoot(path.TrimStart(_trimChars))); + } - /// - /// Works out the relative path of a file to the site. - /// - /// - /// if the path is outside of the site root, then we return the whole path. - /// - public string GetSiteRelativePath(string path) - { - if (Path.IsPathFullyQualified(path)) - return path.Substring(_hostEnvironment.ContentRootPath.Length).TrimStart(_trimChars); - return path; - } + /// + /// Works out the relative path of a file to the site. + /// + /// + /// if the path is outside of the site root, then we return the whole path. + /// + public string GetSiteRelativePath(string path) + { + if (Path.IsPathFullyQualified(path) && path.StartsWith(_hostEnvironment.ContentRootPath)) + return path.Substring(_hostEnvironment.ContentRootPath.Length).TrimStart(_trimChars); + return path; + } - /// - /// clean up the local path, and full expand any short file names - /// - private string CleanLocalPath(string path) - => Path.GetFullPath(path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)); - - /// - /// does a file exist - /// - public bool FileExists(string path) - => File.Exists(GetAbsPath(path)); - - - /// - /// compare two file paths, and tell us if they match - /// - public bool PathMatches(string a, string b) - => GetAbsPath(a).Equals(GetAbsPath(b), StringComparison.InvariantCultureIgnoreCase); - - /// - /// does a directory exist - /// - /// - /// - public bool DirectoryExists(string path) - => Directory.Exists(GetAbsPath(path)); - - /// - /// checks and tells you if a folder exists and has sub folders. - /// - /// - /// we use this to confirm that a uSync folder has something init. - /// - public bool DirectoryHasChildren(string path) - { - var fullPath = GetAbsPath(path); - if (Directory.Exists(fullPath) is false) return false; - return Directory.GetDirectories(fullPath).Length > 0; - } + /// + /// clean up the local path, and full expand any short file names + /// + private string CleanLocalPath(string path) + => Path.GetFullPath(path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)); - /// - /// dies the root path exist. - /// - /// - /// - public bool RootExists(string path) - => DirectoryExists(path); - - /// - /// remove a file from disk. - /// - /// - public void DeleteFile(string path) - { - var localPath = GetAbsPath(path); - if (FileExists(localPath)) - File.Delete(localPath); - } + /// + /// does a file exist + /// + public bool FileExists(string path) + => File.Exists(GetAbsPath(path)); - /// - /// Check if a file exists throw an exception if it doesn't - /// - public void EnsureFileExists(string path) - { - if (!FileExists(path)) - throw new FileNotFoundException("Missing File", path); - } - /// - /// open a file stream for reading a file - /// - public FileStream OpenRead(string path) - { - var localPath = GetAbsPath(path); + /// + /// compare two file paths, and tell us if they match + /// + public bool PathMatches(string a, string b) + => GetAbsPath(a).Equals(GetAbsPath(b), StringComparison.InvariantCultureIgnoreCase); - if (!FileExists(localPath)) return null; - return File.OpenRead(localPath); - } + /// + /// does a directory exist + /// + /// + /// + public bool DirectoryExists(string path) + => Directory.Exists(GetAbsPath(path)); - /// - /// Open a file stream for writing a file - /// - public FileStream OpenWrite(string path) - { - var localPath = GetAbsPath(path); + /// + /// checks and tells you if a folder exists and has sub folders. + /// + /// + /// we use this to confirm that a uSync folder has something init. + /// + public bool DirectoryHasChildren(string path) + { + var fullPath = GetAbsPath(path); + if (Directory.Exists(fullPath) is false) return false; + return Directory.GetDirectories(fullPath).Length > 0; + } - if (FileExists(localPath)) - DeleteFile(localPath); + /// + /// dies the root path exist. + /// + /// + /// + public bool RootExists(string path) + => DirectoryExists(path); - CreateFoldersForFile(path); - return File.OpenWrite(localPath); - } + /// + /// remove a file from disk. + /// + /// + public void DeleteFile(string path) + { + var localPath = GetAbsPath(path); + if (FileExists(localPath)) + File.Delete(localPath); + } - /// - /// copy a file from one location to another - creating the directory if it is missing - /// - public void CopyFile(string source, string target) - { - var absSource = GetAbsPath(source); - var absTarget = GetAbsPath(target); - Directory.CreateDirectory(Path.GetDirectoryName(absTarget)); - File.Copy(absSource, absTarget, true); - } + /// + /// Check if a file exists throw an exception if it doesn't + /// + public void EnsureFileExists(string path) + { + if (!FileExists(path)) + throw new FileNotFoundException("Missing File", path); + } - /// - /// create the directory for a given file. - /// - /// - public void CreateFoldersForFile(string filePath) - { - var absPath = Path.GetDirectoryName(GetAbsPath(filePath)); - if (!Directory.Exists(absPath)) - Directory.CreateDirectory(absPath); - } + /// + /// open a file stream for reading a file + /// + public FileStream? OpenRead(string path) + { + var localPath = GetAbsPath(path); + + if (!FileExists(localPath)) return null; + return File.OpenRead(localPath); + } + + /// + /// Open a file stream for writing a file + /// + public FileStream OpenWrite(string path) + { + var localPath = GetAbsPath(path); + + if (FileExists(localPath)) + DeleteFile(localPath); + + CreateFoldersForFile(path); + return File.OpenWrite(localPath); + } + + /// + /// copy a file from one location to another - creating the directory if it is missing + /// + public void CopyFile(string source, string target) + { + var absSource = GetAbsPath(source); + var absTarget = GetAbsPath(target); - /// - /// Create a directory. - /// - /// - public void CreateFolder(string folder) + var directoryName = Path.GetDirectoryName(absTarget); + if (string.IsNullOrEmpty(directoryName)) + throw new DirectoryNotFoundException($"Cannot find directory for {absTarget}"); + + Directory.CreateDirectory(directoryName); + File.Copy(absSource, absTarget, true); + } + + /// + /// create the directory for a given file. + /// + /// + public void CreateFoldersForFile(string filePath) + { + var absPath = Path.GetDirectoryName(GetAbsPath(filePath)); + if (string.IsNullOrEmpty(absPath)) return; + + if (Directory.Exists(absPath) is false) + Directory.CreateDirectory(absPath); + } + + /// + /// Create a directory. + /// + /// + public void CreateFolder(string folder) + { + var absPath = GetAbsPath(folder); + if (!Directory.Exists(folder)) + Directory.CreateDirectory(folder); + } + + /// + /// remove a folder and all its contents + /// + public void CleanFolder(string folder) + { + var absPath = GetAbsPath(folder); + + if (Directory.Exists(absPath)) + Directory.Delete(absPath, true); + } + + /// + /// Get a list of files from a folder. + /// + public IEnumerable GetFiles(string folder, string extensions) + => GetFiles(folder, extensions, false); + + /// + /// get all the files in a folder, + /// + /// path to the folder + /// list of extensions (filter) + /// get all files in all decentant folders + /// + public IEnumerable GetFiles(string folder, string extensions, bool allFolders) + { + var localPath = GetAbsPath(folder); + if (DirectoryExists(localPath)) { - var absPath = GetAbsPath(folder); - if (!Directory.Exists(folder)) - Directory.CreateDirectory(folder); + return Directory.GetFiles(localPath, extensions, allFolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); } - /// - /// remove a folder and all its contents - /// - public void CleanFolder(string folder) - { - var absPath = GetAbsPath(folder); + return Enumerable.Empty(); - if (Directory.Exists(absPath)) - Directory.Delete(absPath, true); - } + } - /// - /// Get a list of files from a folder. - /// - public IEnumerable GetFiles(string folder, string extensions) - => GetFiles(folder, extensions, false); - - /// - /// get all the files in a folder, - /// - /// path to the folder - /// list of extensions (filter) - /// get all files in all decentant folders - /// - public IEnumerable GetFiles(string folder, string extensions, bool allFolders) + /// + /// get a list of child folders in a folder + /// + public IEnumerable GetDirectories(string folder) + { + var localPath = GetAbsPath(folder); + if (DirectoryExists(localPath)) { - var localPath = GetAbsPath(folder); - if (DirectoryExists(localPath)) - { - return Directory.GetFiles(localPath, extensions, allFolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); - } + return Directory.GetDirectories(localPath); + } - return Enumerable.Empty(); + return Enumerable.Empty(); + } - } + /// + /// load a file into a XElement object. + /// + public XElement LoadXElement(string file) + { + EnsureFileExists(file); - /// - /// get a list of child folders in a folder - /// - public IEnumerable GetDirectories(string folder) + try { - var localPath = GetAbsPath(folder); - if (DirectoryExists(localPath)) + using (var stream = OpenRead(file)) { - return Directory.GetDirectories(localPath); - } + if (stream is null) + throw new FileNotFoundException($"Cannot create stream for {file}"); ; - return Enumerable.Empty(); + return XElement.Load(stream); + } } - - /// - /// load a file into a XElement object. - /// - public XElement LoadXElement(string file) + catch (Exception ex) { - EnsureFileExists(file); - - try - { - using (var stream = OpenRead(file)) - { - return XElement.Load(stream); - } - } - catch (Exception ex) - { - logger.LogWarning("Error while reading in {file} {message}", file, ex.Message); - throw new Exception($"Error while reading in {file}", ex); - } + logger.LogWarning("Error while reading in {file} {message}", file, ex.Message); + throw new Exception($"Error while reading in {file}", ex); } + } - /// - /// save a stream to disk - /// - public void SaveFile(string filename, Stream stream) - { - logger.LogDebug("Saving File: {file}", filename); + /// + /// save a stream to disk + /// + public void SaveFile(string filename, Stream stream) + { + logger.LogDebug("Saving File: {file}", filename); - using (Stream fileStream = OpenWrite(filename)) - { - stream.CopyTo(fileStream); - fileStream.Flush(); - fileStream.Close(); - } + using (Stream fileStream = OpenWrite(filename)) + { + stream.CopyTo(fileStream); + fileStream.Flush(); + fileStream.Close(); } + } - /// - /// save a string to disk - /// - public void SaveFile(string filename, string content) - { - var localFile = GetAbsPath(filename); - logger.LogDebug("Saving File: {local} [{length}]", localFile, content.Length); + /// + /// save a string to disk + /// + public void SaveFile(string filename, string content) + { + var localFile = GetAbsPath(filename); + logger.LogDebug("Saving File: {local} [{length}]", localFile, content.Length); - using (Stream stream = OpenWrite(localFile)) - { - byte[] info = new UTF8Encoding(true).GetBytes(content); - stream.Write(info, 0, info.Length); - stream.Flush(); - stream.Dispose(); - } + using (Stream stream = OpenWrite(localFile)) + { + byte[] info = new UTF8Encoding(true).GetBytes(content); + stream.Write(info, 0, info.Length); + stream.Flush(); + stream.Dispose(); } + } - /// - /// Save an XML Element to disk - /// - public void SaveXElement(XElement node, string filename) + /// + /// Save an XML Element to disk + /// + public void SaveXElement(XElement node, string filename) + { + var localPath = GetAbsPath(filename); + using (var stream = OpenWrite(localPath)) { - var localPath = GetAbsPath(filename); - using (var stream = OpenWrite(localPath)) - { - node.Save(stream); - stream.Flush(); - stream.Dispose(); - } + node.Save(stream); + stream.Flush(); + stream.Dispose(); } + } - /// - /// Load an object from XML representation on disk. - /// - public TObject LoadXml(string file) + /// + /// Load an object from XML representation on disk. + /// + public TObject? LoadXml(string file) + { + if (FileExists(file)) { - if (FileExists(file)) + XmlSerializer xmlSerializer = new XmlSerializer(typeof(TObject)); + using (var stream = OpenRead(file)) { - XmlSerializer xmlSerializer = new XmlSerializer(typeof(TObject)); - using (var stream = OpenRead(file)) - { - var item = (TObject)xmlSerializer.Deserialize(stream); - return item; - } + if (stream is null) return default; + + TObject? item = (TObject?)xmlSerializer.Deserialize(stream); + return item; } - return default(TObject); } + return default; + } - /// - /// load the contents of a file into a string - /// - public string LoadContent(string file) + /// + /// load the contents of a file into a string + /// + public string LoadContent(string file) + { + if (FileExists(file)) { - if (FileExists(file)) - { - var absPath = this.GetAbsPath(file); - return File.ReadAllText(absPath); - } - - return string.Empty; + var absPath = this.GetAbsPath(file); + return File.ReadAllText(absPath); } - /// - /// Remove a folder from disk - /// - public void DeleteFolder(string folder, bool safe = false) + return string.Empty; + } + + /// + /// Remove a folder from disk + /// + public void DeleteFolder(string folder, bool safe = false) + { + try { - try - { - var resolvedFolder = GetAbsPath(folder); - if (Directory.Exists(resolvedFolder)) - Directory.Delete(resolvedFolder, true); - } - catch(Exception ex) - { - // can happen when its locked, question is - do you care? - logger.LogWarning(ex, "Failed to remove directory {folder}", folder); - if (!safe) throw; - } + var resolvedFolder = GetAbsPath(folder); + if (Directory.Exists(resolvedFolder)) + Directory.Delete(resolvedFolder, true); } - - /// - /// copy the contents of a folder - /// - public void CopyFolder(string source, string target) + catch (Exception ex) { - var resolvedSource = GetAbsPath(source).TrimEnd(Path.DirectorySeparatorChar); ; - var resolvedTarget = GetAbsPath(target).TrimEnd(Path.DirectorySeparatorChar); + // can happen when its locked, question is - do you care? + logger.LogWarning(ex, "Failed to remove directory {folder}", folder); + if (!safe) throw; + } + } - if (!Directory.Exists(resolvedSource)) - throw new DirectoryNotFoundException(source); + /// + /// copy the contents of a folder + /// + public void CopyFolder(string source, string target) + { + var resolvedSource = GetAbsPath(source).TrimEnd(Path.DirectorySeparatorChar); ; + var resolvedTarget = GetAbsPath(target).TrimEnd(Path.DirectorySeparatorChar); - Directory.CreateDirectory(resolvedTarget); + if (!Directory.Exists(resolvedSource)) + throw new DirectoryNotFoundException(source); - // create all the sub folders - var folders = Directory.GetDirectories(resolvedSource, "*", SearchOption.AllDirectories); - foreach(var folder in folders) - { - Directory.CreateDirectory(folder.Replace(resolvedSource, resolvedTarget)); - } + Directory.CreateDirectory(resolvedTarget); - // copy all the files - var files = Directory.GetFiles(resolvedSource, "*.*", SearchOption.AllDirectories); - foreach(var file in files) - { - File.Copy(file, file.Replace(resolvedSource, resolvedTarget), true); - } - + // create all the sub folders + var folders = Directory.GetDirectories(resolvedSource, "*", SearchOption.AllDirectories); + foreach (var folder in folders) + { + Directory.CreateDirectory(folder.Replace(resolvedSource, resolvedTarget)); } + // copy all the files + var files = Directory.GetFiles(resolvedSource, "*.*", SearchOption.AllDirectories); + foreach (var file in files) + { + File.Copy(file, file.Replace(resolvedSource, resolvedTarget), true); + } + + } + - // TODO: this doesn't need to be public? + // TODO: this doesn't need to be public? - /// - /// Locking item for saves. - /// - public static object _saveLock = new object(); + /// + /// Locking item for saves. + /// + public static object _saveLock = new object(); - /// - /// save an object to an XML file representing it. - /// - public void SaveXml(string file, TObject item) + /// + /// save an object to an XML file representing it. + /// + public void SaveXml(string file, TObject item) + { + lock (_saveLock) { - lock (_saveLock) - { - if (FileExists(file)) - DeleteFile(file); + if (FileExists(file)) + DeleteFile(file); - var xmlSerializer = new XmlSerializer(typeof(TObject)); - using (var stream = OpenWrite(file)) - { - xmlSerializer.Serialize(stream, item); - } + var xmlSerializer = new XmlSerializer(typeof(TObject)); + using (var stream = OpenWrite(file)) + { + xmlSerializer.Serialize(stream, item); } } + } - /// - /// run some basic sanity checks on a folder to see if it looks like a good - /// set of uSync files ? - /// - public List VerifyFolder(string folder, string extension) - { - var resolvedFolder = GetAbsPath(folder); - if (!DirectoryExists(resolvedFolder)) - throw new DirectoryNotFoundException(folder); + /// + /// run some basic sanity checks on a folder to see if it looks like a good + /// set of uSync files ? + /// + public List VerifyFolder(string folder, string extension) + { + var resolvedFolder = GetAbsPath(folder); + if (!DirectoryExists(resolvedFolder)) + throw new DirectoryNotFoundException(folder); - var keys = new Dictionary(); - var errors = new List(); + var keys = new Dictionary(); + var errors = new List(); - var files = Directory.GetFiles(resolvedFolder, $"*.{extension}", SearchOption.AllDirectories) - .ToList(); + var files = Directory.GetFiles(resolvedFolder, $"*.{extension}", SearchOption.AllDirectories) + .ToList(); - if (files.Count == 0) - { - errors.Add($"There are no files with extension .{extension} in this zip file, is it even an import?"); - return errors; - } + if (files.Count == 0) + { + errors.Add($"There are no files with extension .{extension} in this zip file, is it even an import?"); + return errors; + } - foreach (var file in files) + foreach (var file in files) + { + try { - try + var node = XElement.Load(file); + + if (!node.IsEmptyItem()) { - var node = XElement.Load(file); + // make the key unique for the type, then we don't get false + // positives when different bits share ids (like PublicAccess and Content) - if (!node.IsEmptyItem()) + var key = $"{node.Name.LocalName}_{node.GetKey()}"; + var folderName = Path.GetFileName(Path.GetDirectoryName(file)); + var filename = Path.GetFileName(file); + var filePath = GetShortFileName(file); + + if (!keys.ContainsKey(key)) { - // make the key unique for the type, then we don't get false - // positives when different bits share ids (like PublicAccess and Content) - - var key = $"{node.Name.LocalName}_{node.GetKey()}"; - var folderName = Path.GetFileName(Path.GetDirectoryName(file)); - var filename = Path.GetFileName(file); - var filePath = GetShortFileName(file); - - if (!keys.ContainsKey(key)) - { - keys[key] = filePath; - } - else - { - errors.Add($"Clash {filePath} shares an id with {keys[key]}"); - } + keys[key] = filePath; + } + else + { + errors.Add($"Clash {filePath} shares an id with {keys[key]}"); } - } - catch (Exception ex) - { - errors.Add($"{GetShortFileName(file)} is invalid {ex.Message}"); } } - - return errors; + catch (Exception ex) + { + errors.Add($"{GetShortFileName(file)} is invalid {ex.Message}"); + } } - string GetShortFileName(string file) - => $"{Path.DirectorySeparatorChar}{Path.GetFileName(Path.GetDirectoryName(file))}" + - $"{Path.DirectorySeparatorChar}{Path.GetFileName(file)}"; - - // roots - #region roots - - /// - /// Merge a number of uSync folders into a single 'usync source' - /// - /// - /// this is the core of the "roots" functionality. folders are - /// merged upwards (so the last folder will win) - /// - /// custom merging can be achieved using additional methods on - /// the change trackers. - /// - /// the doctype tracker merges properties so you can have - /// property level root values for doctypes. - /// - public IEnumerable MergeFolders(string[] folders, string extension, ISyncTrackerBase trackerBase) - { - var elements = new Dictionary(); + return errors; + } - foreach (var folder in folders) - { - var absPath = GetAbsPath($"~/{folder}"); + string GetShortFileName(string file) + => $"{Path.DirectorySeparatorChar}{Path.GetFileName(Path.GetDirectoryName(file))}" + + $"{Path.DirectorySeparatorChar}{Path.GetFileName(file)}"; - if (DirectoryExists(absPath) is false) continue; + // roots + #region roots - var items = GetFolderItems(absPath, extension); + /// + /// Merge a number of uSync folders into a single 'usync source' + /// + /// + /// this is the core of the "roots" functionality. folders are + /// merged upwards (so the last folder will win) + /// + /// custom merging can be achieved using additional methods on + /// the change trackers. + /// + /// the doctype tracker merges properties so you can have + /// property level root values for doctypes. + /// + public IEnumerable MergeFolders(string[] folders, string extension, ISyncTrackerBase? trackerBase) + { + var elements = new Dictionary(); - foreach (var item in items) - { - if (elements.ContainsKey(item.Key)) - { - // merge these files. - item.Value.SetNode(trackerBase.MergeFiles(elements[item.Key].Node, item.Value.Node)); - } + foreach (var folder in folders) + { + var absPath = GetAbsPath($"~/{folder}"); - elements[item.Key] = item.Value; - } - } + if (DirectoryExists(absPath) is false) continue; - return elements.Values; - } + var items = GetFolderItems(absPath, extension); - private IEnumerable> GetFolderItems(string folder, string extension) - { - foreach (var file in GetFilePaths(folder, extension)) + foreach (var item in items) { - var element = LoadXElementSafe(file); - if (element != null) + if (trackerBase is not null && elements.ContainsKey(item.Key)) { - var path = file.Substring(folder.Length); - - yield return new KeyValuePair( - key: path, - value: new OrderedNodeInfo( - filename: file, - node: element, - level: (element.GetLevel() * 1000) + element.GetItemSortOrder(), - path: path, - isRoot: true)); + // merge these files. + item.Value.SetNode(trackerBase.MergeFiles(elements[item.Key].Node, item.Value.Node)); } + + elements[item.Key] = item.Value; } } - /// - /// will load the most relevant version of a file. - /// - public XElement GetNearestNode(string filePath, string[] folders) + return elements.Values; + } + + private IEnumerable> GetFolderItems(string folder, string extension) + { + foreach (var file in GetFilePaths(folder, extension)) { - foreach (var folder in folders.Reverse()) + var element = LoadXElementSafe(file); + if (element != null) { - var path = Path.Combine(folder, filePath); - if (FileExists(path)) - return LoadXElementSafe(path); + var path = file.Substring(folder.Length); + + yield return new KeyValuePair( + key: path, + value: new OrderedNodeInfo( + filename: file, + node: element, + level: (element.GetLevel() * 1000) + element.GetItemSortOrder(), + path: path, + isRoot: true)); } - - return null; } + } - /// - /// get a XML representation of the differences between two files - /// - /// - /// the default merger returns the whole xml as the difference. - /// - public XElement GetDifferences(List nodes, ISyncTrackerBase trackerBase) + /// + /// will load the most relevant version of a file. + /// + public XElement? GetNearestNode(string filePath, string[] folders) + { + foreach (var folder in folders.Reverse()) { - if (nodes?.Count == 0) return null; - if (nodes.Count == 1) return nodes[0]; - - return trackerBase.GetDifferences(nodes); + var path = Path.Combine(folder, filePath); + if (FileExists(path)) + return LoadXElementSafe(path); } - /// - /// get all xml elements that represent this item across - /// all folders. - /// - public List GetAllNodes(string[] filePaths) - { - var nodes = new List(filePaths.Length); - foreach(var file in filePaths) - { - if (!FileExists(file)) continue; - var element = LoadXElementSafe(file); - if (element != null) - nodes.Add(element); - } - - return nodes; - } + return null; + } - /// - /// checks a list of folders to see if any of them exists - /// - /// true if any but the last folder exists - public bool AnyFolderExists(string[] folders) - => folders.Any(DirectoryExists); + /// + /// get a XML representation of the differences between two files + /// + /// + /// the default merger returns the whole xml as the difference. + /// + public XElement? GetDifferences(List nodes, ISyncTrackerBase? trackerBase) + { + if (nodes is null || nodes?.Count == 0) return null; + if (nodes!.Count == 1) return nodes[0]; - private string[] GetFilePaths(string folder, string extension) - => Directory.GetFiles(folder, $"*.{extension}", SearchOption.AllDirectories); + if (trackerBase is null) return null; + return trackerBase.GetDifferences(nodes); + } - private XElement LoadXElementSafe(string file) + /// + /// get all xml elements that represent this item across + /// all folders. + /// + public List GetAllNodes(string[] filePaths) + { + var nodes = new List(filePaths.Length); + foreach (var file in filePaths) { - var absPath = GetAbsPath(file); - - if (FileExists(absPath) is false) return null; - - try - { - return XElement.Load(absPath); - } - catch - { - return null; - } + if (!FileExists(file)) continue; + var element = LoadXElementSafe(file); + if (element != null) + nodes.Add(element); } - #endregion - + return nodes; } + /// + /// checks a list of folders to see if any of them exists + /// + /// true if any but the last folder exists + public bool AnyFolderExists(string[] folders) + => folders.Any(DirectoryExists); + + private string[] GetFilePaths(string folder, string extension) + => Directory.GetFiles(folder, $"*.{extension}", SearchOption.AllDirectories); + private XElement? LoadXElementSafe(string file) + { + var absPath = GetAbsPath(file); + if (FileExists(absPath) is false) return null; + + try + { + return XElement.Load(absPath); + } + catch + { + return null; + } + } + #endregion } diff --git a/uSync.BackOffice/Services/uSyncEventService.cs b/uSync.BackOffice/Services/uSyncEventService.cs index 0c19ae41..b2db5632 100644 --- a/uSync.BackOffice/Services/uSyncEventService.cs +++ b/uSync.BackOffice/Services/uSyncEventService.cs @@ -3,89 +3,88 @@ using Umbraco.Cms.Core.Events; -namespace uSync.BackOffice.Services +namespace uSync.BackOffice.Services; + +/// +/// handles the events and locks for uSync events. +/// +/// +/// stops us tripping up over uSync firing save events etc while importing +/// gives us one place to fire our notifications from +/// +public class uSyncEventService { + private readonly IEventAggregator _eventAggregator; + + /// + /// generate a new uSyncEventService object + /// + /// + public uSyncEventService(IEventAggregator eventAggregator) + { + _eventAggregator = eventAggregator; + } + + /// + /// is uSync paused or not ? + /// + public bool IsPaused { get; private set; } + + + /// + /// pause the uSync triggering process + /// + public void Pause() => IsPaused = true; + + /// + /// un-pause the uSync triggering process + /// + public void UnPause() => IsPaused = false; + /// - /// handles the events and locks for uSync events. + /// get an import pause object (pauses the import until it is disposed) + /// + [Obsolete("You should tell uSync if it needs to pause or not during the process (removed in v12)")] + public uSyncImportPause ImportPause() + => new uSyncImportPause(this); + + /// + /// get an import pause object (pauses the import until it is disposed) /// /// - /// stops us tripping up over uSync firing save events etc while importing - /// gives us one place to fire our notifications from + /// you should wrap code that might trigger Umbraco events in using(var pause = _mutexService.ImportPause()) + /// this will ensure that uSync doesn't then pickup the imports as new things and saves them to disk. /// - public class uSyncEventService + public uSyncImportPause ImportPause(bool pause) + => new uSyncImportPause(this, pause); + + + + //// notification events. + /// + + internal bool FireBulkStarting(CancelableuSyncBulkNotification bulkNotification) { - private readonly IEventAggregator _eventAggregator; - - /// - /// generate a new uSyncEventService object - /// - /// - public uSyncEventService(IEventAggregator eventAggregator) - { - _eventAggregator = eventAggregator; - } - - /// - /// is uSync paused or not ? - /// - public bool IsPaused { get; private set; } - - - /// - /// pause the uSync triggering process - /// - public void Pause() => IsPaused = true; - - /// - /// un-pause the uSync triggering process - /// - public void UnPause() => IsPaused = false; - - /// - /// get an import pause object (pauses the import until it is disposed) - /// - [Obsolete("You should tell uSync if it needs to pause or not during the process (removed in v12)")] - public uSyncImportPause ImportPause() - => new uSyncImportPause(this); - - /// - /// get an import pause object (pauses the import until it is disposed) - /// - /// - /// you should wrap code that might trigger Umbraco events in using(var pause = _mutexService.ImportPause()) - /// this will ensure that uSync doesn't then pickup the imports as new things and saves them to disk. - /// - public uSyncImportPause ImportPause(bool pause) - => new uSyncImportPause(this, pause); - - - - //// notification events. - /// - - internal bool FireBulkStarting(CancelableuSyncBulkNotification bulkNotification) - { - _eventAggregator.PublishCancelable(bulkNotification); - return bulkNotification.Cancel; - } - - internal void FireBulkComplete(uSyncBulkNotification notification) - { - _eventAggregator.Publish(notification); - } - - internal bool FireItemStartingEvent(CancelableuSyncItemNotification notification) - { - _eventAggregator.PublishCancelable(notification); - return notification.Cancel; - } - - internal void FireItemCompletedEvent(uSyncItemNotification notification) - { - _eventAggregator.Publish(notification); - } + _eventAggregator.PublishCancelable(bulkNotification); + return bulkNotification.Cancel; + } + internal void FireBulkComplete(uSyncBulkNotification notification) + { + _eventAggregator.Publish(notification); + } + internal bool FireItemStartingEvent(CancelableuSyncItemNotification notification) + { + _eventAggregator.PublishCancelable(notification); + return notification.Cancel; + } + internal void FireItemCompletedEvent(uSyncItemNotification notification) + { + _eventAggregator.Publish(notification); } + + + } diff --git a/uSync.BackOffice/Services/uSyncService.cs b/uSync.BackOffice/Services/uSyncService.cs index 3151a230..94f1fae5 100644 --- a/uSync.BackOffice/Services/uSyncService.cs +++ b/uSync.BackOffice/Services/uSyncService.cs @@ -21,603 +21,602 @@ using uSync.Core; using uSync.Core.Serialization; -namespace uSync.BackOffice +namespace uSync.BackOffice; + + +/// +/// the service that does all the processing, +/// this forms the entry point as an API to +/// uSync, it is where imports, exports and reports +/// are actually ran from. +/// +public partial class uSyncService { + /// + /// Callback event for SignalR hub + /// + public delegate void SyncEventCallback(SyncProgressSummary summary); + + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + + private readonly IEventAggregator _eventAggregator; + + private readonly uSyncConfigService _uSyncConfig; + private readonly SyncHandlerFactory _handlerFactory; + private readonly SyncFileService _syncFileService; + private readonly uSyncEventService _mutexService; + + private readonly ICoreScopeProvider _scopeProvider; + + private readonly IBackgroundTaskQueue? _backgroundTaskQueue; + + private readonly IAppCache _appCache; /// - /// the service that does all the processing, - /// this forms the entry point as an API to - /// uSync, it is where imports, exports and reports - /// are actually ran from. + /// Create uSync Service /// - public partial class uSyncService + [Obsolete("Use method with background service will be removed in v15")] + public uSyncService( + ILogger logger, + IEventAggregator eventAggregator, + uSyncConfigService uSyncConfigService, + SyncHandlerFactory handlerFactory, + SyncFileService syncFileService, + uSyncEventService mutexService, + AppCaches appCaches, + ICoreScopeProvider scopeProvider, + ILoggerFactory loggerFactory) { - /// - /// Callback event for SignalR hub - /// - public delegate void SyncEventCallback(SyncProgressSummary summary); - - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - - private readonly IEventAggregator _eventAggregator; - - private readonly uSyncConfigService _uSyncConfig; - private readonly SyncHandlerFactory _handlerFactory; - private readonly SyncFileService _syncFileService; - private readonly uSyncEventService _mutexService; - - private readonly ICoreScopeProvider _scopeProvider; - - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - - private readonly IAppCache _appCache; - - /// - /// Create uSync Service - /// - [Obsolete("Use method with background service will be removed in v15")] - public uSyncService( - ILogger logger, - IEventAggregator eventAggregator, - uSyncConfigService uSyncConfigService, - SyncHandlerFactory handlerFactory, - SyncFileService syncFileService, - uSyncEventService mutexService, - AppCaches appCaches, - ICoreScopeProvider scopeProvider, - ILoggerFactory loggerFactory) - { - this._logger = logger; + this._logger = logger; - this._eventAggregator = eventAggregator; + this._eventAggregator = eventAggregator; - this._uSyncConfig = uSyncConfigService; - this._handlerFactory = handlerFactory; - this._syncFileService = syncFileService; - this._mutexService = mutexService; + this._uSyncConfig = uSyncConfigService; + this._handlerFactory = handlerFactory; + this._syncFileService = syncFileService; + this._mutexService = mutexService; - this._appCache = appCaches.RuntimeCache; + this._appCache = appCaches.RuntimeCache; - uSyncTriggers.DoExport += USyncTriggers_DoExport; - uSyncTriggers.DoImport += USyncTriggers_DoImport; - _scopeProvider = scopeProvider; - _loggerFactory = loggerFactory; - } + uSyncTriggers.DoExport += USyncTriggers_DoExport; + uSyncTriggers.DoImport += USyncTriggers_DoImport; + _scopeProvider = scopeProvider; + _loggerFactory = loggerFactory; + } - /// - /// Create a new uSyncService (done via DI) - /// - public uSyncService( - ILogger logger, - IEventAggregator eventAggregator, - uSyncConfigService uSyncConfigService, - SyncHandlerFactory handlerFactory, - SyncFileService syncFileService, - uSyncEventService mutexService, - AppCaches appCaches, - ICoreScopeProvider scopeProvider, - ILoggerFactory loggerFactory, - IBackgroundTaskQueue backgroundTaskQueue) - : this(logger, eventAggregator, uSyncConfigService, - handlerFactory, syncFileService, mutexService, - appCaches, scopeProvider, loggerFactory) - { - _backgroundTaskQueue = backgroundTaskQueue; - } + /// + /// Create a new uSyncService (done via DI) + /// + public uSyncService( + ILogger logger, + IEventAggregator eventAggregator, + uSyncConfigService uSyncConfigService, + SyncHandlerFactory handlerFactory, + SyncFileService syncFileService, + uSyncEventService mutexService, + AppCaches appCaches, + ICoreScopeProvider scopeProvider, + ILoggerFactory loggerFactory, + IBackgroundTaskQueue backgroundTaskQueue) + : this(logger, eventAggregator, uSyncConfigService, + handlerFactory, syncFileService, mutexService, + appCaches, scopeProvider, loggerFactory) + { + _backgroundTaskQueue = backgroundTaskQueue; + } - /// - /// Does the given folder contain and uSync files for Content - /// - public bool HasContentFiles(string rootFolder) - { - return _syncFileService.DirectoryExists(rootFolder + "Content"); - } + /// + /// Does the given folder contain and uSync files for Content + /// + public bool HasContentFiles(string rootFolder) + { + return _syncFileService.DirectoryExists(rootFolder + "Content"); + } - /// - /// check to see if any of the uSync folders have content files. - /// - public bool HasContentFiles(string[] folders) - => folders.Any(x => HasContentFiles(x)); - - /// - /// check if there are any root files on disk. - /// - public bool HasRootFiles(string[] folders) - => folders[..^1].Any(x => _syncFileService.DirectoryHasChildren(x)); - - - #region Reporting - - /// - /// Report the changes for a folder - /// - /// Folder to run the report for - /// Options to use for the report - used to load the handlers. - /// Callback functions to keep UI up to date - /// List of actions detailing what would and wouldn't change - public IEnumerable Report(string folder, SyncHandlerOptions handlerOptions, uSyncCallbacks callbacks = null) - { - handlerOptions ??= new SyncHandlerOptions(); - handlerOptions.Action = HandlerActions.Report; + /// + /// check to see if any of the uSync folders have content files. + /// + public bool HasContentFiles(string[] folders) + => folders.Any(x => HasContentFiles(x)); - var handlers = _handlerFactory.GetValidHandlers(handlerOptions); - return Report(folder, handlers, callbacks); - } + /// + /// check if there are any root files on disk. + /// + public bool HasRootFiles(string[] folders) + => folders[..^1].Any(x => _syncFileService.DirectoryHasChildren(x)); - /// - /// Report the changes for a folder - /// - /// Folder to run the report for - /// List of Aliases for the sync handlers to use - /// Callback functions to keep UI up to date - /// List of actions detailing what would and wouldn't change - public IEnumerable Report(string folder, IEnumerable handlerAliases, uSyncCallbacks callbacks) - { - var handlers = _handlerFactory.GetDefaultHandlers(handlerAliases); - return Report(folder, handlers, callbacks); - } - /// - /// Report the changes for a folder - /// - /// Folder to run the report for - /// List of SyncHandlers to use for the report - /// Callback functions to keep UI up to date - /// List of actions detailing what would and wouldn't change - public IEnumerable Report(string folder, IEnumerable handlers, uSyncCallbacks callbacks) - { + #region Reporting - var sw = Stopwatch.StartNew(); + /// + /// Report the changes for a folder + /// + /// Folder to run the report for + /// Options to use for the report - used to load the handlers. + /// Callback functions to keep UI up to date + /// List of actions detailing what would and wouldn't change + public IEnumerable Report(string folder, SyncHandlerOptions handlerOptions, uSyncCallbacks? callbacks = null) + { + handlerOptions ??= new SyncHandlerOptions(); + handlerOptions.Action = HandlerActions.Report; + + var handlers = _handlerFactory.GetValidHandlers(handlerOptions); + return Report(folder, handlers, callbacks); + } - _mutexService.FireBulkStarting(new uSyncReportStartingNotification()); + /// + /// Report the changes for a folder + /// + /// Folder to run the report for + /// List of Aliases for the sync handlers to use + /// Callback functions to keep UI up to date + /// List of actions detailing what would and wouldn't change + public IEnumerable Report(string folder, IEnumerable handlerAliases, uSyncCallbacks? callbacks) + { + var handlers = _handlerFactory.GetDefaultHandlers(handlerAliases); + return Report(folder, handlers, callbacks); + } - _logger.LogDebug("Reporting For [{handlers}]", string.Join(",", handlers.Select(x => x.Handler.Name))); + /// + /// Report the changes for a folder + /// + /// Folder to run the report for + /// List of SyncHandlers to use for the report + /// Callback functions to keep UI up to date + /// List of actions detailing what would and wouldn't change + public IEnumerable Report(string folder, IEnumerable handlers, uSyncCallbacks? callbacks) + { - var actions = new List(); + var sw = Stopwatch.StartNew(); - var summary = new SyncProgressSummary(handlers.Select(x => x.Handler), "Reporting", handlers.Count()); + _mutexService.FireBulkStarting(new uSyncReportStartingNotification()); - foreach (var configuredHandler in handlers) - { - var handler = configuredHandler.Handler; - var handlerSettings = configuredHandler.Settings; + _logger.LogDebug("Reporting For [{handlers}]", string.Join(",", handlers.Select(x => x.Handler.Name))); - summary.Increment(); + var actions = new List(); - summary.UpdateHandler(handler.Name, HandlerStatus.Processing, $"Reporting {handler.Name}", 0); + var summary = new SyncProgressSummary(handlers.Select(x => x.Handler), "Reporting", handlers.Count()); - callbacks?.Callback?.Invoke(summary); + foreach (var configuredHandler in handlers) + { + var handler = configuredHandler.Handler; + var handlerSettings = configuredHandler.Settings; - var handlerActions = handler.Report($"{folder}/{handler.DefaultFolder}", handlerSettings, callbacks?.Update); - actions.AddRange(handlerActions); + summary.Increment(); - summary.UpdateHandler(handler.Name, HandlerStatus.Complete, - handlerActions.CountChanges(), - handlerActions.ContainsErrors()); - } + summary.UpdateHandler(handler.Name, HandlerStatus.Processing, $"Reporting {handler.Name}", 0); - summary.UpdateMessage("Report Complete"); callbacks?.Callback?.Invoke(summary); + var handlerActions = handler.Report($"{folder}/{handler.DefaultFolder}", handlerSettings, callbacks?.Update); + actions.AddRange(handlerActions); - _mutexService.FireBulkComplete(new uSyncReportCompletedNotification(actions)); - sw.Stop(); + summary.UpdateHandler(handler.Name, HandlerStatus.Complete, + handlerActions.CountChanges(), + handlerActions.ContainsErrors()); + } - _logger.LogInformation("uSync Report: {handlerCount} handlers, processed {itemCount} items, {changeCount} changes in {ElapsedMilliseconds}ms", - handlers.Count(), actions.Count, - actions.CountChanges(), - sw.ElapsedMilliseconds); + summary.UpdateMessage("Report Complete"); + callbacks?.Callback?.Invoke(summary); - callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); - return actions; - } + _mutexService.FireBulkComplete(new uSyncReportCompletedNotification(actions)); + sw.Stop(); - #endregion - - #region Importing - private static object _importLock = new object(); - - /// - /// Import items into Umbraco from a given folder - /// - /// Folder to use for the import - /// Push changes in even if there is no difference between the file and the item in Umbraco - /// Handler options to use (used to calculate handlers to use) - /// Callbacks to keep UI informed - /// List of actions detailing what did and didn't change - [Obsolete("call import with the folder array to utilize root functionality.")] - public IEnumerable Import(string folder, bool force, SyncHandlerOptions handlerOptions, uSyncCallbacks callbacks = null) - => Import([folder], force, handlerOptions, callbacks); - - /// - /// Import items into Umbraco from a given set of folders - /// - public IEnumerable Import(string[] folders, bool force, SyncHandlerOptions handlerOptions, uSyncCallbacks callbacks = null) - { - handlerOptions ??= new SyncHandlerOptions(); - handlerOptions.Action = HandlerActions.Import; + _logger.LogInformation("uSync Report: {handlerCount} handlers, processed {itemCount} items, {changeCount} changes in {ElapsedMilliseconds}ms", + handlers.Count(), actions.Count, + actions.CountChanges(), + sw.ElapsedMilliseconds); - var handlers = _handlerFactory.GetValidHandlers(handlerOptions); - return Import(folders, force, handlers, callbacks); + callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); - } - /// - /// Import items into Umbraco from a given folder - /// - /// Folder to use for the import - /// Push changes in even if there is no difference between the file and the item in Umbraco - /// List of aliases for the handlers you want to use - /// Callbacks to keep UI informed - /// List of actions detailing what did and didn't change - [Obsolete("call import with the folder array to utilize root functionality.")] - public IEnumerable Import(string folder, bool force, IEnumerable handlerAliases, uSyncCallbacks callbacks) - { - var handlers = _handlerFactory.GetDefaultHandlers(handlerAliases); - return Import([folder], force, handlers, callbacks); - } + return actions; + } + + #endregion + + #region Importing + private static object _importLock = new object(); + + /// + /// Import items into Umbraco from a given folder + /// + /// Folder to use for the import + /// Push changes in even if there is no difference between the file and the item in Umbraco + /// Handler options to use (used to calculate handlers to use) + /// Callbacks to keep UI informed + /// List of actions detailing what did and didn't change + [Obsolete("call import with the folder array to utilize root functionality.")] + public IEnumerable Import(string folder, bool force, SyncHandlerOptions handlerOptions, uSyncCallbacks? callbacks = null) + => Import([folder], force, handlerOptions, callbacks); + + /// + /// Import items into Umbraco from a given set of folders + /// + public IEnumerable Import(string[] folders, bool force, SyncHandlerOptions handlerOptions, uSyncCallbacks? callbacks = null) + { + handlerOptions ??= new SyncHandlerOptions(); + handlerOptions.Action = HandlerActions.Import; + + var handlers = _handlerFactory.GetValidHandlers(handlerOptions); + return Import(folders, force, handlers, callbacks); + + } + /// + /// Import items into Umbraco from a given folder + /// + /// Folder to use for the import + /// Push changes in even if there is no difference between the file and the item in Umbraco + /// List of aliases for the handlers you want to use + /// Callbacks to keep UI informed + /// List of actions detailing what did and didn't change + [Obsolete("call import with the folder array to utilize root functionality.")] + public IEnumerable Import(string folder, bool force, IEnumerable handlerAliases, uSyncCallbacks? callbacks) + { + var handlers = _handlerFactory.GetDefaultHandlers(handlerAliases); + return Import([folder], force, handlers, callbacks); + } + + /// + /// Import items into Umbraco from a given folder + /// + /// Folder to use for the import + /// Push changes in even if there is no difference between the file and the item in Umbraco + /// List of Handlers & config to use for import + /// Callbacks to keep UI informed + /// List of actions detailing what did and didn't change + [Obsolete("we import multiple folders now, will be removed in v15")] + public IEnumerable Import(string folder, bool force, IEnumerable handlers, uSyncCallbacks? callbacks) + => Import([folder], force, handlers, callbacks); + + /// + /// Import items into Umbraco from a given set of folders + /// + /// List of actions detailing what did and didn't change + public IEnumerable Import(string[] folders, bool force, IEnumerable handlers, uSyncCallbacks? callbacks) + { + // if its blank, we just throw it back empty. + if (handlers == null || !handlers.Any()) return Enumerable.Empty(); - /// - /// Import items into Umbraco from a given folder - /// - /// Folder to use for the import - /// Push changes in even if there is no difference between the file and the item in Umbraco - /// List of Handlers & config to use for import - /// Callbacks to keep UI informed - /// List of actions detailing what did and didn't change - [Obsolete("we import multiple folders now, will be removed in v15")] - public IEnumerable Import(string folder, bool force, IEnumerable handlers, uSyncCallbacks callbacks) - => Import([folder], force, handlers, callbacks); - - /// - /// Import items into Umbraco from a given set of folders - /// - /// List of actions detailing what did and didn't change - public IEnumerable Import(string[] folders, bool force, IEnumerable handlers, uSyncCallbacks callbacks) + lock (_importLock) { - // if its blank, we just throw it back empty. - if (handlers == null || !handlers.Any()) return Enumerable.Empty(); + var sw = Stopwatch.StartNew(); - lock (_importLock) + using (var pause = _mutexService.ImportPause(true)) { - var sw = Stopwatch.StartNew(); - using (var pause = _mutexService.ImportPause(true)) - { - - // pre import event - _mutexService.FireBulkStarting(new uSyncImportStartingNotification()); + // pre import event + _mutexService.FireBulkStarting(new uSyncImportStartingNotification()); - var actions = new List(); + var actions = new List(); - var summary = new SyncProgressSummary(handlers.Select(x => x.Handler), "Importing", handlers.Count() + 1); - summary.Handlers.Add(new SyncHandlerSummary() - { - Icon = "icon-defrag", - Name = "Post Import", - Status = HandlerStatus.Pending - }); + var summary = new SyncProgressSummary(handlers.Select(x => x.Handler), "Importing", handlers.Count() + 1); + summary.Handlers.Add(new SyncHandlerSummary() + { + Icon = "icon-defrag", + Name = "Post Import", + Status = HandlerStatus.Pending + }); - var importOptions = new uSyncImportOptions - { - Flags = force ? SerializerFlags.Force : SerializerFlags.None, - Callbacks = callbacks - }; + var importOptions = new uSyncImportOptions + { + Flags = force ? SerializerFlags.Force : SerializerFlags.None, + Callbacks = callbacks + }; - foreach (var configuredHandler in handlers) - { - var handler = configuredHandler.Handler; - var handlerSettings = configuredHandler.Settings; + foreach (var configuredHandler in handlers) + { + var handler = configuredHandler.Handler; + var handlerSettings = configuredHandler.Settings; - summary.Increment(); + summary.Increment(); - summary.UpdateHandler( - handler.Name, HandlerStatus.Processing, $"Importing {handler.Name}", 0); + summary.UpdateHandler( + handler.Name, HandlerStatus.Processing, $"Importing {handler.Name}", 0); - callbacks?.Callback?.Invoke(summary); + callbacks?.Callback?.Invoke(summary); - var handlerFolders = folders.Select(x => $"{x}/{handler.DefaultFolder}").ToArray(); - var handlerActions = handler.ImportAll(handlerFolders, handlerSettings, importOptions); + var handlerFolders = folders.Select(x => $"{x}/{handler.DefaultFolder}").ToArray(); + var handlerActions = handler.ImportAll(handlerFolders, handlerSettings, importOptions); - actions.AddRange(handlerActions); + actions.AddRange(handlerActions); - summary.UpdateHandler(handler.Name, HandlerStatus.Complete, - handlerActions.CountChanges(), - handlerActions.ContainsErrors()); + summary.UpdateHandler(handler.Name, HandlerStatus.Complete, + handlerActions.CountChanges(), + handlerActions.ContainsErrors()); - } + } - // postImport things (mainly cleaning up folders) + // postImport things (mainly cleaning up folders) - summary.Increment(); - summary.UpdateHandler("Post Import", HandlerStatus.Pending, "Post Import Actions", 0); + summary.Increment(); + summary.UpdateHandler("Post Import", HandlerStatus.Pending, "Post Import Actions", 0); - callbacks?.Callback?.Invoke(summary); + callbacks?.Callback?.Invoke(summary); - actions.AddRange(PerformPostImport(handlers, actions)); + actions.AddRange(PerformPostImport(handlers, actions)); - sw.Stop(); - summary.UpdateHandler("Post Import", HandlerStatus.Complete, "Import Completed", 0); - callbacks?.Callback?.Invoke(summary); + sw.Stop(); + summary.UpdateHandler("Post Import", HandlerStatus.Complete, "Import Completed", 0); + callbacks?.Callback?.Invoke(summary); - // fire complete - _mutexService.FireBulkComplete(new uSyncImportCompletedNotification(actions)); + // fire complete + _mutexService.FireBulkComplete(new uSyncImportCompletedNotification(actions)); - _logger.LogInformation("uSync Import: {handlerCount} handlers, processed {itemCount} items, {changeCount} changes in {ElapsedMilliseconds}ms", - handlers.Count(), - actions.Count, - actions.CountChanges(), - sw.ElapsedMilliseconds); + _logger.LogInformation("uSync Import: {handlerCount} handlers, processed {itemCount} items, {changeCount} changes in {ElapsedMilliseconds}ms", + handlers.Count(), + actions.Count, + actions.CountChanges(), + sw.ElapsedMilliseconds); - callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); + callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); - return actions; - } + return actions; } } + } - private static IEnumerable PerformPostImport(IEnumerable handlers, IEnumerable actions) - { - var postImportActions = actions.Where(x => x.Success && x.Change > Core.ChangeType.NoChange && x.RequiresPostProcessing).ToList(); - if (postImportActions.Count == 0) return Enumerable.Empty(); + private static IEnumerable PerformPostImport(IEnumerable handlers, IEnumerable actions) + { + var postImportActions = actions.Where(x => x.Success && x.Change > Core.ChangeType.NoChange && x.RequiresPostProcessing).ToList(); + if (postImportActions.Count == 0) return Enumerable.Empty(); - var results = new List(); + var results = new List(); - foreach (var handlerPair in handlers) + foreach (var handlerPair in handlers) + { + if (handlerPair.Handler is ISyncPostImportHandler postImportHandler) { - if (handlerPair.Handler is ISyncPostImportHandler postImportHandler) - { - var handlerActions = postImportActions.Where(x => x.ItemType == handlerPair.Handler.ItemType); - if (handlerActions.Any() == false) continue; + var handlerActions = postImportActions.Where(x => x.ItemType == handlerPair.Handler.ItemType); + if (handlerActions.Any() == false) continue; - results.AddRange(postImportHandler.ProcessPostImport(handlerActions, handlerPair.Settings)); - } + results.AddRange(postImportHandler.ProcessPostImport(handlerActions, handlerPair.Settings)); } - - return results; } + return results; + } - /// - /// Import a single item based on a uSyncAction item - /// - /// - /// Importing a single item based on an action, the action will - /// detail what handler to use and the filename of the item to import - /// - /// Action item, to use as basis for import - /// Action detailing change or not - public uSyncAction ImportSingleAction(uSyncAction action) - { - var handlerConfig = _handlerFactory.GetValidHandler(action.HandlerAlias); - if (handlerConfig != null) - { - return handlerConfig.Handler - .Import(action.FileName, handlerConfig.Settings, true) - .FirstOrDefault(); - } + /// + /// Import a single item based on a uSyncAction item + /// + /// + /// Importing a single item based on an action, the action will + /// detail what handler to use and the filename of the item to import + /// + /// Action item, to use as basis for import + /// Action detailing change or not + public uSyncAction ImportSingleAction(uSyncAction action) + { + if (action.HandlerAlias is null || action.FileName is null) return new(); - return new uSyncAction(); + var handlerConfig = _handlerFactory.GetValidHandler(action.HandlerAlias); + if (handlerConfig != null) + { + return handlerConfig.Handler + .Import(action.FileName, handlerConfig.Settings, true) + .FirstOrDefault(); } - #endregion + return new uSyncAction(); + } + + #endregion - #region Exporting + #region Exporting - /// - /// Remove all the files from an export folder - /// - public bool CleanExportFolder(string folder) + /// + /// Remove all the files from an export folder + /// + public bool CleanExportFolder(string folder) + { + try { - try - { - if (_syncFileService.DirectoryExists(folder)) - _syncFileService.CleanFolder(folder); - } - catch (Exception ex) - { - throw new ApplicationException("Failed to delete uSync folder (may be in use)", ex); - } - - return true; + if (_syncFileService.DirectoryExists(folder)) + _syncFileService.CleanFolder(folder); } + catch (Exception ex) + { + throw new ApplicationException("Failed to delete uSync folder (may be in use)", ex); + } + + return true; + } - /// - /// Export items from Umbraco into a given folder - /// - /// folder to place items - /// Handler options to use when loading handlers - /// callback functions to update the UI - /// List of actions detailing what was exported - public IEnumerable Export(string folder, SyncHandlerOptions handlerOptions, uSyncCallbacks callbacks = null) - { - handlerOptions ??= new SyncHandlerOptions(); - handlerOptions.Action = HandlerActions.Export; + /// + /// Export items from Umbraco into a given folder + /// + /// folder to place items + /// Handler options to use when loading handlers + /// callback functions to update the UI + /// List of actions detailing what was exported + public IEnumerable Export(string folder, SyncHandlerOptions handlerOptions, uSyncCallbacks? callbacks = null) + { + handlerOptions ??= new SyncHandlerOptions(); + handlerOptions.Action = HandlerActions.Export; - var handlers = _handlerFactory.GetValidHandlers(handlerOptions); + var handlers = _handlerFactory.GetValidHandlers(handlerOptions); - WriteVersionFile(folder); + WriteVersionFile(folder); - return Export(folder, handlers, callbacks); - } + return Export(folder, handlers, callbacks); + } - /// - /// checks all the possible folders for the version file - /// - public bool CheckVersionFile(string[] folders) + /// + /// checks all the possible folders for the version file + /// + public bool CheckVersionFile(string[] folders) + { + foreach (var folder in folders.Reverse()) { - foreach(var folder in folders.Reverse()) - { - if (CheckVersionFile(folder)) - return true; - } + if (CheckVersionFile(folder)) + return true; + } + return false; + } + + /// + /// Check the uSync version file (in the root) to see if we are importing up to date files + /// + public bool CheckVersionFile(string folder) + { + var versionFile = Path.Combine(_syncFileService.GetAbsPath(folder), $"usync.{_uSyncConfig.Settings.DefaultExtension}"); + + if (!_syncFileService.FileExists(versionFile)) + { return false; } - - /// - /// Check the uSync version file (in the root) to see if we are importing up to date files - /// - public bool CheckVersionFile(string folder) + else { - var versionFile = Path.Combine(_syncFileService.GetAbsPath(folder), $"usync.{_uSyncConfig.Settings.DefaultExtension}"); - - if (!_syncFileService.FileExists(versionFile)) - { - return false; - } - else + try { - try + var node = _syncFileService.LoadXElement(versionFile); + var format = node.Attribute("format").ValueOrDefault(""); + if (!format.InvariantEquals(Core.uSyncConstants.FormatVersion)) { - var node = _syncFileService.LoadXElement(versionFile); - var format = node.Attribute("format").ValueOrDefault(""); - if (!format.InvariantEquals(Core.uSyncConstants.FormatVersion)) + var expectedVersion = SemVersion.Parse(Core.uSyncConstants.FormatVersion); + if (SemVersion.TryParse(format, out SemVersion? current) && current is not null) { - var expectedVersion = SemVersion.Parse(Core.uSyncConstants.FormatVersion); - if (SemVersion.TryParse(format, out SemVersion current)) - { - if (current.CompareTo(expectedVersion) >= 0) return true; - } - - return false; + if (current.CompareTo(expectedVersion) >= 0) return true; } - } - catch - { + return false; } } - - return true; + catch + { + return false; + } } - private void WriteVersionFile(string folder) - { - try - { - var versionFile = Path.Combine(_syncFileService.GetAbsPath(folder), $"usync.{_uSyncConfig.Settings.DefaultExtension}"); - var versionNode = new XElement("uSync", - new XAttribute("version", typeof(uSync).Assembly.GetName().Version.ToString()), - new XAttribute("format", Core.uSyncConstants.FormatVersion)); - // remove date, we don't really care, and it causes unnecessary git changes. + return true; + } - Directory.CreateDirectory(Path.GetDirectoryName(versionFile)); + private void WriteVersionFile(string folder) + { + try + { + var versionFile = Path.Combine(_syncFileService.GetAbsPath(folder), $"usync.{_uSyncConfig.Settings.DefaultExtension}"); + var versionNode = new XElement("uSync", + new XAttribute("version", typeof(uSync).Assembly.GetName()?.Version?.ToString() ?? "14.0.0"), + new XAttribute("format", Core.uSyncConstants.FormatVersion)); + // remove date, we don't really care, and it causes unnecessary git changes. - versionNode.Save(versionFile); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Issue saving the usync.config file in the root of {folder}", folder); - } + _syncFileService.CreateFoldersForFile(versionFile); + _syncFileService.SaveXElement(versionNode, versionFile); } - - /// - /// Export items from Umbraco into a given folder - /// - /// folder to place items - /// aliases for the handlers to use while exporting - /// callback functions to update the UI - /// List of actions detailing what was exported - public IEnumerable Export(string folder, IEnumerable handlerAliases, uSyncCallbacks callbacks) + catch (Exception ex) { - var handlers = _handlerFactory.GetDefaultHandlers(handlerAliases); - return Export(folder, handlers, callbacks); + _logger.LogWarning(ex, "Issue saving the usync.config file in the root of {folder}", folder); } + } - /// - /// Export items from Umbraco into a given folder - /// - /// folder to place items - /// Handler config pairs - /// callback functions to update the UI - /// List of actions detailing what was exported - public IEnumerable Export(string folder, IEnumerable handlers, uSyncCallbacks callbacks) - { - var sw = Stopwatch.StartNew(); + /// + /// Export items from Umbraco into a given folder + /// + /// folder to place items + /// aliases for the handlers to use while exporting + /// callback functions to update the UI + /// List of actions detailing what was exported + public IEnumerable Export(string folder, IEnumerable handlerAliases, uSyncCallbacks? callbacks) + { + var handlers = _handlerFactory.GetDefaultHandlers(handlerAliases); + return Export(folder, handlers, callbacks); + } - _mutexService.FireBulkStarting(new uSyncExportStartingNotification()); + /// + /// Export items from Umbraco into a given folder + /// + /// folder to place items + /// Handler config pairs + /// callback functions to update the UI + /// List of actions detailing what was exported + public IEnumerable Export(string folder, IEnumerable handlers, uSyncCallbacks? callbacks) + { + var sw = Stopwatch.StartNew(); - var actions = new List(); - var summary = new SyncProgressSummary(handlers.Select(x => x.Handler), "Exporting", handlers.Count()); + _mutexService.FireBulkStarting(new uSyncExportStartingNotification()); - foreach (var configuredHandler in handlers) - { - var handler = configuredHandler.Handler; + var actions = new List(); + var summary = new SyncProgressSummary(handlers.Select(x => x.Handler), "Exporting", handlers.Count()); - summary.Increment(); - summary.UpdateHandler( - handler.Name, HandlerStatus.Processing, $"Exporting {handler.Name}", 0); + foreach (var configuredHandler in handlers) + { + var handler = configuredHandler.Handler; - callbacks?.Callback?.Invoke(summary); + summary.Increment(); + summary.UpdateHandler( + handler.Name, HandlerStatus.Processing, $"Exporting {handler.Name}", 0); - var handlerActions = handler.ExportAll($"{folder}/{handler.DefaultFolder}", configuredHandler.Settings, callbacks?.Update); + callbacks?.Callback?.Invoke(summary); - actions.AddRange(handlerActions); + var handlerActions = handler.ExportAll($"{folder}/{handler.DefaultFolder}", configuredHandler.Settings, callbacks?.Update); - summary.UpdateHandler(handler.Name, HandlerStatus.Complete, - handlerActions.CountChanges(), - handlerActions.ContainsErrors()); - } + actions.AddRange(handlerActions); + summary.UpdateHandler(handler.Name, HandlerStatus.Complete, + handlerActions.CountChanges(), + handlerActions.ContainsErrors()); + } - summary.UpdateMessage("Export Completed"); - callbacks?.Callback?.Invoke(summary); - _mutexService.FireBulkComplete(new uSyncExportCompletedNotification(actions)); + summary.UpdateMessage("Export Completed"); + callbacks?.Callback?.Invoke(summary); - sw.Stop(); + _mutexService.FireBulkComplete(new uSyncExportCompletedNotification(actions)); - _logger.LogInformation("uSync Export: {handlerCount} handlers, processed {itemCount} items, {changeCount} changes in {ElapsedMilliseconds}ms", - handlers.Count(), actions.Count, - actions.CountChanges(), - sw.ElapsedMilliseconds); + sw.Stop(); - callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); + _logger.LogInformation("uSync Export: {handlerCount} handlers, processed {itemCount} items, {changeCount} changes in {ElapsedMilliseconds}ms", + handlers.Count(), actions.Count, + actions.CountChanges(), + sw.ElapsedMilliseconds); - return actions; - } + callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); - #endregion + return actions; + } + + #endregion - /// - /// Do an import triggered by an event. - /// - /// - private void USyncTriggers_DoImport(uSyncTriggerArgs e) + /// + /// Do an import triggered by an event. + /// + /// + private void USyncTriggers_DoImport(uSyncTriggerArgs e) + { + if (e.EntityTypes != null && !string.IsNullOrWhiteSpace(e.Folder)) { - if (e.EntityTypes != null && !string.IsNullOrWhiteSpace(e.Folder)) - { - _logger.LogInformation("Import Triggered by downlevel change {folder}", e.Folder); + _logger.LogInformation("Import Triggered by downlevel change {folder}", e.Folder); - var handlers = _handlerFactory - .GetValidHandlersByEntityType(e.EntityTypes, e.HandlerOptions); + var handlers = _handlerFactory + .GetValidHandlersByEntityType(e.EntityTypes, e.HandlerOptions); - if (handlers.Any()) this.Import(e.Folder, false, handlers, null); - } + if (handlers.Any()) this.Import(e.Folder, false, handlers, null); } + } - /// - /// do an export triggered by events. - /// - /// - private void USyncTriggers_DoExport(uSyncTriggerArgs e) + /// + /// do an export triggered by events. + /// + /// + private void USyncTriggers_DoExport(uSyncTriggerArgs e) + { + if (e.EntityTypes != null && !string.IsNullOrWhiteSpace(e.Folder)) { - if (e.EntityTypes != null && !string.IsNullOrWhiteSpace(e.Folder)) - { - _logger.LogInformation("Export Triggered by downlevel change {folder}", e.Folder); + _logger.LogInformation("Export Triggered by downlevel change {folder}", e.Folder); - var handlers = _handlerFactory - .GetValidHandlersByEntityType(e.EntityTypes, e.HandlerOptions); + var handlers = _handlerFactory + .GetValidHandlersByEntityType(e.EntityTypes, e.HandlerOptions); - if (handlers.Any()) this.Export(e.Folder, handlers, null); - } + if (handlers.Any()) this.Export(e.Folder, handlers, null); } } } diff --git a/uSync.BackOffice/Services/uSyncService_Files.cs b/uSync.BackOffice/Services/uSyncService_Files.cs index 5cb2ddc8..9174b644 100644 --- a/uSync.BackOffice/Services/uSyncService_Files.cs +++ b/uSync.BackOffice/Services/uSyncService_Files.cs @@ -65,7 +65,7 @@ public void DeCompressFile(string zipArchive, string target) var destinationFolder = Path.GetDirectoryName(destination); - if (!Directory.Exists(destinationFolder)) + if (destinationFolder is not null && Directory.Exists(destinationFolder) is false) Directory.CreateDirectory(destinationFolder); entry.ExtractToFile(destination, true); @@ -100,7 +100,7 @@ private string GetRelativePath(string root, string file) private string GetOSDependentPath(string file) => file.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) .Trim(Path.DirectorySeparatorChar); - + private string CleanPathForZip(string path) => Path.GetFullPath( path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)) diff --git a/uSync.BackOffice/Services/uSyncService_Handlers.cs b/uSync.BackOffice/Services/uSyncService_Handlers.cs index 69389ffa..1779cdb9 100644 --- a/uSync.BackOffice/Services/uSyncService_Handlers.cs +++ b/uSync.BackOffice/Services/uSyncService_Handlers.cs @@ -10,158 +10,157 @@ using uSync.BackOffice.Models; using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// actions on individual handlers. +/// + +public partial class uSyncService { /// - /// actions on individual handlers. + /// Run a report for a given handler /// - - public partial class uSyncService + public IEnumerable ReportHandler(string handler, uSyncImportOptions options) { - /// - /// Run a report for a given handler - /// - public IEnumerable ReportHandler(string handler, uSyncImportOptions options) + var handlerPair = _handlerFactory.GetValidHandler(handler, new SyncHandlerOptions { - var handlerPair = _handlerFactory.GetValidHandler(handler, new SyncHandlerOptions - { - Set = options.HandlerSet, - Action = HandlerActions.Report - }); + Set = options.HandlerSet, + Action = HandlerActions.Report + }); - if (handlerPair == null) return Enumerable.Empty(); - var folders = GetHandlerFolders(options.Folders, handlerPair.Handler); + if (handlerPair == null) return Enumerable.Empty(); + var folders = GetHandlerFolders(options.Folders, handlerPair.Handler); - return handlerPair.Handler.Report(folders, handlerPair.Settings, options.Callbacks?.Update); - } + return handlerPair.Handler.Report(folders, handlerPair.Settings, options.Callbacks?.Update); + } - /// - /// run an import for a given handler - /// - public IEnumerable ImportHandler(string handlerAlias, uSyncImportOptions options) + /// + /// run an import for a given handler + /// + public IEnumerable ImportHandler(string handlerAlias, uSyncImportOptions options) + { + lock (_importLock) { - lock (_importLock) + using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) { - using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) + var handlerPair = _handlerFactory.GetValidHandler(handlerAlias, new SyncHandlerOptions { - var handlerPair = _handlerFactory.GetValidHandler(handlerAlias, new SyncHandlerOptions - { - Set = options.HandlerSet, - Action = HandlerActions.Import - }); - - if (handlerPair == null) return Enumerable.Empty(); - var folders = GetHandlerFolders(options.Folders, handlerPair.Handler); - - _logger.LogDebug("> Import Handler {handler}", handlerAlias); - - using var scope = _scopeProvider.CreateNotificationScope( - eventAggregator: _eventAggregator, - loggerFactory: _loggerFactory, - syncConfigService: _uSyncConfig, - syncEventService: _mutexService, - backgroundTaskQueue: _backgroundTaskQueue, - options.Callbacks?.Update); - - var results = handlerPair.Handler.ImportAll(folders, handlerPair.Settings, options); - - _logger.LogDebug("< Import Handler {handler}", handlerAlias); - - scope.Complete(); - - return results; - } + Set = options.HandlerSet, + Action = HandlerActions.Import + }); + + if (handlerPair == null) return Enumerable.Empty(); + var folders = GetHandlerFolders(options.Folders, handlerPair.Handler); + + _logger.LogDebug("> Import Handler {handler}", handlerAlias); + + using var scope = _scopeProvider.CreateNotificationScope( + eventAggregator: _eventAggregator, + loggerFactory: _loggerFactory, + syncConfigService: _uSyncConfig, + syncEventService: _mutexService, + backgroundTaskQueue: _backgroundTaskQueue, + options.Callbacks?.Update); + + var results = handlerPair.Handler.ImportAll(folders, handlerPair.Settings, options); + + _logger.LogDebug("< Import Handler {handler}", handlerAlias); + + scope.Complete(); + + return results; } } + } - /// - /// perform the post import actions for a handler - /// - [Obsolete("Pass array of folders, will be removed in v15")] - public IEnumerable PerformPostImport(string rootFolder, string handlerSet, IEnumerable actions) - => PerformPostImport([rootFolder], handlerSet, actions); - - /// - /// perform the post import actions for a handler - /// - public IEnumerable PerformPostImport(string[] folders, string handlerSet, IEnumerable actions) + /// + /// perform the post import actions for a handler + /// + [Obsolete("Pass array of folders, will be removed in v15")] + public IEnumerable PerformPostImport(string rootFolder, string handlerSet, IEnumerable actions) + => PerformPostImport([rootFolder], handlerSet, actions); + + /// + /// perform the post import actions for a handler + /// + public IEnumerable PerformPostImport(string[] folders, string handlerSet, IEnumerable actions) + { + lock (_importLock) { - lock (_importLock) + using (var pause = _mutexService.ImportPause(true)) { - using (var pause = _mutexService.ImportPause(true)) - { - var handlers = _handlerFactory.GetValidHandlers(new SyncHandlerOptions { Set = handlerSet, Action = HandlerActions.Import }); - return PerformPostImport(handlers, actions); - } + var handlers = _handlerFactory.GetValidHandlers(new SyncHandlerOptions { Set = handlerSet, Action = HandlerActions.Import }); + return PerformPostImport(handlers, actions); } } + } - /// - /// run an export for a given handler - /// - public IEnumerable ExportHandler(string handler, uSyncImportOptions options) + /// + /// run an export for a given handler + /// + public IEnumerable ExportHandler(string handler, uSyncImportOptions options) + { + var handlerPair = _handlerFactory.GetValidHandler(handler, new SyncHandlerOptions { - var handlerPair = _handlerFactory.GetValidHandler(handler, new SyncHandlerOptions - { - Set = options.HandlerSet, - Action = HandlerActions.Export - }); + Set = options.HandlerSet, + Action = HandlerActions.Export + }); - if (handlerPair == null) return Enumerable.Empty(); - var folders = GetHandlerFolders(options.Folders, handlerPair.Handler); - return handlerPair.Handler.ExportAll(folders, handlerPair.Settings, options.Callbacks?.Update); - } + if (handlerPair == null) return Enumerable.Empty(); + var folders = GetHandlerFolders(options.Folders, handlerPair.Handler); + return handlerPair.Handler.ExportAll(folders, handlerPair.Settings, options.Callbacks?.Update); + } - /// - /// Start a bulk run, fires events, and for exports writes the version file. - /// - public void StartBulkProcess(HandlerActions action) + /// + /// Start a bulk run, fires events, and for exports writes the version file. + /// + public void StartBulkProcess(HandlerActions action) + { + switch (action) { - switch (action) - { - case HandlerActions.Export: - _mutexService.FireBulkStarting(new uSyncExportStartingNotification()); - break; - case HandlerActions.Import: - // cleans any caches we might have set. - _appCache.ClearByKey("usync_"); - - _mutexService.FireBulkStarting(new uSyncImportStartingNotification()); - break; - case HandlerActions.Report: - _mutexService.FireBulkStarting(new uSyncReportStartingNotification()); - break; - } + case HandlerActions.Export: + _mutexService.FireBulkStarting(new uSyncExportStartingNotification()); + break; + case HandlerActions.Import: + // cleans any caches we might have set. + _appCache.ClearByKey("usync_"); + + _mutexService.FireBulkStarting(new uSyncImportStartingNotification()); + break; + case HandlerActions.Report: + _mutexService.FireBulkStarting(new uSyncReportStartingNotification()); + break; } + } - /// - /// Complete a bulk run, fire the event so other things know we have done it. - /// - public void FinishBulkProcess(HandlerActions action, IEnumerable actions) + /// + /// Complete a bulk run, fire the event so other things know we have done it. + /// + public void FinishBulkProcess(HandlerActions action, IEnumerable actions) + { + switch (action) { - switch (action) - { - case HandlerActions.Export: - WriteVersionFile(_uSyncConfig.GetRootFolder()); - _mutexService.FireBulkComplete(new uSyncExportCompletedNotification(actions)); - break; - case HandlerActions.Import: - _mutexService.FireBulkComplete(new uSyncImportCompletedNotification(actions)); - break; - case HandlerActions.Report: - _mutexService.FireBulkComplete(new uSyncReportCompletedNotification(actions)); - break; - } + case HandlerActions.Export: + WriteVersionFile(_uSyncConfig.GetRootFolder()); + _mutexService.FireBulkComplete(new uSyncExportCompletedNotification(actions)); + break; + case HandlerActions.Import: + _mutexService.FireBulkComplete(new uSyncImportCompletedNotification(actions)); + break; + case HandlerActions.Report: + _mutexService.FireBulkComplete(new uSyncReportCompletedNotification(actions)); + break; } + } - /// - /// gets the physical folder for a handler. ( root + handler folder) - /// - private static string GetHandlerFolder(string rootFolder, ISyncHandler handler) - => Path.Combine(rootFolder, handler.DefaultFolder); + /// + /// gets the physical folder for a handler. ( root + handler folder) + /// + private static string GetHandlerFolder(string rootFolder, ISyncHandler handler) + => Path.Combine(rootFolder, handler.DefaultFolder); - private static string[] GetHandlerFolders(string[] folders, ISyncHandler handler) - => folders.Select(x => GetHandlerFolder(x, handler)).ToArray(); + private static string[] GetHandlerFolders(string[] folders, ISyncHandler handler) + => folders.Select(x => GetHandlerFolder(x, handler)).ToArray(); - } } diff --git a/uSync.BackOffice/Services/uSyncService_Single.cs b/uSync.BackOffice/Services/uSyncService_Single.cs index a5a789e0..4dcd810a 100644 --- a/uSync.BackOffice/Services/uSyncService_Single.cs +++ b/uSync.BackOffice/Services/uSyncService_Single.cs @@ -16,399 +16,406 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice +namespace uSync.BackOffice; + + +/// +/// Implementation of paged import methods. +/// +public partial class uSyncService { + /// + /// Perform a paged report against a given folder + /// + [Obsolete("For better performance pass handler, will be removed in v15")] + public IEnumerable ReportPartial(string folder, uSyncPagedImportOptions options, out int total) + => ReportPartial([folder], options, out total); /// - /// Implementation of paged import methods. + /// Perform a paged report against a given folder /// - public partial class uSyncService + [Obsolete("For better performance pass handler, will be removed in v15")] + public IEnumerable ReportPartial(string[] folder, uSyncPagedImportOptions options, out int total) { - /// - /// Perform a paged report against a given folder - /// - [Obsolete("For better performance pass handler, will be removed in v15")] - public IEnumerable ReportPartial(string folder, uSyncPagedImportOptions options, out int total) - => ReportPartial([folder], options, out total); - - /// - /// Perform a paged report against a given folder - /// - [Obsolete("For better performance pass handler, will be removed in v15")] - public IEnumerable ReportPartial(string[] folder, uSyncPagedImportOptions options, out int total) - { - var orderedNodes = LoadOrderedNodes(folder); - return ReportPartial(orderedNodes, options, out 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; + /// + /// 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 actions = new List(); + var lastType = string.Empty; - var folder = Path.GetDirectoryName(orderedNodes.FirstOrDefault()?.FileName ?? options.Folders?.FirstOrDefault() ?? _uSyncConfig.GetRootFolder()); + var folder = Path.GetDirectoryName(orderedNodes.FirstOrDefault()?.FileName ?? options.Folders?.FirstOrDefault() ?? _uSyncConfig.GetRootFolder()) ?? string.Empty; - SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); + SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); - HandlerConfigPair handlerPair = null; + HandlerConfigPair? handlerPair = null; - var index = options.PageNumber * options.PageSize; + var index = options.PageNumber * options.PageSize; - foreach (var item in orderedNodes.Skip(options.PageNumber * options.PageSize).Take(options.PageSize)) + foreach (var item in orderedNodes.Skip(options.PageNumber * options.PageSize).Take(options.PageSize)) + { + var itemType = item.Node.GetItemType(); + if (!itemType.InvariantEquals(lastType)) { - var itemType = item.Node.GetItemType(); - if (!itemType.InvariantEquals(lastType)) - { - lastType = itemType; - handlerPair = _handlerFactory.GetValidHandlerByTypeName(itemType, syncHandlerOptions); + lastType = itemType; + handlerPair = _handlerFactory.GetValidHandlerByTypeName(itemType, syncHandlerOptions); - handlerPair?.Handler.PreCacheFolderKeys(folder, orderedNodes.Select(x => x.Key).ToList()); - } - - if (handlerPair == null) - { - _logger.LogWarning("No handler for {itemType} {alias}", itemType, item.Node.GetAlias()); - continue; - } + handlerPair?.Handler.PreCacheFolderKeys(folder, orderedNodes.Select(x => x.Key).ToList()); + } - options.Callbacks?.Update.Invoke(item.Node.GetAlias(), - CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); + if (handlerPair == null) + { + _logger.LogWarning("No handler for {itemType} {alias}", itemType, item.Node.GetAlias()); + continue; + } - if (handlerPair != null) - { - actions.AddRange(handlerPair.Handler.ReportElement(item.Node, item.FileName, handlerPair.Settings, options)); - } + options.Callbacks?.Update?.Invoke(item.Node.GetAlias(), + CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); - index++; + if (handlerPair != null) + { + actions.AddRange(handlerPair.Handler.ReportElement(item.Node, item.FileName, handlerPair.Settings, options)); } - return actions; + index++; } - /// - /// Perform a paged Import against a given folder - /// - [Obsolete("For better performance pass handler, will be removed in v15")] - public IEnumerable ImportPartial(string folder, uSyncPagedImportOptions options, out int total) - { - var orderedNodes = LoadOrderedNodes(folder); - return ImportPartial(orderedNodes, options, out total); - } + return actions; + } + + /// + /// Perform a paged Import against a given folder + /// + [Obsolete("For better performance pass handler, will be removed in v15")] + 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) + /// + /// perform an import of items from the suppled ordered node list. + /// + public IEnumerable ImportPartial(IList orderedNodes, uSyncPagedImportOptions options, out int total) + { + lock (_importLock) { - lock (_importLock) + using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) { - using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) - { - total = orderedNodes.Count; + total = orderedNodes.Count; - var actions = new List(); - var lastType = string.Empty; + var actions = new List(); + var lastType = string.Empty; - var range = options.ProgressMax - options.ProgressMin; + var range = options.ProgressMax - options.ProgressMin; - SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); + SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); - HandlerConfigPair handlerPair = null; + HandlerConfigPair? handlerPair = null; - var index = options.PageNumber * options.PageSize; + var index = options.PageNumber * options.PageSize; - using var scope = _scopeProvider.CreateNotificationScope( - eventAggregator: _eventAggregator, - loggerFactory: _loggerFactory, - syncConfigService: _uSyncConfig, - syncEventService: _mutexService, - backgroundTaskQueue: _backgroundTaskQueue, - options.Callbacks?.Update); + using var scope = _scopeProvider.CreateNotificationScope( + eventAggregator: _eventAggregator, + loggerFactory: _loggerFactory, + syncConfigService: _uSyncConfig, + syncEventService: _mutexService, + backgroundTaskQueue: _backgroundTaskQueue, + options.Callbacks?.Update); + { + try { - try + foreach (var item in orderedNodes.Skip(options.PageNumber * options.PageSize).Take(options.PageSize)) { - foreach (var item in orderedNodes.Skip(options.PageNumber * options.PageSize).Take(options.PageSize)) - { - var node = item.Node ?? XElement.Load(item.FileName); + var node = item.Node ?? XElement.Load(item.FileName); - var itemType = 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 (node.IsContent() && node.IsBlueprint()) - { - lastType = UdiEntityType.DocumentBlueprint; - handlerPair = _handlerFactory.GetValidHandlerByEntityType(UdiEntityType.DocumentBlueprint); - } - } + var itemType = node.GetItemType(); + if (!itemType.InvariantEquals(lastType)) + { + lastType = itemType; + handlerPair = _handlerFactory.GetValidHandlerByTypeName(itemType, syncHandlerOptions); - if (handlerPair == null) + // 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 (node.IsContent() && node.IsBlueprint()) { - _logger.LogWarning("No handler was found for {alias} item might not process correctly", itemType); - continue; + lastType = UdiEntityType.DocumentBlueprint; + handlerPair = _handlerFactory.GetValidHandlerByEntityType(UdiEntityType.DocumentBlueprint); } + } - options.Callbacks?.Update?.Invoke(node.GetAlias(), - CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); + if (handlerPair == null) + { + _logger.LogWarning("No handler was found for {alias} item might not process correctly", itemType); + continue; + } - if (handlerPair != null) - { - actions.AddRange(handlerPair.Handler.ImportElement(node, item.FileName, handlerPair.Settings, options)); - } + options.Callbacks?.Update?.Invoke(node.GetAlias(), + CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); - index++; + if (handlerPair != null) + { + actions.AddRange(handlerPair.Handler.ImportElement(node, item.FileName, handlerPair.Settings, options)); } - } - finally - { - scope.Complete(); - } + index++; + } + } + finally + { + scope.Complete(); } - return actions; } + + return actions; } } + } - /// - /// Perform a paged Import second pass against a given folder - /// - public IEnumerable ImportPartialSecondPass(IEnumerable actions, uSyncPagedImportOptions options) + /// + /// Perform a paged Import second pass against a given folder + /// + public IEnumerable ImportPartialSecondPass(IEnumerable actions, uSyncPagedImportOptions options) + { + lock (_importLock) { - lock (_importLock) + using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) { - using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) - { - SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); + SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); - var secondPassActions = new List(); + var secondPassActions = new List(); - var total = actions.Count(); + var total = actions.Count(); - var lastType = string.Empty; - HandlerConfigPair handlerPair = null; + var lastType = string.Empty; + HandlerConfigPair? handlerPair = null; - var index = options.PageNumber * options.PageSize; + var index = options.PageNumber * options.PageSize; - using (var scope = _scopeProvider.CreateNotificationScope( - eventAggregator: _eventAggregator, - loggerFactory: _loggerFactory, - syncConfigService: _uSyncConfig, - syncEventService: _mutexService, - backgroundTaskQueue: _backgroundTaskQueue, - options.Callbacks?.Update)) + using (var scope = _scopeProvider.CreateNotificationScope( + eventAggregator: _eventAggregator, + loggerFactory: _loggerFactory, + syncConfigService: _uSyncConfig, + syncEventService: _mutexService, + backgroundTaskQueue: _backgroundTaskQueue, + options.Callbacks?.Update)) + { + try { - try + foreach (var action in actions.Skip(options.PageNumber * options.PageSize).Take(options.PageSize)) { - foreach (var action in actions.Skip(options.PageNumber * options.PageSize).Take(options.PageSize)) + if (action.HandlerAlias is null) continue; + + if (!action.HandlerAlias.InvariantEquals(lastType)) { - if (!action.HandlerAlias.InvariantEquals(lastType)) - { - lastType = action.HandlerAlias; - handlerPair = _handlerFactory.GetValidHandler(action.HandlerAlias, syncHandlerOptions); - } + lastType = action.HandlerAlias; + handlerPair = _handlerFactory.GetValidHandler(action.HandlerAlias, syncHandlerOptions); + } - if (handlerPair == null) - { - _logger.LogWarning("No handler was found for {alias} item might not process correctly", action.HandlerAlias); - continue; - } + if (handlerPair == null) + { + _logger.LogWarning("No handler was found for {alias} item might not process correctly", action.HandlerAlias); + continue; + } - options.Callbacks?.Update?.Invoke($"Second Pass: {action.Name}", - CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); + options.Callbacks?.Update?.Invoke($"Second Pass: {action.Name}", + CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); - secondPassActions.AddRange(handlerPair.Handler.ImportSecondPass(action, handlerPair.Settings, options)); + secondPassActions.AddRange(handlerPair.Handler.ImportSecondPass(action, handlerPair.Settings, options)); - index++; - } - } - finally - { - scope.Complete(); + index++; } } - - return secondPassActions; + finally + { + scope.Complete(); + } } + + return secondPassActions; } } + } - /// - /// Perform a paged Import post import against a given folder - /// - public IEnumerable ImportPartialPostImport(IEnumerable actions, uSyncPagedImportOptions options) - { - if (actions == null || !actions.Any()) return Enumerable.Empty(); + /// + /// Perform a paged Import post import against a given folder + /// + public IEnumerable ImportPartialPostImport(IEnumerable actions, uSyncPagedImportOptions options) + { + if (actions == null || !actions.Any()) return Enumerable.Empty(); - lock (_importLock) + lock (_importLock) + { + using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) { - using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) - { - SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); + SyncHandlerOptions syncHandlerOptions = HandlerOptionsFromPaged(options); - var aliases = actions.Select(x => x.HandlerAlias).Distinct(); + var aliases = actions.Select(x => x.HandlerAlias).Distinct(); - var folders = actions - .Where(x => x.RequiresPostProcessing) - .Select(x => new { alias = x.HandlerAlias, folder = Path.GetDirectoryName(x.FileName), actions = x }) - .SafeDistinctBy(x => x.folder) - .GroupBy(x => x.alias) - .ToList(); + var folders = actions + .Where(x => x.RequiresPostProcessing) + .Select(x => new { alias = x.HandlerAlias, folder = Path.GetDirectoryName(x.FileName), actions = x }) + .DistinctBy(x => x.folder) + .GroupBy(x => x.alias) + .ToList(); - var results = new List(); + var results = new List(); - var index = 0; + var index = 0; - foreach (var actionItem in folders.SelectMany(actionGroup => actionGroup)) - { - var handlerPair = _handlerFactory.GetValidHandler(actionItem.alias, syncHandlerOptions); + foreach (var actionItem in folders.SelectMany(actionGroup => actionGroup)) + { + if (actionItem.alias is null) continue; - if (handlerPair == null) - { - _logger.LogWarning("No handler was found for {alias} item might not process correctly", actionItem.alias); - } - else + var handlerPair = _handlerFactory.GetValidHandler(actionItem.alias, syncHandlerOptions); + + if (handlerPair == null) + { + _logger.LogWarning("No handler was found for {alias} item might not process correctly", actionItem.alias); + } + else + { + if (handlerPair.Handler is ISyncPostImportHandler postImportHandler) { - if (handlerPair.Handler is ISyncPostImportHandler postImportHandler) - { - options.Callbacks?.Update?.Invoke(actionItem.alias, index, folders.Count); + options.Callbacks?.Update?.Invoke(actionItem.alias, index, folders.Count); - var handlerActions = actions.Where(x => x.HandlerAlias.InvariantEquals(handlerPair.Handler.Alias)); - results.AddRange(postImportHandler.ProcessPostImport(actionItem.folder, handlerActions, handlerPair.Settings)); - } + var handlerActions = actions.Where(x => x.HandlerAlias.InvariantEquals(handlerPair.Handler.Alias)); + results.AddRange(postImportHandler.ProcessPostImport(actionItem.folder ?? string.Empty, handlerActions, handlerPair.Settings)); } - - index++; } - return results; + index++; } + + return results; } } + } - /// - /// Perform a paged Clean after import for a given folder - /// - public IEnumerable ImportPostCleanFiles(IEnumerable actions, uSyncPagedImportOptions options) - { - if (actions == null) return Enumerable.Empty(); + /// + /// Perform a paged Clean after import for a given folder + /// + public IEnumerable ImportPostCleanFiles(IEnumerable actions, uSyncPagedImportOptions options) + { + if (actions == null) return Enumerable.Empty(); - lock (_importLock) + lock (_importLock) + { + using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) { - using (var pause = _mutexService.ImportPause(options.PauseDuringImport)) - { - SyncHandlerOptions syncHandlerOptions = new SyncHandlerOptions( - options.HandlerSet, options.UserId); + SyncHandlerOptions syncHandlerOptions = new SyncHandlerOptions( + options.HandlerSet, options.UserId); + + var cleans = actions + .Where(x => x.Change == ChangeType.Clean && !string.IsNullOrWhiteSpace(x.FileName)) + .Select(x => new { alias = x.HandlerAlias, folder = Path.GetDirectoryName(x.FileName), actions = x }) + .DistinctBy(x => x.folder) + .GroupBy(x => x.alias) + .ToList(); - var cleans = actions - .Where(x => x.Change == ChangeType.Clean && !string.IsNullOrWhiteSpace(x.FileName)) - .Select(x => new { alias = x.HandlerAlias, folder = Path.GetDirectoryName(x.FileName), actions = x }) - .SafeDistinctBy(x => x.folder) - .GroupBy(x => x.alias) - .ToList(); + var results = new List(); - var results = new List(); + var index = 0; + + foreach (var actionItem in cleans.SelectMany(actionGroup => actionGroup)) + { + if (actionItem.alias is null) continue; - var index = 0; + var handlerPair = _handlerFactory.GetValidHandler(actionItem.alias, syncHandlerOptions); + if (handlerPair is null) continue; - foreach (var actionItem in cleans.SelectMany(actionGroup => actionGroup)) + if (handlerPair.Handler is ISyncCleanEntryHandler cleanEntryHandler) { - var handlerPair = _handlerFactory.GetValidHandler(actionItem.alias, syncHandlerOptions); - if (handlerPair.Handler is ISyncCleanEntryHandler cleanEntryHandler) - { - options.Callbacks?.Update?.Invoke(actionItem.alias, index, cleans.Count); + options.Callbacks?.Update?.Invoke(actionItem.alias, index, cleans.Count); - var handlerActions = actions.Where(x => x.HandlerAlias.InvariantEquals(handlerPair.Handler.Alias)); - results.AddRange(cleanEntryHandler.ProcessCleanActions(actionItem.folder, handlerActions, handlerPair.Settings)); - } - index++; + var handlerActions = actions.Where(x => x.HandlerAlias.InvariantEquals(handlerPair.Handler.Alias)); + results.AddRange(cleanEntryHandler.ProcessCleanActions(actionItem.folder, handlerActions, handlerPair.Settings)); } - - return results; + index++; } + + return results; } } + } - private SyncHandlerOptions HandlerOptionsFromPaged(uSyncPagedImportOptions options) - => new SyncHandlerOptions(options.HandlerSet, options.UserId) - { - IncludeDisabled = options.IncludeDisabledHandlers - }; - - /// - /// Load the xml in a folder in level order so we process the higher level items first. - /// - [Obsolete("use handler and multiple folder method will be removed in v15")] - public IList LoadOrderedNodes(string folder) - => LoadOrderedNodes([folder]).ToList(); - - /// - /// Load the xml in a folder in level order so we process the higher level items first. - /// - [Obsolete("use handler and multiple folder method will be removed in v15")] - public IList LoadOrderedNodes(string[] folders) + private SyncHandlerOptions HandlerOptionsFromPaged(uSyncPagedImportOptions options) + => new SyncHandlerOptions(options.HandlerSet, options.UserId) { - var nodes = new List(); + IncludeDisabled = options.IncludeDisabledHandlers + }; + + /// + /// Load the xml in a folder in level order so we process the higher level items first. + /// + [Obsolete("use handler and multiple folder method will be removed in v15")] + public IList LoadOrderedNodes(string folder) + => LoadOrderedNodes([folder]).ToList(); - foreach (var folder in folders) + /// + /// Load the xml in a folder in level order so we process the higher level items first. + /// + [Obsolete("use handler and multiple folder method will be removed in v15")] + public IList LoadOrderedNodes(string[] folders) + { + var nodes = new List(); + + foreach (var folder in folders) + { + var files = _syncFileService.GetFiles(folder, $"*.{_uSyncConfig.Settings.DefaultExtension}", true); + foreach (var file in files) { - var files = _syncFileService.GetFiles(folder, $"*.{_uSyncConfig.Settings.DefaultExtension}", true); - foreach (var file in files) - { - var xml = _syncFileService.LoadXElement(file); - nodes.Add(new OrderedNodeInfo( - filename: file, - node: xml, - level: xml.GetLevel(), - path: file.Substring(folder.Length), - isRoot: false)); - - } - } + var xml = _syncFileService.LoadXElement(file); + nodes.Add(new OrderedNodeInfo( + filename: file, + node: xml, + level: xml.GetLevel(), + path: file.Substring(folder.Length), + isRoot: false)); - return nodes - .OrderBy(x => (x.Level * 1000) + x.Node.GetItemSortOrder()) - .ToList(); + } } - /// - /// load up ordered nodes from a handler folder, - /// - /// - /// this makes ordered node loading faster, when we are processing multiple requests, because we don't have to calculate it each time - /// - [Obsolete("use handler and multiple folder method will be removed in v15")] - public IList LoadOrderedNodes(ISyncHandler handler, string handlerFolder) - => LoadOrderedNodes(handler, [handlerFolder]).ToList(); - - /// - /// load up ordered nodes from a handler folder, - /// - /// - /// this makes ordered node loading faster, when we are processing multiple requests, because we don't have to calculate it each time - /// - public IList LoadOrderedNodes(ISyncHandler handler, string[] handlerFolders) - => handler.FetchAllNodes(handlerFolders).ToList(); - - /// - /// calculate the percentage progress we are making between a range. - /// - /// - /// for partial imports this allows the calling progress to smooth out the progress bar. - /// - private int CalculateProgress(int value, int total, int min, int max) - => (int)(min + (((float)value / total) * (max - min))); + return nodes + .OrderBy(x => (x.Level * 1000) + x.Node.GetItemSortOrder()) + .ToList(); } + + /// + /// load up ordered nodes from a handler folder, + /// + /// + /// this makes ordered node loading faster, when we are processing multiple requests, because we don't have to calculate it each time + /// + [Obsolete("use handler and multiple folder method will be removed in v15")] + public IList LoadOrderedNodes(ISyncHandler handler, string handlerFolder) + => LoadOrderedNodes(handler, [handlerFolder]).ToList(); + + /// + /// load up ordered nodes from a handler folder, + /// + /// + /// this makes ordered node loading faster, when we are processing multiple requests, because we don't have to calculate it each time + /// + public IList LoadOrderedNodes(ISyncHandler handler, string[] handlerFolders) + => handler.FetchAllNodes(handlerFolders).ToList(); + + /// + /// calculate the percentage progress we are making between a range. + /// + /// + /// for partial imports this allows the calling progress to smooth out the progress bar. + /// + private int CalculateProgress(int value, int total, int min, int max) + => (int)(min + (((float)value / total) * (max - min))); } diff --git a/uSync.BackOffice/Services/uSyncTriggers.cs b/uSync.BackOffice/Services/uSyncTriggers.cs index b06a63a4..8017079a 100644 --- a/uSync.BackOffice/Services/uSyncTriggers.cs +++ b/uSync.BackOffice/Services/uSyncTriggers.cs @@ -3,57 +3,56 @@ using uSync.BackOffice.SyncHandlers; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +internal delegate void uSyncTriggerEventHandler(uSyncTriggerArgs e); + +/// +/// used to trigger uSync events from within other elements of uSync +/// +internal class uSyncTriggers { - internal delegate void uSyncTriggerEventHandler(uSyncTriggerArgs e); + internal static event uSyncTriggerEventHandler? DoExport; + internal static event uSyncTriggerEventHandler? DoImport; /// - /// used to trigger uSync events from within other elements of uSync + /// trigger an export of an item programatically. /// - internal class uSyncTriggers + /// folder to use for export + /// entity types to trigger export for + /// handler options to use for handlers + public static void TriggerExport(string folder, IEnumerable entityTypes, SyncHandlerOptions options) + => TriggerExport([folder], entityTypes, options); + + public static void TriggerExport(string[] folders, IEnumerable entityTypes, SyncHandlerOptions? options) { - internal static event uSyncTriggerEventHandler DoExport; - internal static event uSyncTriggerEventHandler DoImport; - - /// - /// trigger an export of an item programatically. - /// - /// folder to use for export - /// entity types to trigger export for - /// handler options to use for handlers - public static void TriggerExport(string folder, IEnumerable entityTypes, SyncHandlerOptions options) - => TriggerExport([folder], entityTypes, options); - - public static void TriggerExport(string[] folders, IEnumerable entityTypes, SyncHandlerOptions options) + DoExport?.Invoke(new uSyncTriggerArgs() { - DoExport?.Invoke(new uSyncTriggerArgs() - { - EntityTypes = entityTypes, - Folder = folders.Last(), - HandlerOptions = options - }); - } - - public static void TriggerImport(string folder, IEnumerable entityTypes, SyncHandlerOptions options) + EntityTypes = entityTypes, + Folder = folders.Last(), + HandlerOptions = options + }); + } + + public static void TriggerImport(string folder, IEnumerable entityTypes, SyncHandlerOptions? options) + { + DoImport?.Invoke(new uSyncTriggerArgs() { - DoImport?.Invoke(new uSyncTriggerArgs() - { - EntityTypes = entityTypes, - Folder = folder, - HandlerOptions = options - }); - } + EntityTypes = entityTypes, + Folder = folder, + HandlerOptions = options + }); + } - } +} - internal class uSyncTriggerArgs - { - public string Folder { get; set; } +internal class uSyncTriggerArgs +{ + public string Folder { get; set; } = string.Empty; - public IEnumerable EntityTypes { get; set; } + public IEnumerable EntityTypes { get; set; } = []; - public SyncHandlerOptions HandlerOptions { get; set; } + public SyncHandlerOptions? HandlerOptions { get; set; } - } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs index 50712af2..22579075 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs @@ -12,87 +12,88 @@ using uSync.BackOffice.Configuration; using uSync.BackOffice.Services; -using uSync.BackOffice.SyncHandlers.Interfaces; using uSync.Core; using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to manage content items in uSync +/// +[SyncHandler(uSyncConstants.Handlers.ContentHandler, "Content", "Content", uSyncConstants.Priorites.Content + , Icon = "icon-document", IsTwoPass = true, EntityType = UdiEntityType.Document)] +public class ContentHandler : ContentHandlerBase, ISyncHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> + { /// - /// Handler to manage content items in uSync + /// the default group for which events matter (content group) /// - [SyncHandler(uSyncConstants.Handlers.ContentHandler, "Content", "Content", uSyncConstants.Priorites.Content - , Icon = "icon-document", IsTwoPass = true, EntityType = UdiEntityType.Document)] - public class ContentHandler : ContentHandlerBase, ISyncHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + public override string Group => uSyncConstants.Groups.Content; - { - /// - /// the default group for which events matter (content group) - /// - public override string Group => uSyncConstants.Groups.Content; + private readonly IContentService contentService; - private readonly IContentService contentService; - - /// - /// Constructor, called via DI - /// - public ContentHandler( - ILogger logger, - IEntityService entityService, - IContentService contentService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfigService, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) - { - this.contentService = contentService; + /// + /// Constructor, called via DI + /// + public ContentHandler( + ILogger logger, + IEntityService entityService, + IContentService contentService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfigService, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) + { + this.contentService = contentService; - // make sure we get the default content serializer (not just the first one that loads) - this.serializer = syncItemFactory.GetSerializer("ContentSerializer"); - } + // make sure we get the default content serializer (not just the first one that loads) + var s = syncItemFactory.GetSerializer("ContentSerializer"); + if (s is null) + throw new KeyNotFoundException("Cannot load content serializer"); + this.serializer = s; + } - /// - protected override bool HasChildren(IContent item) - => contentService.HasChildren(item.Id); + /// + protected override bool HasChildren(IContent item) + => contentService.HasChildren(item.Id); - /// - /// Get child items - /// - /// - /// The core method works for all services, (using entities) - but if we look up - /// the actual type for content and media, we save ourselves an extra lookup later on - /// and this speeds up the itteration by quite a bit (onle less db trip per item). - /// - protected override IEnumerable GetChildItems(IEntity parent) + /// + /// Get child items + /// + /// + /// The core method works for all services, (using entities) - but if we look up + /// the actual type for content and media, we save ourselves an extra lookup later on + /// and this speeds up the itteration by quite a bit (onle less db trip per item). + /// + protected override IEnumerable GetChildItems(IEntity? parent) + { + if (parent != null) { - if (parent != null) + var items = new List(); + const int pageSize = 5000; + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) { - var items = new List(); - const int pageSize = 5000; - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) - { - items.AddRange(contentService.GetPagedChildren(parent.Id, page++, pageSize, out total)); - } - return items; - } - else - { - return contentService.GetRootContent(); + items.AddRange(contentService.GetPagedChildren(parent.Id, page++, pageSize, out total)); } + return items; + } + else + { + return contentService.GetRootContent(); } } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandlerBase.cs b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandlerBase.cs index 299f0414..7eb65881 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandlerBase.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandlerBase.cs @@ -16,235 +16,233 @@ using uSync.Core; using uSync.Core.Serialization; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// base for all content based handlers +/// +/// +/// Content based handlers can have the same name in different +/// places around the tree, so we have to check for file name +/// clashes. +/// +public abstract class ContentHandlerBase : SyncHandlerTreeBase + where TObject : IContentBase + where TService : IService { /// - /// base for all content based handlers + /// Base constructor, should never be called directly /// - /// - /// Content based handlers can have the same name in different - /// places around the tree, so we have to check for file name - /// clashes. - /// - public abstract class ContentHandlerBase : SyncHandlerTreeBase - where TObject : IContentBase - where TService : IService - { - /// - /// Base constructor, should never be called directly - /// - protected ContentHandlerBase( - ILogger> logger, - IEntityService entityService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfigService, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) - { } - - /// - /// Generate a unique string based on the item name and path, used for matching to existing items - /// - protected override string GetItemMatchString(TObject item) - { - var itemPath = item.Level.ToString(); - if (item.Trashed && serializer is ISyncContentSerializer contentSerializer) - { - itemPath = contentSerializer.GetItemPath(item); - } - return $"{item.Name}_{itemPath}".ToLower(); - } + protected ContentHandlerBase( + ILogger> logger, + IEntityService entityService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfigService, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) + { } - /// - /// Generate a unique string based on the item name and path from a uSync xml file - /// - protected override string GetXmlMatchString(XElement node) + /// + /// Generate a unique string based on the item name and path, used for matching to existing items + /// + protected override string GetItemMatchString(TObject item) + { + var itemPath = item.Level.ToString(); + if (item.Trashed && serializer is ISyncContentSerializer contentSerializer) { - var path = node.Element("Info")?.Element("Path").ValueOrDefault(node.GetLevel().ToString()); - return $"{node.GetAlias()}_{path}".ToLower(); + itemPath = contentSerializer.GetItemPath(item); } + return $"{item.Name}_{itemPath}".ToLower(); + } + /// + /// Generate a unique string based on the item name and path from a uSync xml file + /// + protected override string GetXmlMatchString(XElement node) + { + var path = node.Element("Info")?.Element("Path").ValueOrDefault(node.GetLevel().ToString()); + return $"{node.GetAlias()}_{path}".ToLower(); + } - /* - * Config options. - * Include = Paths (comma separated) (only include if path starts with one of these) - * Exclude = Paths (comma separated) (exclude if path starts with one of these) - * - * RulesOnExport = bool (do we apply the rules on export as well as import?) - */ - /// - /// Should this item be imported (will check rules) - /// - protected override bool ShouldImport(XElement node, HandlerSettings config) - { - // check base first - if it says no - then no point checking this. - if (!base.ShouldImport(node, config)) return false; + /* + * Config options. + * Include = Paths (comma separated) (only include if path starts with one of these) + * Exclude = Paths (comma separated) (exclude if path starts with one of these) + * + * RulesOnExport = bool (do we apply the rules on export as well as import?) + */ - if (!ImportTrashedItem(node, config)) return false; + /// + /// Should this item be imported (will check rules) + /// + protected override bool ShouldImport(XElement node, HandlerSettings config) + { + // check base first - if it says no - then no point checking this. + if (!base.ShouldImport(node, config)) return false; - if (!ImportPaths(node, config)) return false; + if (!ImportTrashedItem(node, config)) return false; - if (!ByDocTypeConfigCheck(node, config)) return false; + if (!ImportPaths(node, config)) return false; - return true; - } + if (!ByDocTypeConfigCheck(node, config)) return false; - /// - /// Import an item that is in the trashed state in the XML file. - /// - /// - /// Trashed items are only imported when the "ImportTrashed" setting is true on the handler - /// - 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", false)) return false; - - return true; - } - - private bool ImportPaths(XElement node, HandlerSettings config) - { - var include = config.GetSetting("Include", "") - .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + return true; + } - if (include.Length > 0) - { - var path = node.Element("Info")?.Element("Path").ValueOrDefault(string.Empty); - if (!string.IsNullOrWhiteSpace(path) && !include.Any(x => path.InvariantStartsWith(x))) - { - logger.LogDebug("Not processing item, {alias} path {path} not in include path", node.GetAlias(), path); - return false; - } - } + /// + /// Import an item that is in the trashed state in the XML file. + /// + /// + /// Trashed items are only imported when the "ImportTrashed" setting is true on the handler + /// + 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", false)) return false; - var exclude = config.GetSetting("Exclude", "") - .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - if (exclude.Length > 0) - { - var path = node.Element("Info")?.Element("Path").ValueOrDefault(string.Empty); - if (!string.IsNullOrWhiteSpace(path) && exclude.Any(x => path.InvariantStartsWith(x))) - { - logger.LogDebug("Not processing item, {alias} path {path} is excluded", node.GetAlias(), path); - return false; - } - } + return true; + } - return true; - } + private bool ImportPaths(XElement node, HandlerSettings config) + { + var include = config.GetSetting("Include", "") + .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - private bool ByDocTypeConfigCheck(XElement node, HandlerSettings config) + if (include.Length > 0) { - var includeDocTypes = config.GetSetting("IncludeContentTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries); - - var doctype = node.Element("Info")?.Element("ContentType").ValueOrDefault(string.Empty); - if (string.IsNullOrEmpty(doctype)) return true; - - if (includeDocTypes.Length > 0 && !includeDocTypes.InvariantContains(doctype)) + var path = node.Element("Info")?.Element("Path").ValueOrDefault(string.Empty); + if (!string.IsNullOrWhiteSpace(path) && !include.Any(x => path.InvariantStartsWith(x))) { - logger.LogDebug("Not processing {alias} as it in not in the Included by ContentType list {contentType}", node.GetAlias(), doctype); + logger.LogDebug("Not processing item, {alias} path {path} not in include path", node.GetAlias(), path); return false; } + } - var excludeDocTypes = config.GetSetting("ExcludeContentTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries); - if (excludeDocTypes.Length > 0 && excludeDocTypes.InvariantContains(doctype)) + var exclude = config.GetSetting("Exclude", "") + .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (exclude.Length > 0) + { + var path = node.Element("Info")?.Element("Path").ValueOrDefault(string.Empty); + if (!string.IsNullOrWhiteSpace(path) && exclude.Any(x => path.InvariantStartsWith(x))) { - logger.LogDebug("Not processing {alias} as it is excluded by ContentType {contentType}", node.GetAlias(), doctype); + logger.LogDebug("Not processing item, {alias} path {path} is excluded", node.GetAlias(), path); return false; } - - return true; } + return true; + } - /// - /// Should we save this value to disk? - /// - /// - /// In general we save everything to disk, even if we are not going to re-import it later - /// but you can stop this with RulesOnExport = true in the settings - /// + private bool ByDocTypeConfigCheck(XElement node, HandlerSettings config) + { + var includeDocTypes = config.GetSetting("IncludeContentTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries); - protected override bool ShouldExport(XElement node, HandlerSettings config) - { - // We export trashed items by default, (but we don't import them by default) - var trashed = node.Element("Info")?.Element("Trashed").ValueOrDefault(false); - if (trashed.GetValueOrDefault(false) && !config.GetSetting("ExportTrashed", true)) return false; + var doctype = node.Element("Info")?.Element("ContentType").ValueOrDefault(string.Empty); + if (string.IsNullOrEmpty(doctype)) return true; - if (config.GetSetting("RulesOnExport", false)) - { - // we run the import rules (but not the base rules as that would confuse.) - if (!ImportTrashedItem(node, config)) return false; - if (!ImportPaths(node, config)) return false; - if (!ByDocTypeConfigCheck(node, config)) return false; - } + if (includeDocTypes.Length > 0 && !includeDocTypes.InvariantContains(doctype)) + { + logger.LogDebug("Not processing {alias} as it in not in the Included by ContentType list {contentType}", node.GetAlias(), doctype); + return false; + } - return true; + var excludeDocTypes = config.GetSetting("ExcludeContentTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries); + if (excludeDocTypes.Length > 0 && excludeDocTypes.InvariantContains(doctype)) + { + logger.LogDebug("Not processing {alias} as it is excluded by ContentType {contentType}", node.GetAlias(), doctype); + return false; } + return true; + } - // we only match duplicate actions by key. - /// - /// Do the uSyncActions match by key (e.g are they the same item) - /// - protected override bool DoActionsMatch(uSyncAction a, uSyncAction b) - => a.key == b.key; + /// + /// Should we save this value to disk? + /// + /// + /// In general we save everything to disk, even if we are not going to re-import it later + /// but you can stop this with RulesOnExport = true in the settings + /// - /// - /// Handle the Umbraco Moved to recycle bin notification, (treated like a move) - /// - /// - public void Handle(MovedToRecycleBinNotification notification) + protected override bool ShouldExport(XElement node, HandlerSettings config) + { + // We export trashed items by default, (but we don't import them by default) + var trashed = node.Element("Info")?.Element("Trashed").ValueOrDefault(false); + if (trashed.GetValueOrDefault(false) && !config.GetSetting("ExportTrashed", true)) return false; + + if (config.GetSetting("RulesOnExport", false)) { - if (!ShouldProcessEvent()) return; - HandleMove(notification.MoveInfoCollection); + // we run the import rules (but not the base rules as that would confuse.) + if (!ImportTrashedItem(node, config)) return false; + if (!ImportPaths(node, config)) return false; + if (!ByDocTypeConfigCheck(node, config)) return false; } - /// - /// Check that roots isn't stopping an item from being recycled. - /// - /// - public void Handle(MovingToRecycleBinNotification notification) + return true; + } + + + // we only match duplicate actions by key. + + /// + /// Do the uSyncActions match by key (e.g are they the same item) + /// + protected override bool DoActionsMatch(uSyncAction a, uSyncAction b) + => a.key == b.key; + + /// + /// Handle the Umbraco Moved to recycle bin notification, (treated like a move) + /// + /// + public void Handle(MovedToRecycleBinNotification notification) + { + if (!ShouldProcessEvent()) return; + HandleMove(notification.MoveInfoCollection); + } + + /// + /// Check that roots isn't stopping an item from being recycled. + /// + /// + public void Handle(MovingToRecycleBinNotification notification) + { + if (ShouldBlockRootChanges(notification.MoveInfoCollection.Select(x => x.Entity))) { - if (ShouldBlockRootChanges(notification.MoveInfoCollection.Select(x => x.Entity))) - { - notification.Cancel = true; - notification.Messages.Add(GetCancelMessageForRoots()); - } + notification.Cancel = true; + notification.Messages.Add(GetCancelMessageForRoots()); } + } - /// - /// Clean up any files on disk, that might be left over when an item moves - /// - protected override void CleanUp(TObject item, string newFile, string folder) - { - // for content this clean up check only catches when an item is moved from - // one location to another, if the site is setup to useGuidNames and a flat - // structure that rename won't actually leave any old files on disk. + /// + /// Clean up any files on disk, that might be left over when an item moves + /// + protected override void CleanUp(TObject item, string newFile, string folder) + { + // for content this clean up check only catches when an item is moved from + // one location to another, if the site is setup to useGuidNames and a flat + // structure that rename won't actually leave any old files on disk. - bool quickCleanup = this.DefaultConfig.GetSetting("QuickCleanup", false); - if (quickCleanup) - { - logger.LogDebug("Quick cleanup is on, so not looking in all config files"); - return; - } + bool quickCleanup = this.DefaultConfig.GetSetting("QuickCleanup", false); + if (quickCleanup) + { + logger.LogDebug("Quick cleanup is on, so not looking in all config files"); + return; + } - // so we can skip this step and get a much quicker save process. - if (this.DefaultConfig.GuidNames && this.DefaultConfig.UseFlatStructure) return; + // so we can skip this step and get a much quicker save process. + if (this.DefaultConfig.GuidNames && this.DefaultConfig.UseFlatStructure) return; - // check to see if we think this was a rename (so only do the clean up if we really have to) - if (item.WasPropertyDirty(nameof(item.Name)) || item.WasPropertyDirty(nameof(item.ParentId))) - { - base.CleanUp(item, newFile, folder); - } + // check to see if we think this was a rename (so only do the clean up if we really have to) + if (item.WasPropertyDirty(nameof(item.Name)) || item.WasPropertyDirty(nameof(item.ParentId))) + { + base.CleanUp(item, newFile, folder); } - } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/ContentTemplateHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/ContentTemplateHandler.cs index 144e097b..a346d04b 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/ContentTemplateHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/ContentTemplateHandler.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using Microsoft.Extensions.Logging; @@ -17,113 +16,114 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to manage content templates in uSync +/// +[SyncHandler(uSyncConstants.Handlers.ContentTemplateHandler, "Blueprints", "Blueprints", uSyncConstants.Priorites.ContentTemplate + , Icon = "icon-blueprint", IsTwoPass = true, EntityType = UdiEntityType.DocumentBlueprint)] +public class ContentTemplateHandler : ContentHandlerBase, ISyncHandler, + INotificationHandler, + INotificationHandler { /// - /// Handler to manage content templates in uSync + /// ContentTypeHandler belongs to the Content group by default + /// + public override string Group => uSyncConstants.Groups.Content; + + private readonly IContentService contentService; + + /// + /// Handler constrcutor Loaded via DI /// - [SyncHandler(uSyncConstants.Handlers.ContentTemplateHandler, "Blueprints", "Blueprints", uSyncConstants.Priorites.ContentTemplate - , Icon = "icon-blueprint", IsTwoPass = true, EntityType = UdiEntityType.DocumentBlueprint)] - public class ContentTemplateHandler : ContentHandlerBase, ISyncHandler, - INotificationHandler, - INotificationHandler + public ContentTemplateHandler( + ILogger logger, + IEntityService entityService, + IContentService contentService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfigService, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) { - /// - /// ContentTypeHandler belongs to the Content group by default - /// - public override string Group => uSyncConstants.Groups.Content; - - private readonly IContentService contentService; - - /// - /// Handler constrcutor Loaded via DI - /// - public ContentTemplateHandler( - ILogger logger, - IEntityService entityService, - IContentService contentService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfigService, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) - { - this.contentService = contentService; + this.contentService = contentService; - // make sure we load up the template serializer - because we need that one, not the normal content one. - this.serializer = syncItemFactory.GetSerializer("contentTemplateSerializer"); - } + // make sure we load up the template serializer - because we need that one, not the normal content one. + this.serializer = syncItemFactory.GetSerializer("contentTemplateSerializer") ?? + throw new NullReferenceException("Can not load the contentTemplateSerializer"); + } + + /// + /// Delete a content template via the ContentService + /// + protected override void DeleteViaService(IContent item) + => contentService.DeleteBlueprint(item); - /// - /// Delete a content template via the ContentService - /// - protected override void DeleteViaService(IContent item) - => contentService.DeleteBlueprint(item); - - /// - /// Fetch a content template via the ContentService - /// - protected override IContent GetFromService(int id) - => contentService.GetBlueprintById(id); - - /// - /// Fetch a content template via the ContentService - /// - protected override IContent GetFromService(Guid key) - => contentService.GetBlueprintById(key); - - /// - /// Fetch a content template via the ContentService - /// - protected override IContent GetFromService(string alias) - => null; - - /// - /// Manage the content blueprint saved notification - /// - public void Handle(ContentSavedBlueprintNotification notification) + /// + /// Fetch a content template via the ContentService + /// + protected override IContent? GetFromService(int id) + => contentService.GetBlueprintById(id); + + /// + /// Fetch a content template via the ContentService + /// + protected override IContent? GetFromService(Guid key) + => contentService.GetBlueprintById(key); + + /// + /// Fetch a content template via the ContentService + /// + protected override IContent? GetFromService(string alias) + => null; + + /// + /// Manage the content blueprint saved notification + /// + public void Handle(ContentSavedBlueprintNotification notification) + { + if (!ShouldProcessEvent()) return; + + var item = notification.SavedBlueprint; + try + { + var handlerFolders = GetDefaultHandlerFolders(); + var attempts = Export(item, handlerFolders, DefaultConfig); + foreach (var attempt in attempts.Where(x => x.Success)) + { + if (attempt.FileName is null) continue; + this.CleanUp(item, attempt.FileName, handlerFolders.Last()); + } + } + catch (Exception ex) { - if (!ShouldProcessEvent()) return; + logger.LogWarning(ex, "Failed to create uSync export file"); + notification.Messages.Add(new EventMessage("uSync", $"Failed to create export file : {ex.Message}", EventMessageType.Warning)); + } + } + + /// + /// manage the notification when a blueprint is deleted + /// + public void Handle(ContentDeletedBlueprintNotification notification) + { + if (!ShouldProcessEvent()) return; - var item = notification.SavedBlueprint; + foreach (var item in notification.DeletedBlueprints) + { try { var handlerFolders = GetDefaultHandlerFolders(); - var attempts = Export(item, handlerFolders, DefaultConfig); - foreach (var attempt in attempts.Where(x => x.Success)) - { - this.CleanUp(item, attempt.FileName, handlerFolders.Last()); - } + ExportDeletedItem(item, handlerFolders, DefaultConfig); } catch (Exception ex) { - logger.LogWarning(ex, "Failed to create uSync export file"); - notification.Messages.Add(new EventMessage("uSync", $"Failed to create export file : {ex.Message}", EventMessageType.Warning)); - } - } - - /// - /// manage the notification when a blueprint is deleted - /// - public void Handle(ContentDeletedBlueprintNotification notification) - { - if (!ShouldProcessEvent()) return; - - foreach(var item in notification.DeletedBlueprints) - { - try - { - var handlerFolders = GetDefaultHandlerFolders(); - ExportDeletedItem(item, handlerFolders, DefaultConfig); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to create delete marker"); - notification.Messages.Add(new EventMessage("uSync", $"Failed to mark as deleted : {ex.Message}", EventMessageType.Warning)); - } + logger.LogWarning(ex, "Failed to create delete marker"); + notification.Messages.Add(new EventMessage("uSync", $"Failed to mark as deleted : {ex.Message}", EventMessageType.Warning)); } } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/ContentTypeBaseHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/ContentTypeBaseHandler.cs index 83d84abf..0743b6b9 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/ContentTypeBaseHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/ContentTypeBaseHandler.cs @@ -14,76 +14,75 @@ using uSync.Core; using uSync.Core.Models; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// handler base for all ContentTypeBase handlers +/// +public abstract class ContentTypeBaseHandler : SyncHandlerContainerBase + where TObject : ITreeEntity + where TService : IService + { + /// - /// handler base for all ContentTypeBase handlers + /// Constructor. /// - public abstract class ContentTypeBaseHandler : SyncHandlerContainerBase - where TObject : ITreeEntity - where TService : IService + protected ContentTypeBaseHandler( + ILogger> logger, + IEntityService entityService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + { } + /// + protected override SyncAttempt Export_DoExport(TObject item, string filename, string[] folders, HandlerSettings config) { + // all the possible files that there could be. + var files = folders.Select(x => GetPath(x, item, config.GuidNames, config.UseFlatStructure)).ToArray(); + var nodes = syncFileService.GetAllNodes(files[..^1]); - /// - /// Constructor. - /// - protected ContentTypeBaseHandler( - ILogger> logger, - IEntityService entityService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { } - - /// - protected override SyncAttempt Export_DoExport(TObject item, string filename, string[] folders, HandlerSettings config) + // with roots enabled - we attempt to merge doctypes ! + // + var attempt = SerializeItem(item, new Core.Serialization.SyncSerializerOptions(config.Settings)); + if (attempt.Success && attempt.Item is not null) { - // all the possible files that there could be. - var files = folders.Select(x => GetPath(x, item, config.GuidNames, config.UseFlatStructure)).ToArray(); - var nodes = syncFileService.GetAllNodes(files[..^1]); - - // with roots enabled - we attempt to merge doctypes ! - // - var attempt = SerializeItem(item, new Core.Serialization.SyncSerializerOptions(config.Settings)); - if (attempt.Success) + if (ShouldExport(attempt.Item, config)) { - if (ShouldExport(attempt.Item, config)) + if (nodes.Count > 0) { - if (nodes.Count > 0) + nodes.Add(attempt.Item); + var difference = syncFileService.GetDifferences(nodes, trackers.FirstOrDefault()); + if (difference != null) { - nodes.Add(attempt.Item); - var difference = syncFileService.GetDifferences(nodes, trackers.FirstOrDefault()); - if (difference != null) - { - syncFileService.SaveXElement(difference, filename); - } - else - { - if (syncFileService.FileExists(filename)) - syncFileService.DeleteFile(filename); - } - + syncFileService.SaveXElement(difference, filename); } else { - syncFileService.SaveXElement(attempt.Item, filename); + if (syncFileService.FileExists(filename)) + syncFileService.DeleteFile(filename); } - if (config.CreateClean && HasChildren(item)) - CreateCleanFile(GetItemKey(item), filename); } else { - return SyncAttempt.Succeed(filename, ChangeType.NoChange, "Not Exported (Based on configuration)"); + syncFileService.SaveXElement(attempt.Item, filename); } - } - return attempt; + if (config.CreateClean && HasChildren(item)) + CreateCleanFile(GetItemKey(item), filename); + } + else + { + return SyncAttempt.Succeed(filename, ChangeType.NoChange, "Not Exported (Based on configuration)"); + } } + + return attempt; } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/ContentTypeHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/ContentTypeHandler.cs index 0aaa2918..557d01fd 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/ContentTypeHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/ContentTypeHandler.cs @@ -17,76 +17,75 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to mange content types in uSync +/// +[SyncHandler(uSyncConstants.Handlers.ContentTypeHandler, "DocTypes", "ContentTypes", uSyncConstants.Priorites.ContentTypes, + IsTwoPass = true, Icon = "icon-item-arrangement", EntityType = UdiEntityType.DocumentType)] +public class ContentTypeHandler : ContentTypeBaseHandler, ISyncHandler, ISyncPostImportHandler, ISyncGraphableHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler, + INotificationHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> + { + private readonly IContentTypeService _contentTypeService; + /// - /// Handler to mange content types in uSync + /// Constructor - loaded via DI /// - [SyncHandler(uSyncConstants.Handlers.ContentTypeHandler, "DocTypes", "ContentTypes", uSyncConstants.Priorites.ContentTypes, - IsTwoPass = true, Icon = "icon-item-arrangement", EntityType = UdiEntityType.DocumentType)] - public class ContentTypeHandler : ContentTypeBaseHandler, ISyncHandler, ISyncPostImportHandler, ISyncGraphableHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler, - INotificationHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + public ContentTypeHandler( + ILogger logger, + IEntityService entityService, + IContentTypeService contentTypeService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + { + this._contentTypeService = contentTypeService; + } + /// + /// Get the entity name we are going to use when constructing a generic path for an item + /// + protected override string GetEntityTreeName(IUmbracoEntity item, bool useGuid) { - private readonly IContentTypeService contentTypeService; + if (useGuid) return item.Key.ToString(); - /// - /// Constructor - loaded via DI - /// - public ContentTypeHandler( - ILogger logger, - IEntityService entityService, - IContentTypeService contentTypeService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + if (item is IContentTypeBase baseItem) { - this.contentTypeService = contentTypeService; + return baseItem.Alias.ToSafeFileName(shortStringHelper); } - /// - /// Get the entity name we are going to use when constructing a generic path for an item - /// - protected override string GetEntityTreeName(IUmbracoEntity item, bool useGuid) - { - if (useGuid) return item.Key.ToString(); - - if (item is IContentTypeBase baseItem) - { - return baseItem.Alias.ToSafeFileName(shortStringHelper); - } - - return item.Name.ToSafeFileName(shortStringHelper); - } + return item.Name?.ToSafeFileName(shortStringHelper) ?? item.Key.ToString(); + } - /// - /// Fetch a ContentType container via the ContentTypeService - /// - protected override IEntity GetContainer(int id) - => contentTypeService.GetContainer(id); + /// + /// Fetch a ContentType container via the ContentTypeService + /// + protected override IEntity? GetContainer(int id) + => _contentTypeService.GetContainer(id); - /// - /// Fetch a ContentType container via the ContentTypeService - /// - protected override IEntity GetContainer(Guid key) - => contentTypeService.GetContainer(key); + /// + /// Fetch a ContentType container via the ContentTypeService + /// + protected override IEntity? GetContainer(Guid key) + => _contentTypeService.GetContainer(key); - /// - /// Delete a ContentType container via the ContentTypeService - /// - protected override void DeleteFolder(int id) - => contentTypeService.DeleteContainer(id); - } + /// + /// Delete a ContentType container via the ContentTypeService + /// + protected override void DeleteFolder(int id) + => _contentTypeService.DeleteContainer(id); } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs index 8e5bb0e5..130b9e17 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs @@ -22,144 +22,144 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to manage DataTypes via uSync +/// +[SyncHandler(uSyncConstants.Handlers.DataTypeHandler, "Datatypes", "DataTypes", uSyncConstants.Priorites.DataTypes, + Icon = "icon-autofill", IsTwoPass = true, EntityType = UdiEntityType.DataType)] +public class DataTypeHandler : SyncHandlerContainerBase, ISyncHandler, ISyncPostImportHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler, + INotificationHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> { + private readonly IDataTypeService dataTypeService; + + /// + /// Constructor called via DI + /// + public DataTypeHandler( + ILogger logger, + IEntityService entityService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + { + this.dataTypeService = dataTypeService; + } + /// - /// Handler to manage DataTypes via uSync + /// Process all DataType actions at the end of the import process /// - [SyncHandler(uSyncConstants.Handlers.DataTypeHandler, "Datatypes", "DataTypes", uSyncConstants.Priorites.DataTypes, - Icon = "icon-autofill", IsTwoPass = true, EntityType = UdiEntityType.DataType)] - public class DataTypeHandler : SyncHandlerContainerBase, ISyncHandler, ISyncPostImportHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler, - INotificationHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + /// + /// 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 IEnumerable ProcessPostImport(IEnumerable actions, HandlerSettings config) { - private readonly IDataTypeService dataTypeService; - - /// - /// Constructor called via DI - /// - public DataTypeHandler( - ILogger logger, - IEntityService entityService, - IDataTypeService dataTypeService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + if (actions == null || !actions.Any()) + return Enumerable.Empty(); + + var results = new List(); + + // we only do deletes here. + foreach (var action in actions.Where(x => x.Change == ChangeType.Hidden)) { - this.dataTypeService = dataTypeService; + if (action.FileName is null) continue; + results.AddRange( + Import(action.FileName, config, SerializerFlags.LastPass)); } - /// - /// 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 IEnumerable ProcessPostImport(IEnumerable actions, HandlerSettings config) - { - if (actions == null || !actions.Any()) - return Enumerable.Empty(); + results.AddRange(CleanFolders(-1)); - var results = new List(); + return results; + } - // we only do deletes here. - foreach (var action in actions.Where(x => x.Change == ChangeType.Hidden)) - { - results.AddRange( - Import(action.FileName, config, SerializerFlags.LastPass)); - } + /// + /// Fetch a DataType Container from the DataTypeService + /// + protected override IEntity? GetContainer(int id) + => dataTypeService.GetContainer(id); - results.AddRange(CleanFolders(-1)); + /// + /// Fetch a DataType Container from the DataTypeService + /// + protected override IEntity? GetContainer(Guid key) + => dataTypeService.GetContainer(key); - return results; - } + /// + /// Delete a DataType Container from the DataTypeService + /// + protected override void DeleteFolder(int id) + => dataTypeService.DeleteContainer(id); + + /// + /// Get the filename to use for a DataType when we save it + /// + protected override string GetItemFileName(IDataType item) + => GetItemAlias(item).ToSafeAlias(shortStringHelper); - /// - /// Fetch a DataType Container from the DataTypeService - /// - protected override IEntity GetContainer(int id) - => dataTypeService.GetContainer(id); - - /// - /// Fetch a DataType Container from the DataTypeService - /// - protected override IEntity GetContainer(Guid key) - => dataTypeService.GetContainer(key); - - /// - /// Delete a DataType Container from the DataTypeService - /// - protected override void DeleteFolder(int id) - => dataTypeService.DeleteContainer(id); - - /// - /// Get the filename to use for a DataType when we save it - /// - protected override string GetItemFileName(IDataType item) - => GetItemAlias(item).ToSafeAlias(shortStringHelper); - - /// - protected override SyncAttempt Export_DoExport(IDataType item, string filename, string[] folders, HandlerSettings config) + /// + protected override SyncAttempt Export_DoExport(IDataType item, string filename, string[] folders, HandlerSettings config) + { + // all the possible files that there could be. + var files = folders.Select(x => GetPath(x, item, config.GuidNames, config.UseFlatStructure)).ToArray(); + var nodes = syncFileService.GetAllNodes(files[..^1]); + + // with roots enabled - we attempt to merge doctypes ! + // + var attempt = SerializeItem(item, new SyncSerializerOptions(config.Settings)); + if (attempt.Success && attempt.Item is not null) { - // all the possible files that there could be. - var files = folders.Select(x => GetPath(x, item, config.GuidNames, config.UseFlatStructure)).ToArray(); - var nodes = syncFileService.GetAllNodes(files[..^1]); - - // with roots enabled - we attempt to merge doctypes ! - // - var attempt = SerializeItem(item, new Core.Serialization.SyncSerializerOptions(config.Settings)); - if (attempt.Success) + if (ShouldExport(attempt.Item, config)) { - if (ShouldExport(attempt.Item, config)) + if (nodes.Count > 0) { - if (nodes.Count > 0) + nodes.Add(attempt.Item); + var difference = syncFileService.GetDifferences(nodes, trackers.FirstOrDefault()); + if (difference != null) { - nodes.Add(attempt.Item); - var difference = syncFileService.GetDifferences(nodes, trackers.FirstOrDefault()); - if (difference != null) - { - syncFileService.SaveXElement(difference, filename); - } - else - { - if (syncFileService.FileExists(filename)) - syncFileService.DeleteFile(filename); - } - + syncFileService.SaveXElement(difference, filename); } else { - syncFileService.SaveXElement(attempt.Item, filename); + if (syncFileService.FileExists(filename)) + syncFileService.DeleteFile(filename); } - if (config.CreateClean && HasChildren(item)) - CreateCleanFile(GetItemKey(item), filename); } else { - return SyncAttempt.Succeed(filename, ChangeType.NoChange, "Not Exported (Based on configuration)"); + syncFileService.SaveXElement(attempt.Item, filename); } - } - return attempt; + if (config.CreateClean && HasChildren(item)) + CreateCleanFile(GetItemKey(item), filename); + } + else + { + return SyncAttempt.Succeed(filename, ChangeType.NoChange, "Not Exported (Based on configuration)"); + } } + + return attempt; } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/DictionaryHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/DictionaryHandler.cs index 15ed9553..0ea83886 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/DictionaryHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/DictionaryHandler.cs @@ -1,10 +1,10 @@ -using Microsoft.Extensions.Logging; - -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Microsoft.Extensions.Logging; + using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -21,169 +21,171 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to manage Dictionary items via uSync +/// +[SyncHandler(uSyncConstants.Handlers.DictionaryHandler, "Dictionary", "Dictionary", uSyncConstants.Priorites.DictionaryItems + , Icon = "icon-book-alt", EntityType = UdiEntityType.DictionaryItem)] +public class DictionaryHandler : SyncHandlerLevelBase, ISyncHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> { /// - /// Handler to manage Dictionary items via uSync + /// Dictionary items belong to the content group by default /// - [SyncHandler(uSyncConstants.Handlers.DictionaryHandler, "Dictionary", "Dictionary", uSyncConstants.Priorites.DictionaryItems - , Icon = "icon-book-alt", EntityType = UdiEntityType.DictionaryItem)] - public class DictionaryHandler : SyncHandlerLevelBase, ISyncHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + public override string Group => uSyncConstants.Groups.Content; + + private readonly ILocalizationService localizationService; + + /// + public DictionaryHandler( + ILogger logger, + IEntityService entityService, + ILocalizationService localizationService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfigService, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) { - /// - /// Dictionary items belong to the content group by default - /// - public override string Group => uSyncConstants.Groups.Content; - - private readonly ILocalizationService localizationService; - - /// - public DictionaryHandler( - ILogger logger, - IEntityService entityService, - ILocalizationService localizationService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfigService, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) - { - this.localizationService = localizationService; - } + this.localizationService = localizationService; + } - /// - public override IEnumerable Import(string filePath, HandlerSettings config, SerializerFlags flags) + /// + public override IEnumerable Import(string filePath, HandlerSettings config, SerializerFlags flags) + { + if (IsOneWay(config)) { - if (IsOneWay(config)) + // only sync dictionary items if they are new + // so if it already exists we don't do the sync + + // + // + // + // + // + var item = GetExistingItem(filePath); + if (item != null) { - // only sync dictionary items if they are new - // so if it already exists we don't do the sync - - // - // - // - // - // - var item = GetExistingItem(filePath); - if (item != null) - { - return uSyncAction.SetAction(true, item.ItemKey, change: ChangeType.NoChange).AsEnumerableOfOne(); - } + return uSyncAction.SetAction(true, item.ItemKey, change: ChangeType.NoChange).AsEnumerableOfOne(); } + } - return base.Import(filePath, config, flags); + return base.Import(filePath, config, flags); - } + } - private IDictionaryItem GetExistingItem(string filePath) + private IDictionaryItem? GetExistingItem(string filePath) + { + syncFileService.EnsureFileExists(filePath); + + using (var stream = syncFileService.OpenRead(filePath)) { - syncFileService.EnsureFileExists(filePath); + if (stream is null) + throw new KeyNotFoundException($"Cannot load file {filePath}"); - using (var stream = syncFileService.OpenRead(filePath)) - { - var node = XElement.Load(stream); - return serializer.FindItem(node); - } + var node = XElement.Load(stream); + return serializer.FindItem(node); } + } + + /// + public override IEnumerable ExportAll(string folder, HandlerSettings config, SyncUpdateCallback? callback) + { + syncFileService.CleanFolder(folder); + return ExportAll(Guid.Empty, folder, config, callback); + } + + /// + /// Export all Dictionary items based on a parent GUID value + /// + /// + /// You can't fetch dictionary items via the entity service so they require their own + /// export method. + /// + public IEnumerable ExportAll(Guid parent, string folder, HandlerSettings config, SyncUpdateCallback? callback) + { + var actions = new List(); - /// - public override IEnumerable ExportAll(string folder, HandlerSettings config, SyncUpdateCallback callback) + var items = new List(); + + if (parent == Guid.Empty) { - syncFileService.CleanFolder(folder); - return ExportAll(Guid.Empty, folder, config, callback); + items = localizationService.GetRootDictionaryItems().ToList(); } - - /// - /// Export all Dictionary items based on a parent GUID value - /// - /// - /// You can't fetch dictionary items via the entity service so they require their own - /// export method. - /// - public IEnumerable ExportAll(Guid parent, string folder, HandlerSettings config, SyncUpdateCallback callback) + else { - var actions = new List(); + items = localizationService.GetDictionaryItemChildren(parent).ToList(); + } - var items = new List(); + int count = 0; + foreach (var item in items) + { + count++; + callback?.Invoke(item.ItemKey, count, items.Count); - if (parent == Guid.Empty) - { - items = localizationService.GetRootDictionaryItems().ToList(); - } - else - { - items = localizationService.GetDictionaryItemChildren(parent).ToList(); - } + actions.AddRange(Export(item, folder, config)); + actions.AddRange(ExportAll(item.Key, folder, config, callback)); + } - int count = 0; - foreach (var item in items) - { - count++; - callback?.Invoke(item.ItemKey, count, items.Count); + return actions; + } - actions.AddRange(Export(item, folder, config)); - actions.AddRange(ExportAll(item.Key, folder, config, callback)); - } + /// + protected override IEnumerable GetFolders(int parent) + => GetChildItems(parent); - return actions; + /// + protected override IEnumerable GetChildItems(int parent) + { + if (parent == -1) + { + return localizationService.GetRootDictionaryItems() + .Where(x => x is IEntity) + .Select(x => x as IEntity); } - - /// - protected override IEnumerable GetFolders(int parent) - => GetChildItems(parent); - - /// - protected override IEnumerable GetChildItems(int parent) + else { - if (parent == -1) - { - return localizationService.GetRootDictionaryItems() - .Where(x => x is IEntity) - .Select(x => x as IEntity); - } - else - { - var item = localizationService.GetDictionaryItemById(parent); - if (item != null) - return localizationService.GetDictionaryItemChildren(item.Key); - } - - return Enumerable.Empty(); + var item = localizationService.GetDictionaryItemById(parent); + if (item != null) + return localizationService.GetDictionaryItemChildren(item.Key); } - /// - protected override string GetItemName(IDictionaryItem item) - => item.ItemKey; + return Enumerable.Empty(); + } + + /// + protected override string GetItemName(IDictionaryItem item) + => item.ItemKey; - /// - protected override string GetItemPath(IDictionaryItem item, bool useGuid, bool isFlat) - => item.ItemKey.ToSafeFileName(shortStringHelper); + /// + protected override string GetItemPath(IDictionaryItem item, bool useGuid, bool isFlat) + => item.ItemKey.ToSafeFileName(shortStringHelper); - /// - protected override IEnumerable ReportElement(XElement node, string filename, HandlerSettings config) + /// + protected override IEnumerable ReportElement(XElement node, string filename, HandlerSettings? config) + { + if (config != null && IsOneWay(config)) { - if (config != null && IsOneWay(config)) + // if we find it then there is no change. + var item = GetExistingItem(filename); + if (item != null) { - // if we find it then there is no change. - var item = GetExistingItem(filename); - if (item != null) - { - return uSyncActionHelper - .ReportAction(ChangeType.NoChange, item.ItemKey, node.GetPath(), syncFileService.GetSiteRelativePath(filename), item.Key, this.Alias, "Existing Item will not be overwritten") - .AsEnumerableOfOne(); - } + return uSyncActionHelper + .ReportAction(ChangeType.NoChange, item.ItemKey, node.GetPath(), syncFileService.GetSiteRelativePath(filename), item.Key, this.Alias, "Existing Item will not be overwritten") + .AsEnumerableOfOne(); } - - return base.ReportElement(node, filename, config); } - private bool IsOneWay(HandlerSettings config) - => config.GetSetting("OneWay", false); + return base.ReportElement(node, filename, config); } + + private bool IsOneWay(HandlerSettings config) + => config.GetSetting("OneWay", false); } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/DomainHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/DomainHandler.cs index c644ade1..f8f26ba4 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/DomainHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/DomainHandler.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Logging; - -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Logging; + using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -16,78 +16,76 @@ using uSync.BackOffice.Services; using uSync.Core; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to mange Domain settings for uSync +/// +[SyncHandler(uSyncConstants.Handlers.DomainHandler, "Domains", "Domains", uSyncConstants.Priorites.DomainSettings + , Icon = "icon-home", EntityType = "domain")] +public class DomainHandler : SyncHandlerBase, ISyncHandler, + INotificationHandler>, + INotificationHandler> { - /// - /// Handler to mange Domain settings for uSync - /// - [SyncHandler(uSyncConstants.Handlers.DomainHandler, "Domains", "Domains", uSyncConstants.Priorites.DomainSettings - , Icon = "icon-home", EntityType = "domain")] - public class DomainHandler : SyncHandlerBase, ISyncHandler, - INotificationHandler>, - INotificationHandler> - { - /// - public override string Group => uSyncConstants.Groups.Content; + /// + public override string Group => uSyncConstants.Groups.Content; - private readonly IDomainService domainService; + private readonly IDomainService domainService; - /// - public DomainHandler( - ILogger logger, - IEntityService entityService, - IDomainService domainService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService configService, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, configService, syncItemFactory) - { - this.domainService = domainService; - } + /// + public DomainHandler( + ILogger logger, + IEntityService entityService, + IDomainService domainService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService configService, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, configService, syncItemFactory) + { + this.domainService = domainService; + } - /// - public override IEnumerable ExportAll(string folder, HandlerSettings config, SyncUpdateCallback callback) - { - var actions = new List(); + /// + public override IEnumerable ExportAll(string folder, HandlerSettings config, SyncUpdateCallback? callback) + { + var actions = new List(); - var domains = domainService.GetAll(true).ToList(); - int count = 0; - foreach (var domain in domains) + var domains = domainService.GetAll(true).ToList(); + int count = 0; + foreach (var domain in domains) + { + count++; + if (domain != null) { - count++; - if (domain != null) - { - callback?.Invoke(domain.DomainName, count, domains.Count); - actions.AddRange(Export(domain, folder, config)); - } + callback?.Invoke(domain.DomainName, count, domains.Count); + actions.AddRange(Export(domain, folder, config)); } - - callback?.Invoke("done", 1, 1); - return actions; } - /// - protected override string GetItemName(IDomain item) - => item.DomainName; + callback?.Invoke("done", 1, 1); + return actions; + } - /// - protected override string GetItemPath(IDomain item, bool useGuid, bool isFlat) - => $"{item.DomainName.ToSafeFileName(shortStringHelper)}_{item.LanguageIsoCode.ToSafeFileName(shortStringHelper)}"; + /// + protected override string GetItemName(IDomain item) + => item.DomainName; + /// + protected override string GetItemPath(IDomain item, bool useGuid, bool isFlat) + => $"{item.DomainName.ToSafeFileName(shortStringHelper)}_{item.LanguageIsoCode?.ToSafeFileName(shortStringHelper)}"; - /// - protected override IEnumerable GetChildItems(int parent) - { - if (parent == -1) - return domainService.GetAll(true) - .Where(x => x is IEntity) - .Select(x => x as IEntity); + /// + protected override IEnumerable GetChildItems(int parent) + { + if (parent == -1) + return domainService.GetAll(true) + .Where(x => x is IEntity) + .Select(x => x as IEntity); - return base.GetChildItems(parent); + return base.GetChildItems(parent); - } } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/LanguageHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/LanguageHandler.cs index 87126e5f..4c484916 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/LanguageHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/LanguageHandler.cs @@ -1,12 +1,12 @@ -using Microsoft.Extensions.Logging; - -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; +using Microsoft.Extensions.Logging; + using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -22,210 +22,211 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to mange language settings in uSync +/// +[SyncHandler(uSyncConstants.Handlers.LanguageHandler, "Languages", "Languages", uSyncConstants.Priorites.Languages, + Icon = "icon-globe", EntityType = UdiEntityType.Language, IsTwoPass = true)] +public class LanguageHandler : SyncHandlerBase, ISyncHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> { - /// - /// Handler to mange language settings in uSync - /// - [SyncHandler(uSyncConstants.Handlers.LanguageHandler, "Languages", "Languages", uSyncConstants.Priorites.Languages, - Icon = "icon-globe", EntityType = UdiEntityType.Language, IsTwoPass = true)] - public class LanguageHandler : SyncHandlerBase, ISyncHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + private readonly ILocalizationService localizationService; + + /// + public LanguageHandler( + ILogger logger, + IEntityService entityService, + ILocalizationService localizationService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) { - private readonly ILocalizationService localizationService; - - /// - public LanguageHandler( - ILogger logger, - IEntityService entityService, - ILocalizationService localizationService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { - this.localizationService = localizationService; - } - - /// - // language guids are not consistant (at least in alpha) - // so we don't save by Guid we save by ISO name everytime. - protected override string GetPath(string folder, ILanguage item, bool GuidNames, bool isFlat) - { - return Path.Combine(folder, $"{this.GetItemPath(item, GuidNames, isFlat)}.{this.uSyncConfig.Settings.DefaultExtension}"); - } + this.localizationService = localizationService; + } - /// - protected override string GetItemPath(ILanguage item, bool useGuid, bool isFlat) - => item.IsoCode.ToSafeFileName(shortStringHelper); + /// + // language guids are not consistant (at least in alpha) + // so we don't save by Guid we save by ISO name everytime. + protected override string GetPath(string folder, ILanguage item, bool GuidNames, bool isFlat) + { + return Path.Combine(folder, $"{this.GetItemPath(item, GuidNames, isFlat)}.{this.uSyncConfig.Settings.DefaultExtension}"); + } + /// + protected override string GetItemPath(ILanguage item, bool useGuid, bool isFlat) + => item.IsoCode.ToSafeFileName(shortStringHelper); - /// - /// ensure we import the 'default' language first, so we don't get errors doing it. - /// - protected override IEnumerable GetImportFiles(string folder) - { - var files = base.GetImportFiles(folder); - try - { - Dictionary ordered = new Dictionary(); - foreach (var file in files) - { - var node = XElement.Load(file); - var order = (node.Element("IsDefault").ValueOrDefault(false) ? "0" : "1") + Path.GetFileName(file); - ordered[file] = order; - } + /// + /// ensure we import the 'default' language first, so we don't get errors doing it. + /// + protected override IEnumerable GetImportFiles(string folder) + { + var files = base.GetImportFiles(folder); - return ordered.OrderBy(x => x.Value).Select(x => x.Key).ToList(); - } - catch + try + { + Dictionary ordered = new Dictionary(); + foreach (var file in files) { - return files; + var node = XElement.Load(file); + var order = (node.Element("IsDefault").ValueOrDefault(false) ? "0" : "1") + Path.GetFileName(file); + ordered[file] = order; } + return ordered.OrderBy(x => x.Value).Select(x => x.Key).ToList(); } - - /// - protected override IEnumerable GetChildItems(int parent) + catch { - if (parent == -1) - return localizationService.GetAllLanguages(); - - return Enumerable.Empty(); + return files; } - /// - protected override string GetItemName(ILanguage item) => item.IsoCode; + } - /// - protected override void CleanUp(ILanguage item, string newFile, string folder) - { - base.CleanUp(item, newFile, folder); + /// + protected override IEnumerable GetChildItems(int parent) + { + if (parent == -1) + return localizationService.GetAllLanguages(); - // for languages we also clean up by id. - // this happens when the language changes . - var physicalFile = syncFileService.GetAbsPath(newFile); - var installedLanguages = localizationService.GetAllLanguages() - .Select(x => x.IsoCode).ToList(); + return Enumerable.Empty(); + } - var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}"); + /// + protected override string GetItemName(ILanguage item) => item.IsoCode; - foreach (string file in files) - { - var node = syncFileService.LoadXElement(file); - var IsoCode = node.Element("IsoCode").ValueOrDefault(string.Empty); + /// + protected override void CleanUp(ILanguage item, string newFile, string folder) + { + base.CleanUp(item, newFile, folder); + + // for languages we also clean up by id. + // this happens when the language changes . + var physicalFile = syncFileService.GetAbsPath(newFile); + var installedLanguages = localizationService.GetAllLanguages() + .Select(x => x.IsoCode).ToList(); + + var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}"); + + foreach (string file in files) + { + var node = syncFileService.LoadXElement(file); + var IsoCode = node.Element("IsoCode").ValueOrDefault(string.Empty); - if (!String.IsNullOrWhiteSpace(IsoCode)) + if (!String.IsNullOrWhiteSpace(IsoCode)) + { + if (!file.InvariantEquals(physicalFile)) { - if (!file.InvariantEquals(physicalFile)) + // not the file we just saved, but matching IsoCode, we remove it. + if (node.Element("IsoCode").ValueOrDefault(string.Empty) == item.IsoCode) { - // not the file we just saved, but matching IsoCode, we remove it. - if (node.Element("IsoCode").ValueOrDefault(string.Empty) == item.IsoCode) + logger.LogDebug("Found Matching Lang File, cleaning"); + var attempt = serializer.SerializeEmpty(item, SyncActionType.Rename, node.GetAlias()); + if (attempt.Success && attempt.Item is not null) { - logger.LogDebug("Found Matching Lang File, cleaning"); - var attempt = serializer.SerializeEmpty(item, SyncActionType.Rename, node.GetAlias()); - if (attempt.Success) - { - syncFileService.SaveXElement(attempt.Item, file); - } + syncFileService.SaveXElement(attempt.Item, file); } } + } - if (!installedLanguages.InvariantContains(IsoCode)) + if (!installedLanguages.InvariantContains(IsoCode)) + { + // language is no longer installed, make the file empty. + logger.LogDebug("Language in file is not on the site, cleaning"); + var attempt = serializer.SerializeEmpty(item, SyncActionType.Delete, node.GetAlias()); + if (attempt.Success && attempt.Item is not null) { - // language is no longer installed, make the file empty. - logger.LogDebug("Language in file is not on the site, cleaning"); - var attempt = serializer.SerializeEmpty(item, SyncActionType.Delete, node.GetAlias()); - if (attempt.Success) - { - syncFileService.SaveXElement(attempt.Item, file); - } + syncFileService.SaveXElement(attempt.Item, file); } } } } + } - private static ConcurrentDictionary newLanguages = new ConcurrentDictionary(); + private static ConcurrentDictionary newLanguages = new ConcurrentDictionary(); - /// - public override void Handle(SavingNotification notification) + /// + public override void Handle(SavingNotification notification) + { + if (_mutexService.IsPaused) return; + + if (ShouldBlockRootChanges(notification.SavedEntities)) { - if (_mutexService.IsPaused) return; - - if (ShouldBlockRootChanges(notification.SavedEntities)) - { - notification.Cancel = true; - notification.Messages.Add(GetCancelMessageForRoots()); - return; - } + notification.Cancel = true; + notification.Messages.Add(GetCancelMessageForRoots()); + return; + } - foreach (var item in notification.SavedEntities) + foreach (var item in notification.SavedEntities) + { + // + if (item.Id == 0) { - // - if (item.Id == 0) - { - newLanguages[item.IsoCode] = item.CultureName; - // is new, we want to set this as a flag, so we don't do the full content save.n - // newLanguages.Add(item.IsoCode); - } + newLanguages[item.IsoCode] = item.CultureName; + // is new, we want to set this as a flag, so we don't do the full content save.n + // newLanguages.Add(item.IsoCode); } } + } - /// - public override void Handle(SavedNotification notification) - { - if (_mutexService.IsPaused) return; + /// + public override void Handle(SavedNotification notification) + { + if (_mutexService.IsPaused) return; - foreach (var item in notification.SavedEntities) + foreach (var item in notification.SavedEntities) + { + bool newItem = false; + if (newLanguages.Count > 0 && newLanguages.ContainsKey(item.IsoCode)) { - bool newItem = false; - if (newLanguages.Count > 0 && newLanguages.ContainsKey(item.IsoCode)) - { - newItem = true; - newLanguages.TryRemove(item.IsoCode, out string name); - } + newItem = true; + newLanguages.TryRemove(item.IsoCode, out string? name); + } - var targetFolders = GetDefaultHandlerFolders(); + var targetFolders = GetDefaultHandlerFolders(); - if (item.WasPropertyDirty("IsDefault")) - { - // changing, this change doesn't trigger a save of the other languages. - // so we need to save all language files. - this.ExportAll(targetFolders, DefaultConfig, null); - } + if (item.WasPropertyDirty("IsDefault")) + { + // changing, this change doesn't trigger a save of the other languages. + // so we need to save all language files. + this.ExportAll(targetFolders, DefaultConfig, null); + } - var attempts = Export(item,targetFolders, DefaultConfig); + var attempts = Export(item, targetFolders, DefaultConfig); - if (!newItem && item.WasPropertyDirty(nameof(ILanguage.IsoCode))) - { - // The language code changed, this can mean we need to do a full content export. - // + we should export the languages again! - uSyncTriggers.TriggerExport(targetFolders, new List() { - UdiEntityType.Document, UdiEntityType.Language }, null); - } + if (!newItem && item.WasPropertyDirty(nameof(ILanguage.IsoCode))) + { + // The language code changed, this can mean we need to do a full content export. + // + we should export the languages again! + uSyncTriggers.TriggerExport(targetFolders, new List() { + UdiEntityType.Document, UdiEntityType.Language }, null); + } - // we always clean up languages, because of the way they are stored. - foreach (var attempt in attempts.Where(x => x.Success)) - { - this.CleanUp(item, attempt.FileName, targetFolders.Last()); - } + // we always clean up languages, because of the way they are stored. + foreach (var attempt in attempts.Where(x => x.Success)) + { + if (attempt.FileName is null) continue; + this.CleanUp(item, attempt.FileName, targetFolders.Last()); } - } - /// - /// we don't support language deletion (because the keys are unstable) - /// - protected override IEnumerable DeleteMissingItems(int parentId, IEnumerable keys, bool reportOnly) - => Enumerable.Empty(); - + } } + + /// + /// we don't support language deletion (because the keys are unstable) + /// + protected override IEnumerable DeleteMissingItems(int parentId, IEnumerable keys, bool reportOnly) + => Enumerable.Empty(); + } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/MacroHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/MacroHandler.cs index 769afb64..ef9b8ed9 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/MacroHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/MacroHandler.cs @@ -1,96 +1,96 @@ -using Microsoft.Extensions.Logging; - -using System.Collections.Generic; -using System.Linq; - -using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; -using Umbraco.Extensions; - -using uSync.BackOffice.Configuration; -using uSync.BackOffice.Services; -using uSync.Core; - -using static Umbraco.Cms.Core.Constants; - -namespace uSync.BackOffice.SyncHandlers.Handlers -{ - /// - /// Handler to mange Macros in uSync - /// - [SyncHandler(uSyncConstants.Handlers.MacroHandler, "Macros", "Macros", uSyncConstants.Priorites.Macros, - Icon = "icon-settings-alt", EntityType = UdiEntityType.Macro)] - public class MacroHandler : SyncHandlerBase, ISyncHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> - { - private readonly IMacroService macroService; - - /// - public MacroHandler( - ILogger logger, - IEntityService entityService, - IMacroService macroService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { - this.macroService = macroService; - } - - /// - /// overrider the default export, because macros, don't exist as an object type??? - /// - public override IEnumerable ExportAll(int parent, string folder, HandlerSettings config, SyncUpdateCallback callback) - { - // we clean the folder out on an export all. - syncFileService.CleanFolder(folder); - - var actions = new List(); - - var items = macroService.GetAll().ToList(); - int count = 0; - foreach (var item in items) - { - count++; - callback?.Invoke(item.Name, count, items.Count); - actions.AddRange(Export(item, folder, config)); - } - - return actions; - } - - /// - protected override string GetItemName(IMacro item) - => item.Name; - - /// - protected override string GetItemFileName(IMacro item) - => GetItemAlias(item).ToSafeAlias(shortStringHelper); - - /// - protected override IEnumerable GetChildItems(int parent) - { - if (parent == -1) - { - return macroService.GetAll().Where(x => x is IEntity) - .Select(x => x as IEntity); - } - - return Enumerable.Empty(); - } - - } - -} +//using Microsoft.Extensions.Logging; + +//using System.Collections.Generic; +//using System.Linq; + +//using Umbraco.Cms.Core.Cache; +//using Umbraco.Cms.Core.Events; +//using Umbraco.Cms.Core.Models; +//using Umbraco.Cms.Core.Models.Entities; +//using Umbraco.Cms.Core.Notifications; +//using Umbraco.Cms.Core.Services; +//using Umbraco.Cms.Core.Strings; +//using Umbraco.Extensions; + +//using uSync.BackOffice.Configuration; +//using uSync.BackOffice.Services; +//using uSync.Core; + +//using static Umbraco.Cms.Core.Constants; + +//namespace uSync.BackOffice.SyncHandlers.Handlers +//{ +// /// +// /// Handler to mange Macros in uSync +// /// +// [SyncHandler(uSyncConstants.Handlers.MacroHandler, "Macros", "Macros", uSyncConstants.Priorites.Macros, +// Icon = "icon-settings-alt", EntityType = UdiEntityType.Macro)] +// public class MacroHandler : SyncHandlerBase, ISyncHandler, +// INotificationHandler>, +// INotificationHandler>, +// INotificationHandler>, +// INotificationHandler> +// { +// private readonly IMacroService macroService; + +// /// +// public MacroHandler( +// ILogger logger, +// IEntityService entityService, +// IMacroService macroService, +// AppCaches appCaches, +// IShortStringHelper shortStringHelper, +// SyncFileService syncFileService, +// uSyncEventService mutexService, +// uSyncConfigService uSyncConfig, +// ISyncItemFactory syncItemFactory) +// : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) +// { +// this.macroService = macroService; +// } + +// /// +// /// overrider the default export, because macros, don't exist as an object type??? +// /// +// public override IEnumerable ExportAll(int parent, string folder, HandlerSettings config, SyncUpdateCallback callback) +// { +// // we clean the folder out on an export all. +// syncFileService.CleanFolder(folder); + +// var actions = new List(); + +// var items = macroService.GetAll().ToList(); +// int count = 0; +// foreach (var item in items) +// { +// count++; +// callback?.Invoke(item.Name, count, items.Count); +// actions.AddRange(Export(item, folder, config)); +// } + +// return actions; +// } + +// /// +// protected override string GetItemName(IMacro item) +// => item.Name; + +// /// +// protected override string GetItemFileName(IMacro item) +// => GetItemAlias(item).ToSafeAlias(shortStringHelper); + +// /// +// protected override IEnumerable GetChildItems(int parent) +// { +// if (parent == -1) +// { +// return macroService.GetAll().Where(x => x is IEntity) +// .Select(x => x as IEntity); +// } + +// return Enumerable.Empty(); +// } + +// } + +//} diff --git a/uSync.BackOffice/SyncHandlers/Handlers/MediaHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/MediaHandler.cs index 94e09785..c3f46aaf 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/MediaHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/MediaHandler.cs @@ -17,68 +17,67 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers -{ - /// - /// Handler to mange Media items in uSync - /// - [SyncHandler(uSyncConstants.Handlers.MediaHandler, "Media", "Media", uSyncConstants.Priorites.Media, - Icon = "icon-picture", IsTwoPass = true, EntityType = UdiEntityType.Media)] - public class MediaHandler : ContentHandlerBase, ISyncHandler, ISyncCleanEntryHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> +namespace uSync.BackOffice.SyncHandlers.Handlers; - { - /// - public override string Group => uSyncConstants.Groups.Content; +/// +/// Handler to mange Media items in uSync +/// +[SyncHandler(uSyncConstants.Handlers.MediaHandler, "Media", "Media", uSyncConstants.Priorites.Media, + Icon = "icon-picture", IsTwoPass = true, EntityType = UdiEntityType.Media)] +public class MediaHandler : ContentHandlerBase, ISyncHandler, ISyncCleanEntryHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> - private readonly IMediaService mediaService; +{ + /// + public override string Group => uSyncConstants.Groups.Content; - /// - public MediaHandler( - ILogger logger, - IEntityService entityService, - IMediaService mediaService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfigService, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) - { - this.mediaService = mediaService; - } + private readonly IMediaService mediaService; - /// - protected override bool HasChildren(IMedia item) - => mediaService.HasChildren(item.Id); + /// + public MediaHandler( + ILogger logger, + IEntityService entityService, + IMediaService mediaService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfigService, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) + { + this.mediaService = mediaService; + } - /// - protected override IEnumerable GetChildItems(IEntity parent) + /// + protected override bool HasChildren(IMedia item) + => mediaService.HasChildren(item.Id); + + /// + protected override IEnumerable GetChildItems(IEntity? parent) + { + if (parent != null) { - if (parent != null) - { - var items = new List(); - const int pageSize = 5000; - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) - { - items.AddRange(mediaService.GetPagedChildren(parent.Id, page++, pageSize, out total)); - } - return items; - } - else + var items = new List(); + const int pageSize = 5000; + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) { - return mediaService.GetRootMedia(); + items.AddRange(mediaService.GetPagedChildren(parent.Id, page++, pageSize, out total)); } + return items; + } + else + { + return mediaService.GetRootMedia(); } } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/MediaTypeHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/MediaTypeHandler.cs index 188dad1c..3f15e16b 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/MediaTypeHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/MediaTypeHandler.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.Logging; +using System; -using System; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; @@ -17,66 +17,65 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to mange Media Types in uSync +/// +[SyncHandler(uSyncConstants.Handlers.MediaTypeHandler, "Media Types", "MediaTypes", uSyncConstants.Priorites.MediaTypes, + IsTwoPass = true, Icon = "icon-thumbnails", EntityType = UdiEntityType.MediaType)] +public class MediaTypeHandler : ContentTypeBaseHandler, ISyncHandler, ISyncGraphableHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler, + INotificationHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> { - /// - /// Handler to mange Media Types in uSync - /// - [SyncHandler(uSyncConstants.Handlers.MediaTypeHandler, "Media Types", "MediaTypes", uSyncConstants.Priorites.MediaTypes, - IsTwoPass = true, Icon = "icon-thumbnails", EntityType = UdiEntityType.MediaType)] - public class MediaTypeHandler : ContentTypeBaseHandler, ISyncHandler, ISyncGraphableHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler, - INotificationHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + private readonly IMediaTypeService mediaTypeService; + + /// + public MediaTypeHandler( + ILogger logger, + IEntityService entityService, + IMediaTypeService mediaTypeService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + { - private readonly IMediaTypeService mediaTypeService; + this.mediaTypeService = mediaTypeService; + } - /// - public MediaTypeHandler( - ILogger logger, - IEntityService entityService, - IMediaTypeService mediaTypeService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + /// + protected override string GetEntityTreeName(IUmbracoEntity item, bool useGuid) + { + if (useGuid) return item.Key.ToString(); + if (item is IMediaType mediaType) { - this.mediaTypeService = mediaTypeService; + return mediaType.Alias.ToSafeFileName(shortStringHelper); } - /// - protected override string GetEntityTreeName(IUmbracoEntity item, bool useGuid) - { - if (useGuid) return item.Key.ToString(); - - if (item is IMediaType mediaType) - { - return mediaType.Alias.ToSafeFileName(shortStringHelper); - } - - return item.Name.ToSafeFileName(shortStringHelper); - } + return item.Name?.ToSafeFileName(shortStringHelper) ?? item.Key.ToString(); + } - /// - protected override void DeleteFolder(int id) - => mediaTypeService.DeleteContainer(id); + /// + protected override void DeleteFolder(int id) + => mediaTypeService.DeleteContainer(id); - /// - protected override IEntity GetContainer(int id) - => mediaTypeService.GetContainer(id); + /// + protected override IEntity? GetContainer(int id) + => mediaTypeService.GetContainer(id); - /// - protected override IEntity GetContainer(Guid key) - => mediaTypeService.GetContainer(key); + /// + protected override IEntity? GetContainer(Guid key) + => mediaTypeService.GetContainer(key); - } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/MemberTypeHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/MemberTypeHandler.cs index f4c383e1..fb96c823 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/MemberTypeHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/MemberTypeHandler.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.Logging; +using System; -using System; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; @@ -17,65 +17,63 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to mange Member types in uSync +/// +[SyncHandler(uSyncConstants.Handlers.MemberTypeHandler, "Member Types", "MemberTypes", uSyncConstants.Priorites.MemberTypes, + IsTwoPass = true, Icon = "icon-users", EntityType = UdiEntityType.MemberType)] +public class MemberTypeHandler : ContentTypeBaseHandler, ISyncHandler, ISyncGraphableHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler, + INotificationHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> { - /// - /// Handler to mange Member types in uSync - /// - [SyncHandler(uSyncConstants.Handlers.MemberTypeHandler, "Member Types", "MemberTypes", uSyncConstants.Priorites.MemberTypes, - IsTwoPass = true, Icon = "icon-users", EntityType = UdiEntityType.MemberType)] - public class MemberTypeHandler : ContentTypeBaseHandler, ISyncHandler, ISyncGraphableHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler, - INotificationHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + private readonly IMemberTypeService memberTypeService; + + /// + public MemberTypeHandler( + ILogger logger, + IEntityService entityService, + IMemberTypeService memberTypeService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) { - private readonly IMemberTypeService memberTypeService; + this.memberTypeService = memberTypeService; + } - /// - public MemberTypeHandler( - ILogger logger, - IEntityService entityService, - IMemberTypeService memberTypeService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { - this.memberTypeService = memberTypeService; - } - - /// - protected override void DeleteFolder(int id) - => memberTypeService.DeleteContainer(id); + /// + protected override void DeleteFolder(int id) + => memberTypeService.DeleteContainer(id); - /// - protected override IEntity GetContainer(int id) - => memberTypeService.GetContainer(id); + /// + protected override IEntity? GetContainer(int id) + => memberTypeService.GetContainer(id); - /// - protected override IEntity GetContainer(Guid key) - => memberTypeService.GetContainer(key); + /// + protected override IEntity? GetContainer(Guid key) + => memberTypeService.GetContainer(key); - /// - protected override string GetEntityTreeName(IUmbracoEntity item, bool useGuid) - { - if (useGuid) return item.Key.ToString(); - - if (item is IMemberType memberType) - { - return memberType.Alias.ToSafeFileName(shortStringHelper); - } + /// + protected override string GetEntityTreeName(IUmbracoEntity item, bool useGuid) + { + if (useGuid) return item.Key.ToString(); - return item.Name.ToSafeFileName(shortStringHelper); + if (item is IMemberType memberType) + { + return memberType.Alias.ToSafeFileName(shortStringHelper); } - } + return item.Name?.ToSafeFileName(shortStringHelper) ?? item.Key.ToString(); + } } diff --git a/uSync.BackOffice/SyncHandlers/Handlers/RelationTypeHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/RelationTypeHandler.cs index f84190f3..842fd587 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/RelationTypeHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/RelationTypeHandler.cs @@ -1,9 +1,9 @@ -using Microsoft.Extensions.Logging; - -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Microsoft.Extensions.Logging; + using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -18,153 +18,152 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler to mange Relation types in uSync +/// +[SyncHandler(uSyncConstants.Handlers.RelationTypeHandler, "Relations", + "RelationTypes", uSyncConstants.Priorites.RelationTypes, + Icon = "icon-link", + EntityType = UdiEntityType.RelationType, IsTwoPass = false)] +public class RelationTypeHandler : SyncHandlerBase, ISyncHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> { - /// - /// Handler to mange Relation types in uSync - /// - [SyncHandler(uSyncConstants.Handlers.RelationTypeHandler, "Relations", - "RelationTypes", uSyncConstants.Priorites.RelationTypes, - Icon = "icon-link", - EntityType = UdiEntityType.RelationType, IsTwoPass = false)] - public class RelationTypeHandler : SyncHandlerBase, ISyncHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> + private readonly IRelationService relationService; + + /// + public override string Group => uSyncConstants.Groups.Content; + + /// + public RelationTypeHandler( + ILogger logger, + IEntityService entityService, + IRelationService relationService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfigService, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) { - private readonly IRelationService relationService; - - /// - public override string Group => uSyncConstants.Groups.Content; - - /// - public RelationTypeHandler( - ILogger logger, - IEntityService entityService, - IRelationService relationService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfigService, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) - { - this.relationService = relationService; - } - - /// - public override IEnumerable ExportAll(string folder, HandlerSettings config, SyncUpdateCallback callback) - { - var actions = new List(); + this.relationService = relationService; + } - var items = relationService.GetAllRelationTypes().ToList(); + /// + public override IEnumerable ExportAll(string folder, HandlerSettings config, SyncUpdateCallback? callback) + { + var actions = new List(); - foreach (var item in items.Select((relationType, index) => new { relationType, index })) - { - callback?.Invoke(item.relationType.Name, item.index, items.Count); - actions.AddRange(Export(item.relationType, folder, config)); - } + var items = relationService.GetAllRelationTypes().ToList(); - return actions; + foreach (var item in items.Select((relationType, index) => new { relationType, index })) + { + callback?.Invoke(item.relationType.Name ?? item.relationType.Alias, item.index, items.Count); + actions.AddRange(Export(item.relationType, folder, config)); } + return actions; + } - /// - /// Relations that by default we exclude, if the exlude setting is used,then it will override these values - /// and they will be included if not explicity set; - /// - private const string defaultRelations = "relateParentDocumentOnDelete,relateParentMediaFolderOnDelete,relateDocumentOnCopy,umbMedia,umbDocument"; - /// - /// Workout if we are excluding this relationType from export/import - /// - protected override bool ShouldExport(XElement node, HandlerSettings config) - { - var exclude = config.GetSetting("Exclude", defaultRelations); + /// + /// Relations that by default we exclude, if the exlude setting is used,then it will override these values + /// and they will be included if not explicity set; + /// + private const string defaultRelations = "relateParentDocumentOnDelete,relateParentMediaFolderOnDelete,relateDocumentOnCopy,umbMedia,umbDocument"; - if (!string.IsNullOrWhiteSpace(exclude) && exclude.Contains(node.GetAlias())) - return false; + /// + /// Workout if we are excluding this relationType from export/import + /// + protected override bool ShouldExport(XElement node, HandlerSettings config) + { + var exclude = config.GetSetting("Exclude", defaultRelations); - return true; - } + if (!string.IsNullOrWhiteSpace(exclude) && exclude.Contains(node.GetAlias())) + return false; - /// - protected override bool ShouldImport(XElement node, HandlerSettings config) - => ShouldExport(node, config); - - - /// - protected override string GetItemName(IRelationType item) - => item.Name; - - /// - protected override string GetItemFileName(IRelationType item) - => GetItemAlias(item).ToSafeAlias(shortStringHelper); - - // private void RelationService_SavedRelation(IRelationService sender, Umbraco.Core.Events.SaveEventArgs e) - // { - // if (uSync8BackOffice.eventsPaused) return; - - // lock (saveLock) - // { - // saveTimer.Stop(); - // saveTimer.Start(); - - // // add each item to the save list (if we haven't already) - // foreach (var item in e.SavedEntities) - // { - // if (!pendingSaveIds.Contains(item.RelationTypeId)) - // pendingSaveIds.Add(item.RelationTypeId); - // } - // } - // } - - // private void SaveTimer_Elapsed(object sender, ElapsedEventArgs e) - // { - // lock (saveLock) - // { - // UpdateRelationTypes(pendingSaveIds); - // pendingSaveIds.Clear(); - // } - // } - - // private static Timer saveTimer; - // private static List pendingSaveIds; - // private static object saveLock; - - // private void RelationService_DeletedRelation(IRelationService sender, Umbraco.Core.Events.DeleteEventArgs e) - // { - // if (uSync8BackOffice.eventsPaused) return; - - // var types = new List(); - - // foreach (var item in e.DeletedEntities) - // { - // if (!types.Contains(item.RelationTypeId)) - // types.Add(item.RelationTypeId); - // } - - // UpdateRelationTypes(types); - // } - - // private void UpdateRelationTypes(IEnumerable types) - // { - // foreach (var type in types) - // { - // var relationType = relationService.GetRelationTypeById(type); - - // var attempts = Export(relationType, Path.Combine(rootFolder, this.DefaultFolder), DefaultConfig); - - // if (!(this.DefaultConfig.GuidNames && this.DefaultConfig.UseFlatStructure)) - // { - // foreach (var attempt in attempts.Where(x => x.Success)) - // { - // this.CleanUp(relationType, attempt.FileName, Path.Combine(rootFolder, this.DefaultFolder)); - // } - // } - // } - // } + return true; } + + /// + protected override bool ShouldImport(XElement node, HandlerSettings config) + => ShouldExport(node, config); + + + /// + protected override string GetItemName(IRelationType item) + => item.Name ?? item.Alias; + + /// + protected override string GetItemFileName(IRelationType item) + => GetItemAlias(item).ToSafeAlias(shortStringHelper); + + // private void RelationService_SavedRelation(IRelationService sender, Umbraco.Core.Events.SaveEventArgs e) + // { + // if (uSync8BackOffice.eventsPaused) return; + + // lock (saveLock) + // { + // saveTimer.Stop(); + // saveTimer.Start(); + + // // add each item to the save list (if we haven't already) + // foreach (var item in e.SavedEntities) + // { + // if (!pendingSaveIds.Contains(item.RelationTypeId)) + // pendingSaveIds.Add(item.RelationTypeId); + // } + // } + // } + + // private void SaveTimer_Elapsed(object sender, ElapsedEventArgs e) + // { + // lock (saveLock) + // { + // UpdateRelationTypes(pendingSaveIds); + // pendingSaveIds.Clear(); + // } + // } + + // private static Timer saveTimer; + // private static List pendingSaveIds; + // private static object saveLock; + + // private void RelationService_DeletedRelation(IRelationService sender, Umbraco.Core.Events.DeleteEventArgs e) + // { + // if (uSync8BackOffice.eventsPaused) return; + + // var types = new List(); + + // foreach (var item in e.DeletedEntities) + // { + // if (!types.Contains(item.RelationTypeId)) + // types.Add(item.RelationTypeId); + // } + + // UpdateRelationTypes(types); + // } + + // private void UpdateRelationTypes(IEnumerable types) + // { + // foreach (var type in types) + // { + // var relationType = relationService.GetRelationTypeById(type); + + // var attempts = Export(relationType, Path.Combine(rootFolder, this.DefaultFolder), DefaultConfig); + + // if (!(this.DefaultConfig.GuidNames && this.DefaultConfig.UseFlatStructure)) + // { + // foreach (var attempt in attempts.Where(x => x.Success)) + // { + // this.CleanUp(relationType, attempt.FileName, Path.Combine(rootFolder, this.DefaultFolder)); + // } + // } + // } + // } } \ No newline at end of file diff --git a/uSync.BackOffice/SyncHandlers/Handlers/TemplateHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/TemplateHandler.cs index b76f7aab..85056daa 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/TemplateHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/TemplateHandler.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Logging; - -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.Logging; + using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -18,53 +18,52 @@ using static Umbraco.Cms.Core.Constants; -namespace uSync.BackOffice.SyncHandlers.Handlers +namespace uSync.BackOffice.SyncHandlers.Handlers; + +/// +/// Handler for Template items in Umbraco +/// +[SyncHandler(uSyncConstants.Handlers.TemplateHandler, "Templates", "Templates", uSyncConstants.Priorites.Templates, + Icon = "icon-layout", EntityType = UdiEntityType.Template, IsTwoPass = true)] +public class TemplateHandler : SyncHandlerLevelBase, ISyncHandler, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler>, + INotificationHandler> { - /// - /// Handler for Template items in Umbraco - /// - [SyncHandler(uSyncConstants.Handlers.TemplateHandler, "Templates", "Templates", uSyncConstants.Priorites.Templates, - Icon = "icon-layout", EntityType = UdiEntityType.Template, IsTwoPass = true)] - public class TemplateHandler : SyncHandlerLevelBase, ISyncHandler, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler>, - INotificationHandler> - { - private readonly IFileService fileService; + private readonly IFileService _fileService; - /// - public TemplateHandler( - ILogger logger, - IEntityService entityService, - IFileService fileService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { - this.fileService = fileService; - } + /// + public TemplateHandler( + ILogger logger, + IEntityService entityService, + IFileService fileService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + { + this._fileService = fileService; + } - /// - protected override string GetItemName(ITemplate item) => item.Name; + /// + protected override string GetItemName(ITemplate item) => item.Name ?? item.Alias; - /// - protected override IEnumerable GetChildItems(int parent) - => fileService.GetTemplates(parent).Where(x => x is IEntity) - .Select(x => x as IEntity); + /// + protected override IEnumerable GetChildItems(int parent) + => _fileService.GetTemplates(parent).Where(x => x is IEntity) + .Select(x => x as IEntity); - /// - protected override IEnumerable GetFolders(int parent) - => GetChildItems(parent); + /// + protected override IEnumerable GetFolders(int parent) + => GetChildItems(parent); - /// - protected override string GetItemPath(ITemplate item, bool useGuid, bool isFlat) - => useGuid ? item.Key.ToString() : item.Alias.ToSafeFileName(shortStringHelper); - } + /// + protected override string GetItemPath(ITemplate item, bool useGuid, bool isFlat) + => useGuid ? item.Key.ToString() : item.Alias.ToSafeFileName(shortStringHelper); } diff --git a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncCleanEntryHandler.cs b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncCleanEntryHandler.cs index 7d75b277..cd613bc3 100644 --- a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncCleanEntryHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncCleanEntryHandler.cs @@ -2,16 +2,15 @@ using uSync.BackOffice.Configuration; -namespace uSync.BackOffice.SyncHandlers.Interfaces +namespace uSync.BackOffice.SyncHandlers.Interfaces; + +/// +/// Interface for handlers that can process cleaning the folder with _clean files +/// +public interface ISyncCleanEntryHandler { /// - /// Interface for handlers that can process cleaning the folder with _clean files + /// process any clean actions that have been identified during the import /// - public interface ISyncCleanEntryHandler - { - /// - /// process any clean actions that have been identified during the import - /// - IEnumerable ProcessCleanActions(string folder, IEnumerable actions, HandlerSettings config); - } + IEnumerable ProcessCleanActions(string? folder, IEnumerable actions, HandlerSettings config); } diff --git a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncGraphableHandler.cs b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncGraphableHandler.cs index 26738023..88ee66ae 100644 --- a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncGraphableHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncGraphableHandler.cs @@ -2,19 +2,18 @@ using System.Collections.Generic; using System.Xml.Linq; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// graphable handler - handler can use a Topological graph +/// to sort items into the most efficient order +/// +public interface ISyncGraphableHandler { /// - /// graphable handler - handler can use a Topological graph - /// to sort items into the most efficient order + /// return /// - public interface ISyncGraphableHandler - { - /// - /// return - /// - /// - /// - public IEnumerable GetGraphIds(XElement node); - } + /// + /// + public IEnumerable GetGraphIds(XElement node); } diff --git a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs index 1c24ee81..e5729f44 100644 --- a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs @@ -10,210 +10,209 @@ using uSync.Core.Dependency; using uSync.Core.Models; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// 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 { /// - /// 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 - { - /// - /// alias for handler, used when finding a handler - /// - string Alias { get; } - - /// - /// display name for handler - /// - string Name { get; } - - /// - /// priority order for handler - /// - int Priority { get; } - - /// - /// default folder name for handler - /// - string DefaultFolder { get; } - - /// - /// Icon to use in the UI when this handler is displayed - /// - string Icon { get; } - - /// - /// type of model handler works with - /// - string ItemType { get; } - - /// - /// is the handler enabled. - /// - bool Enabled { get; } - - /// - /// default config for the handler - when being used in events. - /// - HandlerSettings DefaultConfig { get; set; } - - /// - /// Group handler belongs too - /// - string Group { get; } - - /// - /// Umbraco entity type manged by the handler - /// - string EntityType { get; } - - /// - /// The type name of the items handled (Item.getType().ToString()) - /// - string TypeName { get; } - - /// - /// Export an item based on the int id value in umbraco - /// - /// - /// these export methods do not obay roots, there are for use - /// only when exporting to a custom folder. - /// - IEnumerable Export(int id, string folder, HandlerSettings settings); - - /// - /// Export an item based on the Udi value of the item - /// - /// - /// these export methods do not obay roots, there are for use - /// only when exporting to a custom folder. - /// - IEnumerable Export(Udi udi, string folder, HandlerSettings settings); - - /// - /// Export all items - /// - /// folder to use when exporting - /// Handler settings to use for export - /// Callbacks to keep UI up to date - /// List of actions detailing changes - [Obsolete("Call method with folders for roots functionality will be removed in v15")] - IEnumerable ExportAll(string folder, HandlerSettings settings, SyncUpdateCallback callback); - - /// - /// Export all items - /// - /// folders to use when exporting - /// Handler settings to use for export - /// Callbacks to keep UI up to date - /// List of actions detailing changes - IEnumerable ExportAll(string[] folders, HandlerSettings settings, SyncUpdateCallback callback) - => ExportAll(folders[0], settings, callback); // default implementation stops breaking change - - - /// - /// Get any dependencies required to full import this item - /// - IEnumerable GetDependencies(int id, DependencyFlags flags); - /// - /// Get any dependencies required to full import this item - /// - IEnumerable GetDependencies(Guid key, DependencyFlags flags); - - /// - /// Get an XML representation of an item based on its UDI value - /// - SyncAttempt GetElement(Udi udi); - - /// - /// Import an item from disk defined by the file name - /// - IEnumerable Import(string file, HandlerSettings settings, bool force); - - /// - /// Import All items - /// - /// 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 upto date - /// List of actions detailing changes - [Obsolete("Call method with folders for roots functionality will be removed in v15")] - IEnumerable ImportAll(string folder, HandlerSettings settings, bool force, SyncUpdateCallback callback); - - /// - /// Import All items - /// - /// folders to use when Importing - /// Handler settings to use for import - /// Import options to use - /// List of actions detailing changes - IEnumerable ImportAll(string[] folders, HandlerSettings settings, uSyncImportOptions options) - => ImportAll(folders[0], settings, options.Flags.HasFlag(Core.Serialization.SerializerFlags.Force), options.Callbacks?.Update); - - /// - /// Import from a single node. - /// - IEnumerable ImportElement(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options); - - /// - /// Report All items - /// - /// folder to use when reporting - /// Handler settings to use for report - /// Callbacks to keep UI upto date - /// List of actions detailing changes - [Obsolete("Call method with folders for roots functionality will be removed in v15")] - IEnumerable Report(string folder, HandlerSettings settings, SyncUpdateCallback callback); - - /// - /// Report All items - /// - /// folders to use when reporting - /// Handler settings to use for report - /// Callbacks to keep UI upto date - /// List of actions detailing changes - IEnumerable Report(string[] folders, HandlerSettings settings, SyncUpdateCallback callback) - => Report(folders[0], settings, callback); - - /// - /// Report a single item based on loaded uSync xml - /// - IEnumerable ReportElement(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options); - - - /// - /// Import the second pass of an item. - /// - IEnumerable ImportSecondPass(uSyncAction action, HandlerSettings settings, uSyncImportOptions options); - - /// - /// default implementation, root handler does do this. - /// - Udi FindFromNode(XElement node) => null; - - /// - /// 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) { } - - /// - /// fetch all the nodes that are needed for an report/import. - /// - public IReadOnlyList FetchAllNodes(string[] folders) - => new List(); - } + /// alias for handler, used when finding a handler + /// + string Alias { get; } + + /// + /// display name for handler + /// + string Name { get; } + + /// + /// priority order for handler + /// + int Priority { get; } + + /// + /// default folder name for handler + /// + string DefaultFolder { get; } + + /// + /// Icon to use in the UI when this handler is displayed + /// + string Icon { get; } + + /// + /// type of model handler works with + /// + string ItemType { get; } + + /// + /// is the handler enabled. + /// + bool Enabled { get; } + + /// + /// default config for the handler - when being used in events. + /// + HandlerSettings DefaultConfig { get; set; } + + /// + /// Group handler belongs too + /// + string Group { get; } + + /// + /// Umbraco entity type manged by the handler + /// + string EntityType { get; } + + /// + /// The type name of the items handled (Item.getType().ToString()) + /// + string TypeName { get; } + + /// + /// Export an item based on the int id value in umbraco + /// + /// + /// these export methods do not obay roots, there are for use + /// only when exporting to a custom folder. + /// + IEnumerable Export(int id, string folder, HandlerSettings settings); + + /// + /// Export an item based on the Udi value of the item + /// + /// + /// these export methods do not obay roots, there are for use + /// only when exporting to a custom folder. + /// + IEnumerable Export(Udi udi, string folder, HandlerSettings settings); + + /// + /// Export all items + /// + /// folder to use when exporting + /// Handler settings to use for export + /// Callbacks to keep UI up to date + /// List of actions detailing changes + [Obsolete("Call method with folders for roots functionality will be removed in v15")] + IEnumerable ExportAll(string folder, HandlerSettings settings, SyncUpdateCallback? callback); + + /// + /// Export all items + /// + /// folders to use when exporting + /// Handler settings to use for export + /// Callbacks to keep UI up to date + /// List of actions detailing changes + IEnumerable ExportAll(string[] folders, HandlerSettings settings, SyncUpdateCallback? callback) + => ExportAll(folders[0], settings, callback); // default implementation stops breaking change + + + /// + /// Get any dependencies required to full import this item + /// + IEnumerable GetDependencies(int id, DependencyFlags flags); + /// + /// Get any dependencies required to full import this item + /// + IEnumerable GetDependencies(Guid key, DependencyFlags flags); + + /// + /// Get an XML representation of an item based on its UDI value + /// + SyncAttempt GetElement(Udi udi); + + /// + /// Import an item from disk defined by the file name + /// + IEnumerable Import(string file, HandlerSettings settings, bool force); + + /// + /// Import All items + /// + /// 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 upto date + /// List of actions detailing changes + [Obsolete("Call method with folders for roots functionality will be removed in v15")] + IEnumerable ImportAll(string folder, HandlerSettings settings, bool force, SyncUpdateCallback? callback); + + /// + /// Import All items + /// + /// folders to use when Importing + /// Handler settings to use for import + /// Import options to use + /// List of actions detailing changes + IEnumerable ImportAll(string[] folders, HandlerSettings settings, uSyncImportOptions options) + => ImportAll(folders[0], settings, options.Flags.HasFlag(Core.Serialization.SerializerFlags.Force), options.Callbacks?.Update); + + /// + /// Import from a single node. + /// + IEnumerable ImportElement(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options); + + /// + /// Report All items + /// + /// folder to use when reporting + /// Handler settings to use for report + /// Callbacks to keep UI upto date + /// List of actions detailing changes + [Obsolete("Call method with folders for roots functionality will be removed in v15")] + IEnumerable Report(string folder, HandlerSettings settings, SyncUpdateCallback? callback); + + /// + /// Report All items + /// + /// folders to use when reporting + /// Handler settings to use for report + /// Callbacks to keep UI upto date + /// List of actions detailing changes + IEnumerable Report(string[] folders, HandlerSettings settings, SyncUpdateCallback? callback) + => Report(folders[0], settings, callback); + + /// + /// Report a single item based on loaded uSync xml + /// + IEnumerable ReportElement(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options); + + + /// + /// Import the second pass of an item. + /// + IEnumerable ImportSecondPass(uSyncAction action, HandlerSettings settings, uSyncImportOptions options); + + /// + /// default implementation, root handler does do this. + /// + Udi? FindFromNode(XElement node) => null; + + /// + /// 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) { } + + /// + /// fetch all the nodes that are needed for an report/import. + /// + public IReadOnlyList FetchAllNodes(string[] folders) + => new List(); } diff --git a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncPostImportHanlder.cs b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncPostImportHanlder.cs index 1d285335..8a1e046e 100644 --- a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncPostImportHanlder.cs +++ b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncPostImportHanlder.cs @@ -3,36 +3,35 @@ using uSync.BackOffice.Configuration; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// handlers that also need to be called at the end (when everything has been processed) +/// +/// examples of this ? dataTypes that reference docTypes - because docTypes comes in after dataTypes +/// +public interface ISyncPostImportHandler { /// - /// handlers that also need to be called at the end (when everything has been processed) - /// - /// examples of this ? dataTypes that reference docTypes - because docTypes comes in after dataTypes + /// Process items again once all other handlers have performed their import /// - public interface ISyncPostImportHandler - { - /// - /// Process items again once all other handlers have performed their import - /// - /// - /// Some handlers require that import actions are performed after all other handlers have been - /// processed. - /// - /// the prime example for this is a datatype that refrences doctypes. Datatypes are required - /// to be imported before doctypes, but then the post import step has to run so the datatype - /// can refrence the doctypes that may not have been there first time around. - /// - /// List of actions containing items that require post import processing - /// Handler settings to use for processing - /// List of actions detailing post import changes - IEnumerable ProcessPostImport(IEnumerable actions, HandlerSettings config) - => ProcessPostImport(string.Empty, actions, config); + /// + /// Some handlers require that import actions are performed after all other handlers have been + /// processed. + /// + /// the prime example for this is a datatype that refrences doctypes. Datatypes are required + /// to be imported before doctypes, but then the post import step has to run so the datatype + /// can refrence the doctypes that may not have been there first time around. + /// + /// List of actions containing items that require post import processing + /// Handler settings to use for processing + /// List of actions detailing post import changes + IEnumerable ProcessPostImport(IEnumerable actions, HandlerSettings config) + => ProcessPostImport(string.Empty, actions, config); - /// - /// Process import of a folder. - /// - [Obsolete("Folder is not required on post import will be removed in v15")] - IEnumerable ProcessPostImport(string folder, IEnumerable actions, HandlerSettings config); - } + /// + /// Process import of a folder. + /// + [Obsolete("Folder is not required on post import will be removed in v15")] + IEnumerable ProcessPostImport(string folder, IEnumerable actions, HandlerSettings config); } diff --git a/uSync.BackOffice/SyncHandlers/Models/HandlerActionNames.cs b/uSync.BackOffice/SyncHandlers/Models/HandlerActionNames.cs index c1ce5f77..a852d4a4 100644 --- a/uSync.BackOffice/SyncHandlers/Models/HandlerActionNames.cs +++ b/uSync.BackOffice/SyncHandlers/Models/HandlerActionNames.cs @@ -1,60 +1,59 @@ using System; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// Possible actions a handler can do (stored in config) +/// +public enum HandlerActions { /// - /// Possible actions a handler can do (stored in config) + /// No Action /// - public enum HandlerActions - { - /// - /// No Action - /// - [SyncActionName("")] - None, - - /// - /// Report action - /// - [SyncActionName("report")] - Report, - - /// - /// Import Action - /// - [SyncActionName("import")] - Import, - - /// - /// Export Action - /// - [SyncActionName("export")] - Export, - - /// - /// Save Action (triggered via Umbraco save/move events) - /// - [SyncActionName("save")] - Save, - - /// - /// All actions - /// - [SyncActionName("All")] - All - } + [SyncActionName("")] + None, - [AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = false)] - internal class SyncActionName : Attribute - { - private readonly string name; + /// + /// Report action + /// + [SyncActionName("report")] + Report, + + /// + /// Import Action + /// + [SyncActionName("import")] + Import, + + /// + /// Export Action + /// + [SyncActionName("export")] + Export, - public SyncActionName(string name) - { - this.name = name; - } + /// + /// Save Action (triggered via Umbraco save/move events) + /// + [SyncActionName("save")] + Save, - public override string ToString() - => this.name; + /// + /// All actions + /// + [SyncActionName("All")] + All +} + +[AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = false)] +internal class SyncActionName : Attribute +{ + private readonly string name; + + public SyncActionName(string name) + { + this.name = name; } + + public override string ToString() + => this.name; } diff --git a/uSync.BackOffice/SyncHandlers/Models/HandlerConfigPair.cs b/uSync.BackOffice/SyncHandlers/Models/HandlerConfigPair.cs index ea98281a..be606358 100644 --- a/uSync.BackOffice/SyncHandlers/Models/HandlerConfigPair.cs +++ b/uSync.BackOffice/SyncHandlers/Models/HandlerConfigPair.cs @@ -1,90 +1,86 @@ - -using Microsoft.AspNetCore.DataProtection.KeyManagement; - -using Umbraco.Extensions; +using Umbraco.Extensions; using uSync.BackOffice.Configuration; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// A Hanlder and its configuration +/// +public class HandlerConfigPair { /// - /// A Hanlder and its configuration + /// Sync handler /// - public class HandlerConfigPair - { - /// - /// Sync handler - /// - public ISyncHandler Handler { get; set; } + public required ISyncHandler Handler { get; set; } - /// - /// Loaded configuration for a handler - /// - public HandlerSettings Settings { get; set; } - } + /// + /// Loaded configuration for a handler + /// + public required HandlerSettings Settings { get; set; } +} + +/// +/// Extension methods for HandlerConfig pair class +/// +public static class HandlerConfigPairExtensions +{ + /// + /// Is the handler enabled + /// + public static bool IsEnabled(this HandlerConfigPair handlerAndConfig) + => handlerAndConfig.Handler.Enabled && handlerAndConfig.Settings.Enabled; /// - /// Extension methods for HandlerConfig pair class + /// Is the handler valid for the given group? /// - public static class HandlerConfigPairExtensions + public static bool IsValidGroup(this HandlerConfigPair handlerAndConfig, string group) { - /// - /// Is the handler enabled - /// - public static bool IsEnabled(this HandlerConfigPair handlerAndConfig) - => handlerAndConfig.Handler.Enabled && handlerAndConfig.Settings.Enabled; + // empty means all as does 'all' + if (string.IsNullOrWhiteSpace(group) || group.InvariantEquals("all")) return true; - /// - /// Is the handler valid for the given group? - /// - public static bool IsValidGroup(this HandlerConfigPair handlerAndConfig, string group) + var handlerGroup = handlerAndConfig.Handler.Group; + if (!string.IsNullOrWhiteSpace(handlerAndConfig.Settings.Group)) { - // empty means all as does 'all' - if (string.IsNullOrWhiteSpace(group) || group.InvariantEquals("all")) return true; - - var handlerGroup = handlerAndConfig.Handler.Group; - if (!string.IsNullOrWhiteSpace(handlerAndConfig.Settings.Group)) - { - handlerGroup = handlerAndConfig.Settings.Group; - } - - return group.InvariantContains(handlerGroup); + handlerGroup = handlerAndConfig.Settings.Group; } - /// - /// Is the handler valid for the given action - /// - public static bool IsValidAction(this HandlerConfigPair handlerAndConfig, HandlerActions action) - { - if (action.IsValidAction(handlerAndConfig.Settings.Actions)) return true; - return false; - } + return group.InvariantContains(handlerGroup); + } - /// - /// What group is the handler configured to be in - /// - /// - /// Gets the group from config or returns the default group for the selected handler - /// - public static string GetConfigGroup(this HandlerConfigPair handlerConfigPair) - { - if (!string.IsNullOrWhiteSpace(handlerConfigPair.Settings.Group)) - return handlerConfigPair.Settings.Group; + /// + /// Is the handler valid for the given action + /// + public static bool IsValidAction(this HandlerConfigPair handlerAndConfig, HandlerActions action) + { + if (action.IsValidAction(handlerAndConfig.Settings.Actions)) return true; + return false; + } - return handlerConfigPair.Handler.Group; - } + /// + /// What group is the handler configured to be in + /// + /// + /// Gets the group from config or returns the default group for the selected handler + /// + public static string GetConfigGroup(this HandlerConfigPair handlerConfigPair) + { + if (!string.IsNullOrWhiteSpace(handlerConfigPair.Settings.Group)) + return handlerConfigPair.Settings.Group; - /// - /// Get the icon for the group the handler belongs to - /// - public static string GetGroupIcon(this HandlerConfigPair handlerConfigPair) - { - var group = GetConfigGroup(handlerConfigPair); + return handlerConfigPair.Handler.Group; + } + + /// + /// Get the icon for the group the handler belongs to + /// + public static string GetGroupIcon(this HandlerConfigPair handlerConfigPair) + { + var group = GetConfigGroup(handlerConfigPair); - if (uSyncConstants.Groups.Icons.ContainsKey(group)) - return uSyncConstants.Groups.Icons[group]; + if (uSyncConstants.Groups.Icons.ContainsKey(group)) + return uSyncConstants.Groups.Icons[group]; - return handlerConfigPair.Handler.Icon; - } + return handlerConfigPair.Handler.Icon; } } diff --git a/uSync.BackOffice/SyncHandlers/Models/SyncHandlerAttribute.cs b/uSync.BackOffice/SyncHandlers/Models/SyncHandlerAttribute.cs index c15090ac..2a83803d 100644 --- a/uSync.BackOffice/SyncHandlers/Models/SyncHandlerAttribute.cs +++ b/uSync.BackOffice/SyncHandlers/Models/SyncHandlerAttribute.cs @@ -1,56 +1,57 @@ using System; -namespace uSync.BackOffice.SyncHandlers +using static Umbraco.Cms.Core.Constants; + +namespace uSync.BackOffice.SyncHandlers; + +/// +/// Attribute used to markup a handler in code. +/// +public class SyncHandlerAttribute : Attribute { /// - /// Attribute used to markup a handler in code. + /// Construct a new SyncHandlerAttribute /// - public class SyncHandlerAttribute : Attribute + public SyncHandlerAttribute(string alias, string name, string folder, int priority) { - /// - /// Construct a new SyncHandlerAttribute - /// - public SyncHandlerAttribute(string alias, string name, string folder, int priority) - { - Alias = alias; - Name = name; - Priority = priority; - Folder = folder; - } - - /// - /// Name of the handler - /// - public string Name { get; set; } - - /// - /// Alias used when finding the handler - /// - public string Alias { get; set; } - - /// - /// order of execution (lower is first) - /// - public int Priority { get; set; } - - /// - /// default folder name for items to be stored in - /// - public string Folder { get; set; } - - /// - /// does the handler require two passes at an item to import it. - /// - public bool IsTwoPass { get; set; } = false; - - /// - /// icon for handler used in UI - /// - public string Icon { get; set; } - - /// - /// Umbraco Entity type handler works with - /// - public string EntityType { get; set; } + Alias = alias; + Name = name; + Priority = priority; + Folder = folder; } + + /// + /// Name of the handler + /// + public string Name { get; set; } + + /// + /// Alias used when finding the handler + /// + public string Alias { get; set; } + + /// + /// order of execution (lower is first) + /// + public int Priority { get; set; } + + /// + /// default folder name for items to be stored in + /// + public string Folder { get; set; } + + /// + /// does the handler require two passes at an item to import it. + /// + public bool IsTwoPass { get; set; } = false; + + /// + /// icon for handler used in UI + /// + public string Icon { get; set; } = "icon-bug"; + + /// + /// Umbraco Entity type handler works with + /// + public string EntityType { get; set; } = UdiEntityType.Unknown; } diff --git a/uSync.BackOffice/SyncHandlers/Models/SyncHandlerOptions.cs b/uSync.BackOffice/SyncHandlers/Models/SyncHandlerOptions.cs index 66030e42..70fe9e49 100644 --- a/uSync.BackOffice/SyncHandlers/Models/SyncHandlerOptions.cs +++ b/uSync.BackOffice/SyncHandlers/Models/SyncHandlerOptions.cs @@ -1,77 +1,74 @@ -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// options that define how we define a handler +/// +public class SyncHandlerOptions { /// - /// options that define how we define a handler + /// Handler grouping (settings, content, etc) /// - public class SyncHandlerOptions - { - /// - /// Handler grouping (settings, content, etc) - /// - public string Group { get; set; } = string.Empty; - - /// - /// What action do you want to perform. - /// - public HandlerActions Action { get; set; } = HandlerActions.None; - - /// - /// handler set - /// - public string Set { get; set; } = uSync.Sets.DefaultSet; + public string Group { get; set; } = string.Empty; - /// - /// include handlers that are by default disabled - /// - public bool IncludeDisabled { get; set; } = false; + /// + /// What action do you want to perform. + /// + public HandlerActions Action { get; set; } = HandlerActions.None; - /// - /// the user id doing all the work. - /// - public int UserId { get; set; } = -1; + /// + /// handler set + /// + public string Set { get; set; } = uSync.Sets.DefaultSet; - /// - /// Default constructor - /// - public SyncHandlerOptions() { } + /// + /// include handlers that are by default disabled + /// + public bool IncludeDisabled { get; set; } = false; - /// - /// Construct Options for a given set - /// - public SyncHandlerOptions(string setName) - : this() - { - this.Set = setName; - } + /// + /// the user id doing all the work. + /// + public int UserId { get; set; } = -1; - /// - /// construct for a given site and userId - /// - public SyncHandlerOptions(string setName, int userId) - :this(setName) - { - this.UserId = userId; - } + /// + /// Default constructor + /// + public SyncHandlerOptions() { } - /// - /// Construct options with set and handler action set. - /// - public SyncHandlerOptions(string setName, HandlerActions action) - : this(setName) - { - this.Set = setName; - this.Action = action; - } + /// + /// Construct Options for a given set + /// + public SyncHandlerOptions(string setName) + : this() + { + this.Set = setName; + } - /// - /// construct for a given set, action and userId - /// - public SyncHandlerOptions(string setName, HandlerActions action, int userId) - : this(setName, action) - { - this.UserId = userId; - } + /// + /// construct for a given site and userId + /// + public SyncHandlerOptions(string setName, int userId) + : this(setName) + { + this.UserId = userId; } + /// + /// Construct options with set and handler action set. + /// + public SyncHandlerOptions(string setName, HandlerActions action) + : this(setName) + { + this.Set = setName; + this.Action = action; + } + /// + /// construct for a given set, action and userId + /// + public SyncHandlerOptions(string setName, HandlerActions action, int userId) + : this(setName, action) + { + this.UserId = userId; + } } diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerBase.cs index ed57382c..40f03a4e 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerBase.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerBase.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Xml.Linq; using Microsoft.Extensions.Logging; @@ -19,245 +18,246 @@ using uSync.Core; using uSync.Core.Models; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// Base class for any Handlers that manage IEntity type objects +/// +public abstract class SyncHandlerBase + : SyncHandlerRoot, ISyncCleanEntryHandler + where TObject : IEntity + where TService : IService { + /// - /// Base class for any Handlers that manage IEntity type objects + /// reference to Umbraco Entity service /// - public abstract class SyncHandlerBase - : SyncHandlerRoot, ISyncCleanEntryHandler - where TObject : IEntity - where TService : IService + protected readonly IEntityService entityService; + + + /// + public SyncHandlerBase( + ILogger> logger, + IEntityService entityService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) { + this.entityService = entityService; + } - /// - /// reference to Umbraco Entity service - /// - protected readonly IEntityService entityService; - - - /// - public SyncHandlerBase( - ILogger> logger, - IEntityService entityService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { - this.entityService = entityService; - } - - /// - protected override bool HasChildren(TObject item) - => entityService.GetChildren(item.Id).Any(); + /// + 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. - /// - protected override IEnumerable CleanFolder(string cleanFile, bool reportOnly, bool flat) - { - var folder = Path.GetDirectoryName(cleanFile); - if (!syncFileService.DirectoryExists(folder)) return Enumerable.Empty(); + /// + /// given a folder we calculate what items we can remove, becuase they are + /// not in one the the files in the folder. + /// + protected override IEnumerable CleanFolder(string cleanFile, bool reportOnly, bool flat) + { + var folder = Path.GetDirectoryName(cleanFile); + if (folder is null || syncFileService.DirectoryExists(folder) is false) return []; - // get the keys for every item in this folder. + // get the keys for every item in this folder. - // this would works on the flat folder stucture too, - // there we are being super defensive, so if an item - // is anywhere in the folder it won't get removed - // even if the folder is wrong - // be a little slower (not much though) + // this would works on the flat folder structure too, + // there we are being super defensive, so if an item + // is anywhere in the folder it won't get removed + // even if the folder is wrong + // be a little slower (not much though) - // we cache this, (it is cleared on an ImportAll) - var keys = GetFolderKeys(folder, flat); - if (keys.Count > 0) - { - // move parent to here, we only need to check it if there are files. - var parentId = GetCleanParentId(cleanFile); - if (parentId == 0) return Enumerable.Empty(); + // we cache this, (it is cleared on an ImportAll) + var keys = GetFolderKeys(folder, flat); + if (keys.Count > 0) + { + // move parent to here, we only need to check it if there are files. + var parentId = GetCleanParentId(cleanFile); + if (parentId == 0) return Enumerable.Empty(); - logger.LogDebug("Got parent with {Id} from clean file {file}", parentId, Path.GetFileName(cleanFile)); + logger.LogDebug("Got parent with {Id} from clean file {file}", parentId, Path.GetFileName(cleanFile)); - // keys should aways have at least one entry (the key from cleanFile) - // if it doesn't then something might have gone wrong. - // because we are being defensive when it comes to deletes, - // we only then do deletes when we know we have loaded some keys! - return DeleteMissingItems(parentId, keys, reportOnly); - } - else - { - logger.LogWarning("Failed to get the keys for items in the folder, there might be a disk issue {folder}", folder); - return Enumerable.Empty(); - } + // keys should aways have at least one entry (the key from cleanFile) + // if it doesn't then something might have gone wrong. + // because we are being defensive when it comes to deletes, + // we only then do deletes when we know we have loaded some keys! + return DeleteMissingItems(parentId, keys, reportOnly); } - - private int GetCleanParentId(string cleanFile) + else { - var node = syncFileService.LoadXElement(cleanFile); - var id = node.Attribute("Id").ValueOrDefault(0); - if (id != 0) return id; - return GetCleanParent(cleanFile)?.Id ?? 0; + logger.LogWarning("Failed to get the keys for items in the folder, there might be a disk issue {folder}", folder); + return Enumerable.Empty(); } + } - /// - /// Process any cleanup actions that may have been loaded up - /// - public virtual IEnumerable ProcessCleanActions(string folder, IEnumerable actions, HandlerSettings config) - { - var cleans = actions.Where(x => x.Change == ChangeType.Clean && !string.IsNullOrWhiteSpace(x.FileName)).ToList(); - if (cleans.Count == 0) return Enumerable.Empty(); + private int GetCleanParentId(string cleanFile) + { + var node = syncFileService.LoadXElement(cleanFile); + var id = node.Attribute("Id").ValueOrDefault(0); + if (id != 0) return id; + return GetCleanParent(cleanFile)?.Id ?? 0; + } - var results = new List(); + /// + /// Process any cleanup actions that may have been loaded up + /// + public virtual IEnumerable ProcessCleanActions(string? folder, IEnumerable actions, HandlerSettings config) + { + if (folder is null) return []; - foreach (var clean in cleans) - { - if (!string.IsNullOrWhiteSpace(clean.FileName)) - results.AddRange(CleanFolder(clean.FileName, false, config.UseFlatStructure)); - } + var cleans = actions.Where(x => x.Change == ChangeType.Clean && !string.IsNullOrWhiteSpace(x.FileName)).ToList(); + if (cleans.Count == 0) return Enumerable.Empty(); + + var results = new List(); - return results; + foreach (var clean in cleans) + { + if (!string.IsNullOrWhiteSpace(clean.FileName)) + results.AddRange(CleanFolder(clean.FileName, false, config.UseFlatStructure)); } - /// - protected override IEnumerable DeleteMissingItems(TObject parent, IEnumerable keys, bool reportOnly) - => DeleteMissingItems(parent?.Id ?? 0, keys, reportOnly); + return results; + } + + /// + protected override IEnumerable DeleteMissingItems(TObject parent, IEnumerable keys, bool reportOnly) + => DeleteMissingItems(parent?.Id ?? 0, keys, reportOnly); + + /// + protected override IEnumerable DeleteMissingItems(int parentId, IEnumerable keys, bool reportOnly) + { + var items = GetChildItems(parentId).ToList(); - /// - protected override IEnumerable DeleteMissingItems(int parentId, IEnumerable keys, bool reportOnly) + logger.LogDebug("DeleteMissingItems: {parentId} Checking {itemCount} items for {keyCount} keys", parentId, items.Count, keys.Count()); + + var actions = new List(); + foreach (var item in items.Where(x => !keys.Contains(x.Key))) { - var items = GetChildItems(parentId).ToList(); + logger.LogDebug("DeleteMissingItems: Found {item} that is not in file list (Reporting: {reportOnly})", item.Id, reportOnly); - logger.LogDebug("DeleteMissingItems: {parentId} Checking {itemCount} items for {keyCount} keys", parentId, items.Count, keys.Count()); + var name = String.Empty; + if (item is IEntitySlim slim) name = slim.Name; - var actions = new List(); - foreach (var item in items.Where(x => !keys.Contains(x.Key))) + if (string.IsNullOrEmpty(name) || !reportOnly) { - logger.LogDebug("DeleteMissingItems: Found {item} that is not in file list (Reporting: {reportOnly})", item.Id, reportOnly); + var actualItem = GetFromService(item.Id); + if (actualItem == null) + { + logger.LogDebug("Actual Item {id} can't be found", item.Id); + continue; + } - var name = String.Empty; - if (item is IEntitySlim slim) name = slim.Name; + name = GetItemName(actualItem); - if (string.IsNullOrEmpty(name) || !reportOnly) + // actually do the delete if we are really not reporting + if (!reportOnly) { - var actualItem = GetFromService(item.Id); - if (actualItem == null) - { - logger.LogDebug("Actual Item {id} can't be found", item.Id); - continue; - } - - name = GetItemName(actualItem); - - // actually do the delete if we are really not reporting - if (!reportOnly) - { - logger.LogInformation("Deleting item: {id} {name} as part of a 'clean' import", actualItem.Id, name); - DeleteViaService(actualItem); - } + logger.LogInformation("Deleting item: {id} {name} as part of a 'clean' import", actualItem.Id, name); + DeleteViaService(actualItem); } - - // for reporting - we use the entity name, - // this stops an extra lookup - which we may not need later - actions.Add( - uSyncActionHelper.SetAction(SyncAttempt.Succeed(name, ChangeType.Delete), string.Empty, item.Key, this.Alias)); } - return actions; + // for reporting - we use the entity name, + // this stops an extra lookup - which we may not need later + actions.Add( + uSyncActionHelper.SetAction(SyncAttempt.Succeed(name, ChangeType.Delete), string.Empty, item.Key, this.Alias)); } - /// - /// Export all items under a suppled parent id - /// - virtual public IEnumerable ExportAll(int parentId, string folder, HandlerSettings config, SyncUpdateCallback callback) - { - var parent = GetFromService(parentId); - return ExportAll(parent, folder, config, callback); - } + return actions; + } - /// - protected override IEnumerable GetChildItems(IEntity parent) - { - if (parent == null) return GetChildItems(-1); - return GetChildItems(parent.Id); - } + /// + /// Export all items under a suppled parent id + /// + virtual public IEnumerable ExportAll(int parentId, string folder, HandlerSettings config, SyncUpdateCallback? callback) + { + var parent = GetFromService(parentId); + return ExportAll(parent, folder, config, callback); + } - /// - /// Get all child items beneath a given item - /// - /// - /// Almost everything does this - but languages can't so we need to - /// let the language Handler override this. - /// - virtual protected IEnumerable GetChildItems(int parent, UmbracoObjectTypes objectType) - { - var cacheKey = $"{GetCacheKeyBase()}_parent_{parent}_{objectType}"; + /// + protected override IEnumerable GetChildItems(IEntity? parent) + { + if (parent == null) return GetChildItems(-1); + return GetChildItems(parent.Id); + } - return runtimeCache.GetCacheItem(cacheKey, () => + /// + /// Get all child items beneath a given item + /// + /// + /// Almost everything does this - but languages can't so we need to + /// let the language Handler override this. + /// + virtual protected IEnumerable GetChildItems(int parent, UmbracoObjectTypes objectType) + { + var cacheKey = $"{GetCacheKeyBase()}_parent_{parent}_{objectType}"; + + return runtimeCache.GetCacheItem(cacheKey, () => + { + // logger.LogDebug("Cache miss [{key}]", cacheKey); + if (parent == -1) { - // logger.LogDebug("Cache miss [{key}]", cacheKey); - if (parent == -1) - { - return entityService.GetChildren(parent, objectType); - } - else - { - // If you ask for the type then you get more info, and there is extra db calls to - // load it, so GetChildren without the object type is quicker. + return entityService.GetChildren(parent, objectType); + } + else + { + // If you ask for the type then you get more info, and there is extra db calls to + // load it, so GetChildren without the object type is quicker. - // but we need to know that we only get our type so we then filter. - var guidType = ObjectTypes.GetGuid(objectType); - return entityService.GetChildren(parent).Where(x => x.NodeObjectType == guidType); - } - }, null); + // but we need to know that we only get our type so we then filter. + var guidType = ObjectTypes.GetGuid(objectType); + return entityService.GetChildren(parent).Where(x => x.NodeObjectType == guidType); + } + }, null) ?? []; - } + } - /// - /// Get all child items beneath a given item - /// - virtual protected IEnumerable GetChildItems(int parent) - { - if (this.itemObjectType == UmbracoObjectTypes.Unknown) - return Enumerable.Empty(); + /// + /// Get all child items beneath a given item + /// + virtual protected IEnumerable GetChildItems(int parent) + { + if (this.itemObjectType == UmbracoObjectTypes.Unknown) + return Enumerable.Empty(); - return GetChildItems(parent, this.itemObjectType); + return GetChildItems(parent, this.itemObjectType); - } + } - /// - /// Get all 'folders' beneath a given item (usally these are Container items) - /// - virtual protected IEnumerable GetFolders(int parent) - { - if (this.itemContainerType != UmbracoObjectTypes.Unknown) - return entityService.GetChildren(parent, this.itemContainerType); + /// + /// Get all 'folders' beneath a given item (usally these are Container items) + /// + virtual protected IEnumerable GetFolders(int parent) + { + if (this.itemContainerType != UmbracoObjectTypes.Unknown) + return entityService.GetChildren(parent, this.itemContainerType); - return Enumerable.Empty(); - } + return []; + } - /// - protected override IEnumerable GetFolders(IEntity parent) - { - if (parent == null) return GetFolders(-1); - return GetFolders(parent.Id); - } + /// + protected override IEnumerable GetFolders(IEntity? parent) + { + if (parent is null) return GetFolders(-1); + return GetFolders(parent.Id); + } - /// - protected override TObject GetFromService(IEntity entity) - => GetFromService(entity.Id); + /// + protected override TObject? GetFromService(IEntity? entity) + => entity is null ? default : GetFromService(entity.Id); - /// - /// for backwards compatability up the tree. - /// - /// - /// - public bool HasChildren(int id) - => GetFolders(id).Any() || GetChildItems(id).Any(); + /// + /// for backwards compatibility up the tree. + /// + /// + /// + public bool HasChildren(int id) + => GetFolders(id).Any() || GetChildItems(id).Any(); - } } diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerCollectionBuilder.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerCollectionBuilder.cs index 6c5d32c7..67de44b7 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerCollectionBuilder.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerCollectionBuilder.cs @@ -3,35 +3,34 @@ using Umbraco.Cms.Core.Composing; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// Collection builder for SyncHandlers +/// +public class SyncHandlerCollectionBuilder + : LazyCollectionBuilderBase +{ + /// + protected override SyncHandlerCollectionBuilder This => this; +} + +/// +/// A collection of SyncHandlers +/// +public class SyncHandlerCollection : BuilderCollectionBase { /// - /// Collection builder for SyncHandlers + /// Construct a collection of handlers from a list of handler items /// - public class SyncHandlerCollectionBuilder - : LazyCollectionBuilderBase - { - /// - protected override SyncHandlerCollectionBuilder This => this; - } + public SyncHandlerCollection(Func> items) + : base(items) + { } /// - /// A collection of SyncHandlers + /// Handlers in the collection /// - public class SyncHandlerCollection : BuilderCollectionBase - { - /// - /// Construct a collection of handlers from a list of handler items - /// - public SyncHandlerCollection(Func> items) - : base(items) - { } - - /// - /// Handlers in the collection - /// - public IEnumerable Handlers => this; + public IEnumerable Handlers => this; - } } diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs index 70bc6e87..c476ef37 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs @@ -16,305 +16,308 @@ using Umbraco.Extensions; using uSync.BackOffice.Configuration; -using uSync.BackOffice.Models; using uSync.BackOffice.Services; using uSync.Core; using uSync.Core.Dependency; using uSync.Core.Serialization; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// Base handler for objects that have container based trees +/// +/// +/// +/// In container based trees all items have unique aliases +/// across the whole tree. +/// +/// +/// you can't for example have two doctypes with the same +/// alias in different containers. +/// +/// +public abstract class SyncHandlerContainerBase + : SyncHandlerTreeBase + where TObject : ITreeEntity + where TService : IService { + + /// + protected SyncHandlerContainerBase( + ILogger> logger, + IEntityService entityService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + { } + /// - /// Base handler for objects that have container based trees + /// Removes any empty 'containers' after import /// - /// - /// - /// In container based trees all items have unique aliases - /// across the whole tree. - /// - /// - /// you can't for example have two doctypes with the same - /// alias in different containers. - /// - /// - public abstract class SyncHandlerContainerBase - : SyncHandlerTreeBase - where TObject : ITreeEntity - where TService : IService - { + [Obsolete("We don't need to pass the folder. will be removed in v15")] + protected IEnumerable CleanFolders(string folder, int parent) + => CleanFolders(parent); - /// - protected SyncHandlerContainerBase( - ILogger> logger, - IEntityService entityService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { } - - /// - /// Removes any empty 'containers' after import - /// - [Obsolete("We don't need to pass the folder. will be removed in v15")] - protected IEnumerable CleanFolders(string folder, int parent) - => CleanFolders(parent); - - /// - /// Removes any empty 'containers' after import - /// - protected IEnumerable CleanFolders(int parent) + /// + /// Removes any empty 'containers' after import + /// + protected IEnumerable CleanFolders(int parent) + { + var actions = new List(); + var folders = GetChildItems(parent, this.itemContainerType); + foreach (var fdlr in folders) { - var actions = new List(); - var folders = GetChildItems(parent, this.itemContainerType); - foreach (var fdlr in folders) - { - logger.LogDebug("Checking Container: {folder} for any childItems [{type}]", fdlr.Id, fdlr?.GetType()?.Name ?? "Unknown"); - actions.AddRange(CleanFolders(fdlr.Id)); + if (fdlr is null) continue; - if (!HasChildren(fdlr)) - { - // get the name (from the slim) - var name = fdlr.Id.ToString(); - if (fdlr is IEntitySlim slim) - { - // if this item isn't an container type, don't delete. - if (ObjectTypes.GetUmbracoObjectType(slim.NodeObjectType) != this.itemContainerType) continue; + logger.LogDebug("Checking Container: {folder} for any childItems [{type}]", fdlr.Id, fdlr.GetType()?.Name ?? "Unknown"); + actions.AddRange(CleanFolders(fdlr.Id)); - name = slim.Name; - logger.LogDebug("Folder has no children {name} {type}", name, slim.NodeObjectType); - } + if (!HasChildren(fdlr)) + { + // get the name (from the slim) + var name = fdlr.Id.ToString() ?? string.Empty; + if (fdlr is IEntitySlim slim) + { + // if this item isn't an container type, don't delete. + if (ObjectTypes.GetUmbracoObjectType(slim.NodeObjectType) != this.itemContainerType) continue; - actions.Add(uSyncAction.SetAction(true, name, typeof(EntityContainer).Name, ChangeType.Delete, "Empty Container")); - DeleteFolder(fdlr.Id); + name = slim.Name ?? name; + logger.LogDebug("Folder has no children {name} {type}", name, slim.NodeObjectType); } - } - return actions; + actions.Add(uSyncAction.SetAction(true, name, typeof(EntityContainer).Name, ChangeType.Delete, "Empty Container")); + DeleteFolder(fdlr.Id); + } } - private bool IsContainer(Guid guid) - => guid == Constants.ObjectTypes.DataTypeContainer - || guid == Constants.ObjectTypes.MediaTypeContainer - || guid == Constants.ObjectTypes.DocumentTypeContainer; - - /// - /// delete a container - /// - abstract protected void DeleteFolder(int id); - - /// - /// Handle events at the end of any import - /// - public virtual IEnumerable ProcessPostImport(string folder, IEnumerable actions, HandlerSettings config) - => ProcessPostImport(actions, config); - - /// - /// Handle events at the end of any import - /// - public virtual IEnumerable ProcessPostImport(IEnumerable actions, HandlerSettings config) - { - if (actions == null || !actions.Any()) - return Enumerable.Empty(); - - return CleanFolders(-1); - } + return actions; + } - /// - /// will resave everything in a folder (and beneath) - /// we need to this when it's renamed - /// - protected IEnumerable UpdateFolder(int folderId, string folder, HandlerSettings config) - => UpdateFolder(folderId, [folder], config); - - /// - /// will resave everything in a folder (and beneath) - /// we need to this when it's renamed - /// - protected IEnumerable UpdateFolder(int folderId, string[] folders, HandlerSettings config) - { - if (this.serializer is SyncContainerSerializerBase containerSerializer) - { - containerSerializer.InitializeCache(); - } + private bool IsContainer(Guid guid) + => guid == Constants.ObjectTypes.DataTypeContainer + || guid == Constants.ObjectTypes.MediaTypeContainer + || guid == Constants.ObjectTypes.DocumentTypeContainer; - var actions = new List(); - var itemFolders = GetChildItems(folderId, this.itemContainerType); - foreach (var item in itemFolders) - { - actions.AddRange(UpdateFolder(item.Id, folders, config)); - } + /// + /// delete a container + /// + abstract protected void DeleteFolder(int id); - var items = GetChildItems(folderId, this.itemObjectType); - foreach (var item in items) - { - var obj = GetFromService(item.Id); - if (obj != null) - { - var attempts = Export(obj, folders, config); - foreach (var attempt in attempts.Where(x => x.Success)) - { - // when its flat structure and use guidNames, we don't need to cleanup. - if (!(this.DefaultConfig.GuidNames && this.DefaultConfig.UseFlatStructure)) - { - CleanUp(obj, attempt.FileName, folders.Last()); - } + /// + /// Handle events at the end of any import + /// + public virtual IEnumerable ProcessPostImport(string folder, IEnumerable actions, HandlerSettings config) + => ProcessPostImport(actions, config); - actions.Add(attempt); - } - } - } + /// + /// Handle events at the end of any import + /// + public virtual IEnumerable ProcessPostImport(IEnumerable actions, HandlerSettings config) + { + if (actions == null || !actions.Any()) + return Enumerable.Empty(); - return actions; + return CleanFolders(-1); + } - } + /// + /// will resave everything in a folder (and beneath) + /// we need to this when it's renamed + /// + protected IEnumerable UpdateFolder(int folderId, string folder, HandlerSettings config) + => UpdateFolder(folderId, [folder], config); - /// - /// Handle container saving events - /// - /// - public virtual void Handle(EntityContainerSavedNotification notification) + /// + /// will resave everything in a folder (and beneath) + /// we need to this when it's renamed + /// + protected IEnumerable UpdateFolder(int folderId, string[] folders, HandlerSettings config) + { + if (this.serializer is SyncContainerSerializerBase containerSerializer) { - // we are not handling saves, we assume a rename, is just that - // if a rename does happen as part of a save then its only - // going to be part of an import, and that will rename the rest of the items - // - // performance wise this is a big improvement, for very little/no impact - - // if (!ShouldProcessEvent()) return; - // logger.LogDebug("Container(s) saved [{count}]", notification.SavedEntities.Count()); - // ProcessContainerChanges(notification.SavedEntities); + containerSerializer.InitializeCache(); } - /// - /// handler renames of containers. - /// - public virtual void Handle(EntityContainerRenamedNotification notification) + var actions = new List(); + var itemFolders = GetChildItems(folderId, this.itemContainerType); + foreach (var item in itemFolders) { - if (!ShouldProcessEvent()) return; - ProcessContainerChanges(notification.Entities); + actions.AddRange(UpdateFolder(item.Id, folders, config)); } - private void ProcessContainerChanges(IEnumerable containers) + var items = GetChildItems(folderId, this.itemObjectType); + foreach (var item in items) { - foreach (var folder in containers) + var obj = GetFromService(item.Id); + if (obj != null) { - logger.LogDebug("Processing container change : {name} [{id}]", folder.Name, folder.Id); - - var targetFolders = rootFolders.Select(x => Path.Combine(x, DefaultFolder)).ToArray(); - - if (folder.ContainedObjectType == this.itemObjectType.GetGuid()) + var attempts = Export(obj, folders, config); + foreach (var attempt in attempts.Where(x => x.Success)) { - UpdateFolder(folder.Id, targetFolders, DefaultConfig); + // when its flat structure and use guidNames, we don't need to cleanup. + if ((this.DefaultConfig.GuidNames && this.DefaultConfig.UseFlatStructure) is false) + { + if (attempt.FileName is not null) + { + CleanUp(obj, attempt.FileName, folders.Last()); + } + } + + actions.Add(attempt); } } } - /// - /// Does this item match the one in a given xml file? - /// - /// - /// container based tree's aren't really trees - as in things can't have the same - /// name inside a folder as something else that might be outside the folder. - /// - /// this means when we are comparing files for clean up, we also want to check the - /// alias. so we check the key (in the base) and if doesn't match we check the alias. - /// - /// under default setup none of this matters because the name of the item is the file - /// name so we find/overwrite it anyway, - /// - /// but for guid / folder structured setups we need to do this compare. - /// - protected override bool DoItemsMatch(XElement node, TObject item) - { - if (base.DoItemsMatch(node, item)) return true; - return node.GetAlias().InvariantEquals(GetItemAlias(item)); - } + return actions; - /// - /// Get merged items from a collection of folders. - /// - protected override IReadOnlyList GetMergedItems(string[] folders) - { - var items = base.GetMergedItems(folders); - - CheckForDuplicates(items); + } - var nodes = items.DistinctBy(x => x.Key).ToDictionary(k => k.Key); - var renames = nodes.Where(x => x.Value.Node.IsEmptyItem()).Select(x => x.Value); - var graph = new List>(); + /// + /// Handle container saving events + /// + /// + public virtual void Handle(EntityContainerSavedNotification notification) + { + // we are not handling saves, we assume a rename, is just that + // if a rename does happen as part of a save then its only + // going to be part of an import, and that will rename the rest of the items + // + // performance wise this is a big improvement, for very little/no impact + + // if (!ShouldProcessEvent()) return; + // logger.LogDebug("Container(s) saved [{count}]", notification.SavedEntities.Count()); + // ProcessContainerChanges(notification.SavedEntities); + } - foreach(var item in items) - { - graph.AddRange(GetCompositions(item.Node).Select(x => GraphEdge.Create(item.Key, x))); - } + /// + /// handler renames of containers. + /// + public virtual void Handle(EntityContainerRenamedNotification notification) + { + if (!ShouldProcessEvent()) return; + ProcessContainerChanges(notification.Entities); + } - var cleanGraph = graph.Where(x => x.Node == x.Edge).ToList(); - var sortedList = nodes.Keys.TopologicalSort(cleanGraph); + private void ProcessContainerChanges(IEnumerable containers) + { + foreach (var folder in containers) + { + logger.LogDebug("Processing container change : {name} [{id}]", folder.Name, folder.Id); - if (sortedList == null) - return items.OrderBy(x => x.Level).ToList(); + var targetFolders = rootFolders.Select(x => Path.Combine(x, DefaultFolder)).ToArray(); - var results = new List(); - foreach (var key in sortedList) + if (folder.ContainedObjectType == this.itemObjectType.GetGuid()) { - if(nodes.TryGetValue(key, out OrderedNodeInfo value)) - results.Add(value); + UpdateFolder(folder.Id, targetFolders, DefaultConfig); } + } + } - if (renames.Any()) - results.AddRange(renames); + /// + /// Does this item match the one in a given xml file? + /// + /// + /// container based tree's aren't really trees - as in things can't have the same + /// name inside a folder as something else that might be outside the folder. + /// + /// this means when we are comparing files for clean up, we also want to check the + /// alias. so we check the key (in the base) and if doesn't match we check the alias. + /// + /// under default setup none of this matters because the name of the item is the file + /// name so we find/overwrite it anyway, + /// + /// but for guid / folder structured setups we need to do this compare. + /// + protected override bool DoItemsMatch(XElement node, TObject item) + { + if (base.DoItemsMatch(node, item)) return true; + return node.GetAlias().InvariantEquals(GetItemAlias(item)); + } - return results; - } + /// + /// Get merged items from a collection of folders. + /// + protected override IReadOnlyList GetMergedItems(string[] folders) + { + var items = base.GetMergedItems(folders); - private void CheckForDuplicates(IReadOnlyList items) - { - var duplicates = items.GroupBy(x => x.Key).Where(x => x.Skip(1).Any()).ToArray(); + CheckForDuplicates(items); - if (duplicates.Length > 0) - { - var dups = string.Join(" \n ", duplicates.SelectMany(x => x.Select(x => $"[{x.Path}]").ToArray())); - logger.LogWarning("Duplicates: one or more items of the same type and key exist on disk [{duplicates}] the item to be imported cannot be guaranteed", dups); + var nodes = items.DistinctBy(x => x.Key).ToDictionary(k => k.Key); + var renames = nodes.Where(x => x.Value.Node.IsEmptyItem()).Select(x => x.Value); + var graph = new List>(); - if (uSyncConfig.Settings.FailOnDuplicates) - { - throw new InvalidOperationException($"Duplicate files detected. Check the disk. {dups}"); - } - } + foreach (var item in items) + { + graph.AddRange(GetCompositions(item.Node).Select(x => GraphEdge.Create(item.Key, x))); } + var cleanGraph = graph.Where(x => x.Node == x.Edge).ToList(); + var sortedList = nodes.Keys.TopologicalSort(cleanGraph); + + if (sortedList == null) + return items.OrderBy(x => x.Level).ToList(); - /// - /// get the Guid values of any compositions so they can be graphed - /// - public IEnumerable GetGraphIds(XElement node) + var results = new List(); + foreach (var key in sortedList) { - return GetCompositions(node); + if (nodes.TryGetValue(key, out OrderedNodeInfo? value) && value is not null) + results.Add(value); } - - private IEnumerable GetCompositions(XElement node) + + if (renames.Any()) + results.AddRange(renames); + + return results; + } + + private void CheckForDuplicates(IReadOnlyList items) + { + var duplicates = items.GroupBy(x => x.Key).Where(x => x.Skip(1).Any()).ToArray(); + + if (duplicates.Length > 0) { - var compositionNode = node.Element("Info")?.Element("Compositions"); - if (compositionNode == null) return Enumerable.Empty(); + var dups = string.Join(" \n ", duplicates.SelectMany(x => x.Select(x => $"[{x.Path}]").ToArray())); + logger.LogWarning("Duplicates: one or more items of the same type and key exist on disk [{duplicates}] the item to be imported cannot be guaranteed", dups); - return GetKeys(compositionNode); + if (uSyncConfig.Settings.FailOnDuplicates) + { + throw new InvalidOperationException($"Duplicate files detected. Check the disk. {dups}"); + } } + } + + + /// + /// get the Guid values of any compositions so they can be graphed + /// + public IEnumerable GetGraphIds(XElement node) + { + return GetCompositions(node); + } - private IEnumerable GetKeys(XElement node) + private IEnumerable GetCompositions(XElement node) + { + var compositionNode = node.Element("Info")?.Element("Compositions"); + if (compositionNode == null) return Enumerable.Empty(); + + return GetKeys(compositionNode); + } + + private IEnumerable GetKeys(XElement node) + { + if (node != null) { - if (node != null) + foreach (var item in node.Elements()) { - foreach (var item in node.Elements()) - { - var key = item.Attribute("Key").ValueOrDefault(Guid.Empty); - if (key == Guid.Empty) continue; + var key = item.Attribute("Key").ValueOrDefault(Guid.Empty); + if (key == Guid.Empty) continue; - yield return key; - } + yield return key; } } } diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerFactory.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerFactory.cs index d722565b..6ee59652 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerFactory.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerFactory.cs @@ -8,233 +8,232 @@ using Umbraco.Extensions; using uSync.BackOffice.Configuration; -using uSync.Core; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// Factory method for accessing the handlers and their configuration +/// +public class SyncHandlerFactory { + private SyncHandlerCollection _syncHandlers; + private uSyncSettings _settings; + private ILogger _logger; + + private IOptionsMonitor _handlerSetSettingsAccessor; + /// - /// Factory method for accessing the handlers and their configuration + /// Create a new SyncHandlerFactory object /// - public class SyncHandlerFactory + public SyncHandlerFactory( + ILogger logger, + SyncHandlerCollection syncHandlers, + IOptionsMonitor handlerSetSettingsAccessor, + IOptionsMonitor options) { - private SyncHandlerCollection _syncHandlers; - private uSyncSettings _settings; - private ILogger _logger; - - private IOptionsMonitor _handlerSetSettingsAccessor; - - /// - /// Craete a new SyncHandlerFactory object - /// - public SyncHandlerFactory( - ILogger logger, - SyncHandlerCollection syncHandlers, - IOptionsMonitor handlerSetSettingsAccessor, - IOptionsMonitor options) - { - _handlerSetSettingsAccessor = handlerSetSettingsAccessor; - _logger = logger; - _syncHandlers = syncHandlers; - _settings = options.CurrentValue; - } + _handlerSetSettingsAccessor = handlerSetSettingsAccessor; + _logger = logger; + _syncHandlers = syncHandlers; + _settings = options.CurrentValue; + } - /// - /// Name of the default handler set - /// - public string DefaultSet => this._settings.DefaultSet; - - #region All getters (regardless of set or config) - - /// - /// Get all handlers - /// - public IEnumerable GetAll() - => _syncHandlers.Handlers; - - /// - /// Get a handler by alias - /// - public ISyncHandler GetHandler(string alias) - => _syncHandlers.Handlers - .FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - - /// - /// Get all handlers that match the list of names in the aliases array - /// - public IEnumerable GetHandlers(params string[] aliases) - => _syncHandlers.Where(x => aliases.InvariantContains(x.Alias)); - - /// - /// returns the handler groups (settings, content, users, etc) that stuff can be grouped into - /// - public IEnumerable GetGroups() - => _syncHandlers.Handlers - .Select(x => x.Group) - .Distinct(); - - #endregion - - #region Default Config Loaders - for when you know exactly what you want - - /// - /// Get Default Handlers based on Alias - /// - /// aliases of handlers you want - /// Handler/Config Pair with default config loaded - public IEnumerable GetDefaultHandlers(IEnumerable aliases) - => GetAll() - .Where(x => aliases.InvariantContains(x.Alias)) - .Select(x => new HandlerConfigPair() - { - Handler = x, - Settings = x.DefaultConfig - }); - - #endregion - - - #region Valid Loaders (need set, group, action) - - /// - /// Get a valid handler (based on config) by alias and options - /// - public HandlerConfigPair GetValidHandler(string alias, SyncHandlerOptions options = null) - => GetValidHandlers(options) - .FirstOrDefault(x => x.Handler.Alias.InvariantEquals(alias)); - - /// - /// Get a valid handler (based on config) by ItemType and options - /// - public HandlerConfigPair GetValidHandlerByTypeName(string itemType, SyncHandlerOptions options = null) - => GetValidHandlers(options) - .Where(x => itemType.InvariantEquals(x.Handler.TypeName)) - .FirstOrDefault(); - - /// - /// Get a valid handler (based on config) by Umbraco Entity Type and options - /// - public HandlerConfigPair GetValidHandlerByEntityType(string entityType, SyncHandlerOptions options = null) - => GetValidHandlers(options) - .Where(x => x.Handler.EntityType.InvariantEquals(entityType)) - .FirstOrDefault(); - - /// - /// Get a valid handler (based on config) by options - /// - public HandlerConfigPair GetValidHander(SyncHandlerOptions options = null) - => GetValidHandlers(options) - .Where(x => x.Handler.ItemType == typeof(TObject).Name) - .FirstOrDefault(); - - - /// - /// Get a all valid handlers (based on config) that can handle a given entityType - /// - public IEnumerable GetValidHandlersByEntityType(IEnumerable entityTypes, SyncHandlerOptions options = null) - => GetValidHandlers(options) - .Where(x => entityTypes.InvariantContains(x.Handler.EntityType)); - - - /// - /// Get the valid (by config) handler groups avalible to this setup - /// - public IEnumerable GetValidGroups(SyncHandlerOptions options = null) - { - var handlers = GetValidHandlers(options); - var groups = handlers - .Select(x => x.GetConfigGroup()) - .ToList(); + /// + /// Name of the default handler set + /// + public string DefaultSet => this._settings.DefaultSet; - groups.AddRange(handlers.Where(x => !string.IsNullOrWhiteSpace(x.Settings.Group)) - .Select(x => x.Settings.Group)); + #region All getters (regardless of set or config) - return groups.Distinct(); - } + /// + /// Get all handlers + /// + public IEnumerable GetAll() + => _syncHandlers.Handlers; - /// - /// get the handler groups and their icons - /// - /// - /// if we don't have a defined icon for a group, the icon from the first handler in the group - /// will be used. - /// - public IDictionary GetValidHandlerGroupsAndIcons(SyncHandlerOptions options = null) - { - var handlers = GetValidHandlers(options); + /// + /// Get a handler by alias + /// + public ISyncHandler? GetHandler(string alias) + => _syncHandlers.Handlers + .FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - return handlers.Select(x => new { group = x.GetConfigGroup(), icon = x.GetGroupIcon() }) - .SafeDistinctBy(x => x.group) - .ToDictionary(k => k.group, v => v.icon); - } + /// + /// Get all handlers that match the list of names in the aliases array + /// + public IEnumerable GetHandlers(params string[] aliases) + => _syncHandlers.Where(x => aliases.InvariantContains(x.Alias)); - /// - /// get a collection of valid handlers that match the list of aliases - /// - public IEnumerable GetValidHandlers(string[] aliases, SyncHandlerOptions options = null) - => GetValidHandlers(options) - .Where(x => aliases.InvariantContains(x.Handler.Alias)); + /// + /// returns the handler groups (settings, content, users, etc) that stuff can be grouped into + /// + public IEnumerable GetGroups() + => _syncHandlers.Handlers + .Select(x => x.Group) + .Distinct(); + #endregion - private uSyncHandlerSetSettings GetSetSettings(string name) - { - return _handlerSetSettingsAccessor.Get(name); - } + #region Default Config Loaders - for when you know exactly what you want + /// + /// Get Default Handlers based on Alias + /// + /// aliases of handlers you want + /// Handler/Config Pair with default config loaded + public IEnumerable GetDefaultHandlers(IEnumerable aliases) + => GetAll() + .Where(x => aliases.InvariantContains(x.Alias)) + .Select(x => new HandlerConfigPair() + { + Handler = x, + Settings = x.DefaultConfig + }); - private HandlerConfigPair LoadHandlerConfig(ISyncHandler handler, uSyncHandlerSetSettings setSettings) - { - return new HandlerConfigPair - { - Handler = handler, - Settings = setSettings.GetHandlerSettings(handler.Alias) - }; - } + #endregion + + + #region Valid Loaders (need set, group, action) + + /// + /// Get a valid handler (based on config) by alias and options + /// + public HandlerConfigPair? GetValidHandler(string alias, SyncHandlerOptions? options = null) + => GetValidHandlers(options) + .FirstOrDefault(x => x.Handler.Alias.InvariantEquals(alias)); + + /// + /// Get a valid handler (based on config) by ItemType and options + /// + public HandlerConfigPair? GetValidHandlerByTypeName(string itemType, SyncHandlerOptions? options = null) + => GetValidHandlers(options) + .Where(x => itemType.InvariantEquals(x.Handler.TypeName)) + .FirstOrDefault(); + + /// + /// Get a valid handler (based on config) by Umbraco Entity Type and options + /// + public HandlerConfigPair? GetValidHandlerByEntityType(string entityType, SyncHandlerOptions? options = null) + => GetValidHandlers(options) + .FirstOrDefault(x => x.Handler.EntityType.InvariantEquals(entityType) is true); - /// - /// Get all valid (by configuration) handlers that fufill the criteria set out in the passed SyncHandlerOptions - /// - public IEnumerable GetValidHandlers(SyncHandlerOptions options = null) + /// + /// Get a valid handler (based on config) by options + /// + public HandlerConfigPair? GetValidHander(SyncHandlerOptions? options = null) + => GetValidHandlers(options) + .Where(x => x.Handler.ItemType == typeof(TObject).Name) + .FirstOrDefault(); + + + /// + /// Get a all valid handlers (based on config) that can handle a given entityType + /// + public IEnumerable GetValidHandlersByEntityType(IEnumerable entityTypes, SyncHandlerOptions? options = null) + => GetValidHandlers(options) + .Where(x => entityTypes.InvariantContains(x.Handler.EntityType)); + + + /// + /// Get the valid (by config) handler groups avalible to this setup + /// + public IEnumerable GetValidGroups(SyncHandlerOptions? options = null) + { + var handlers = GetValidHandlers(options); + var groups = handlers + .Select(x => x.GetConfigGroup()) + .ToList(); + + groups.AddRange(handlers.Where(x => !string.IsNullOrWhiteSpace(x.Settings.Group)) + .Select(x => x.Settings.Group)); + + return groups.Distinct(); + } + + /// + /// get the handler groups and their icons + /// + /// + /// if we don't have a defined icon for a group, the icon from the first handler in the group + /// will be used. + /// + public IDictionary GetValidHandlerGroupsAndIcons(SyncHandlerOptions? options = null) + { + var handlers = GetValidHandlers(options); + + return handlers.Select(x => new { group = x.GetConfigGroup(), icon = x.GetGroupIcon() }) + .DistinctBy(x => x.group) + .ToDictionary(k => k.group, v => v.icon); + } + + /// + /// get a collection of valid handlers that match the list of aliases + /// + public IEnumerable GetValidHandlers(string[] aliases, SyncHandlerOptions? options = null) + => GetValidHandlers(options) + .Where(x => aliases.InvariantContains(x.Handler.Alias)); + + + private uSyncHandlerSetSettings GetSetSettings(string name) + { + return _handlerSetSettingsAccessor.Get(name); + } + + + private HandlerConfigPair LoadHandlerConfig(ISyncHandler handler, uSyncHandlerSetSettings setSettings) + { + return new HandlerConfigPair { - if (options == null) options = new SyncHandlerOptions(); + Handler = handler, + Settings = setSettings.GetHandlerSettings(handler.Alias) + }; + } - var configs = new List(); + /// + /// Get all valid (by configuration) handlers that fufill the criteria set out in the passed SyncHandlerOptions + /// + public IEnumerable GetValidHandlers(SyncHandlerOptions? options = null) + { + if (options == null) options = new SyncHandlerOptions(); - var handlerSetSettings = GetSetSettings(options.Set); - - foreach (var handler in _syncHandlers.Handlers.Where(x => options.IncludeDisabled || x.Enabled)) - { - if (!options.IncludeDisabled && handlerSetSettings.DisabledHandlers.InvariantContains(handler.Alias)) - { - _logger.LogTrace("Handler {handler} is in the disabled handler list", handler.Alias); - continue; - } + var configs = new List(); - var config = LoadHandlerConfig(handler, handlerSetSettings); + var handlerSetSettings = GetSetSettings(options.Set); - // check its valid for the passed group and action. - if (handler != null && IsValidHandler(config, options.Action, options.Group)) - { - configs.Add(config); - } - else - { - _logger.LogDebug("No Handler with {alias} has been loaded", handler.Alias); - // only log if we are doing the default 'everything' group - // because when foing groups we choose not to load things. - if (string.IsNullOrWhiteSpace(options.Group)) - _logger.LogWarning("No Handler with {alias} has been loaded", handler.Alias); - } + foreach (var handler in _syncHandlers.Handlers.Where(x => options.IncludeDisabled || x.Enabled)) + { + if (handler is null) continue; + if (!options.IncludeDisabled && handlerSetSettings.DisabledHandlers.InvariantContains(handler.Alias)) + { + _logger.LogTrace("Handler {handler} is in the disabled handler list", handler.Alias); + continue; + } + + var config = LoadHandlerConfig(handler, handlerSetSettings); + + // check its valid for the passed group and action. + if (IsValidHandler(config, options.Action, options.Group)) + { + configs.Add(config); } - - return configs.OrderBy(x => x.Handler.Priority); + else + { + _logger.LogDebug("No Handler with {alias} has been loaded", handler.Alias); + // only log if we are doing the default 'everything' group + // because when doing groups we choose not to load things. + if (string.IsNullOrWhiteSpace(options.Group)) + _logger.LogWarning("No Handler with {alias} has been loaded", handler.Alias); + } + } - #endregion - /// - /// is this config pair valid for the settings we have for it. - /// - private bool IsValidHandler(HandlerConfigPair handlerConfigPair, HandlerActions actions, string group) - => handlerConfigPair.IsEnabled() && handlerConfigPair.IsValidAction(actions) && handlerConfigPair.IsValidGroup(group); + return configs.OrderBy(x => x.Handler.Priority); } + #endregion + + /// + /// is this config pair valid for the settings we have for it. + /// + private bool IsValidHandler(HandlerConfigPair handlerConfigPair, HandlerActions actions, string group) + => handlerConfigPair.IsEnabled() && handlerConfigPair.IsValidAction(actions) && handlerConfigPair.IsValidGroup(group); } diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs index b4d103e2..3fa53c91 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs @@ -14,172 +14,172 @@ using Umbraco.Extensions; using uSync.BackOffice.Configuration; -using uSync.BackOffice.Models; using uSync.BackOffice.Services; using uSync.Core; using uSync.Core.Models; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// SyncHandler for things that have a level, +/// +/// ideally this would be in SyncHandlerTreeBase, but +/// Templates have levels but are not ITreeEntities +/// +public abstract class SyncHandlerLevelBase + : SyncHandlerBase + where TObject : IEntity + where TService : IService { + /// + protected SyncHandlerLevelBase( + ILogger> logger, + IEntityService entityService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + { } + /// - /// SyncHandler for things that have a level, - /// - /// ideally this would be in SyncHandlerTreeBase, but - /// Templates have levels but are not ITreeEntities + /// Sorts the loaded items. /// - public abstract class SyncHandlerLevelBase - : SyncHandlerBase - where TObject : IEntity - where TService : IService - { - /// - protected SyncHandlerLevelBase( - ILogger> logger, - IEntityService entityService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { } + /// + /// as we are already loading everything to merge, it doesn't + /// then cost us much to sort them when we have to. + /// + protected override IReadOnlyList GetMergedItems(string[] folders) + => base.GetMergedItems(folders).OrderBy(x => x.Level).ToList(); - /// - /// Sorts the loaded items. - /// - /// - /// as we are already loading everything to merge, it doesn't - /// then cost us much to sort them when we have to. - /// - protected override IReadOnlyList GetMergedItems(string[] folders) - => base.GetMergedItems(folders).OrderBy(x => x.Level).ToList(); - - - /// - override protected string GetItemPath(TObject item, bool useGuid, bool isFlat) - { - if (isFlat) return base.GetItemPath(item, useGuid, isFlat); - return GetEntityTreePath((IUmbracoEntity)item, useGuid, true); - } - /// - /// get the tree path for an item (eg. /homepage/about-us/something ) - /// - /// - /// - /// - /// - protected string GetEntityTreePath(IUmbracoEntity item, bool useGuid, bool isTop) + /// + override protected string GetItemPath(TObject item, bool useGuid, bool isFlat) + { + if (isFlat) return base.GetItemPath(item, useGuid, isFlat); + return GetEntityTreePath((IUmbracoEntity)item, useGuid, true); + } + + /// + /// get the tree path for an item (eg. /homepage/about-us/something ) + /// + /// + /// + /// + /// + protected string GetEntityTreePath(IUmbracoEntity item, bool useGuid, bool isTop) + { + var path = string.Empty; + if (item != null) { - var path = string.Empty; - if (item != null) + if (item.ParentId > 0) { - if (item.ParentId > 0) + var parent = this.itemFactory.EntityCache.GetEntity(item.ParentId); + // var parent = entityService.Get(item.ParentId); + if (parent != null) { - var parent = this.itemFactory.EntityCache.GetEntity(item.ParentId); - // var parent = entityService.Get(item.ParentId); - if (parent != null) - { - path = GetEntityTreePath(parent, useGuid, false); - } + path = GetEntityTreePath(parent, useGuid, false); } - - // we only want the guid file name at the top of the tree - path = Path.Combine(path, GetEntityTreeName(item, useGuid && isTop)); } - return path; + // we only want the guid file name at the top of the tree + path = Path.Combine(path, GetEntityTreeName(item, useGuid && isTop)); } - /// - /// the name of an item in an entity tree - /// - virtual protected string GetEntityTreeName(IUmbracoEntity item, bool useGuid) - { - if (item != null) - { - if (useGuid) return item.Key.ToString(); - return item.Name.ToSafeFileName(shortStringHelper); - } + return path; + } - return Guid.NewGuid().ToString(); + /// + /// the name of an item in an entity tree + /// + virtual protected string GetEntityTreeName(IUmbracoEntity item, bool useGuid) + { + if (item is not null) + { + if (useGuid) return item.Key.ToString(); + return item.Name?.ToSafeFileName(shortStringHelper) ?? Guid.NewGuid().ToString(); } + return Guid.NewGuid().ToString(); + } + + /// + /// object representing a file and its level + /// + [Obsolete("Items Will not be loaded using this object, to be removed v15")] + protected class LeveledFile + { /// - /// object representing a file and its level + /// umbraco alias of the item /// - [Obsolete("Items Will not be loaded using this object, to be removed v15")] - protected class LeveledFile - { - /// - /// umbraco alias of the item - /// - public string Alias { get; set; } - - /// - /// level (e.g 0 is root) of file - /// - public int Level { get; set; } - - /// - /// path to the actual file. - /// - public string File { get; set; } - } + public string? Alias { get; set; } /// - /// Load a XElement node for a given path. + /// level (e.g 0 is root) of file /// - [Obsolete("Items Will not be loaded using this object, to be removed v15")] - protected XElement LoadNode(string path) - { - syncFileService.EnsureFileExists(path); - - using (var stream = syncFileService.OpenRead(path)) - { - return XElement.Load(stream); - } - } + public int Level { get; set; } /// - /// Get all the files in a folder and return them sorted by their level + /// path to the actual file. /// - [Obsolete("Items Will not be loaded using this object, to be removed v15")] - protected virtual IList GetLevelOrderedFiles(string folder, IList actions) + public string? File { get; set; } + } + + /// + /// Load a XElement node for a given path. + /// + [Obsolete("Items Will not be loaded using this object, to be removed v15")] + protected XElement LoadNode(string path) + { + syncFileService.EnsureFileExists(path); + + using (var stream = syncFileService.OpenRead(path)) { - List nodes = new List(); + if (stream is null) + throw new FileNotFoundException($"Cannot read stream for {path}"); - var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}"); - foreach (var file in files) + return XElement.Load(stream); + } + } + + /// + /// Get all the files in a folder and return them sorted by their level + /// + [Obsolete("Items Will not be loaded using this object, to be removed v15")] + protected virtual IList GetLevelOrderedFiles(string folder, IList actions) + { + List nodes = new List(); + + var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}"); + foreach (var file in files) + { + try { - try + var node = LoadNode(file); + if (node != null) { - var node = LoadNode(file); - if (node != null) + nodes.Add(new LeveledFile { - nodes.Add(new LeveledFile - { - Alias = node.GetAlias(), - Level = (node.GetLevel() * 1000) + node.GetItemSortOrder(), // will hopefully let us put things in sort order in one go. - File = file - }); - } - } - catch (XmlException ex) - { - // one of the files is wrong. (do we stop or carry on) - logger.LogWarning($"Error loading file: {file} [{ex.Message}]"); - actions.Add(uSyncActionHelper.SetAction( - SyncAttempt.Fail(Path.GetFileName(file), ChangeType.Fail, $"Failed to Load: {ex.Message}"), file, Guid.Empty, this.Alias, false)); + Alias = node.GetAlias(), + Level = (node.GetLevel() * 1000) + node.GetItemSortOrder(), // will hopefully let us put things in sort order in one go. + File = file + }); } } - - return nodes.OrderBy(x => x.Level).ToList(); + catch (XmlException ex) + { + // one of the files is wrong. (do we stop or carry on) + logger.LogWarning($"Error loading file: {file} [{ex.Message}]"); + actions.Add(uSyncActionHelper.SetAction( + SyncAttempt.Fail(Path.GetFileName(file), ChangeType.Fail, $"Failed to Load: {ex.Message}"), file, Guid.Empty, this.Alias, false)); + } } - + return nodes.OrderBy(x => x.Level).ToList(); } + } diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs index c2bcc651..6e137f16 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs @@ -26,661 +26,644 @@ using uSync.Core.Serialization; using uSync.Core.Tracking; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// Root base class for all handlers +/// +/// +/// If the Handler manages something that Implements IEntity use SyncBaseHandler +/// +public abstract class SyncHandlerRoot { /// - /// Root base class for all handlers + /// Reference to the Logger + /// + protected readonly ILogger> logger; + + /// + /// Reference to the uSyncFileService + /// + protected readonly SyncFileService syncFileService; + + /// + /// Reference to the Event service used to handle locking + /// + protected readonly uSyncEventService _mutexService; + + /// + /// List of dependency checkers for this Handler + /// + protected readonly IList> dependencyCheckers; + + /// + /// List of change trackers for this handler + /// + protected readonly IList> trackers; + + /// + /// The Serializer to use for importing/exporting items + /// + protected ISyncSerializer serializer; + + /// + /// Runtime cache for caching lookups + /// + protected readonly IAppPolicyCache runtimeCache; + + /// + /// Alias of the handler, used when getting settings from the configuration file + /// + public string Alias { get; private set; } + + /// + /// Name of handler, displayed to user during reports/imports + /// + public string Name { get; private set; } + + /// + /// name of the folder inside the uSync folder where items are stored + /// + public string DefaultFolder { get; private set; } + + /// + /// priority order items are imported in /// /// - /// If the Handler manages something that Implements IEntity use SyncBaseHandler + /// to import before anything else go below USYNC_RESERVED_LOWER (1000) + /// to import after uSync has done all the other things go past USYNC_RESERVED_UPPER (2000) /// - public abstract class SyncHandlerRoot - { - /// - /// Reference to the Logger - /// - protected readonly ILogger> logger; - - /// - /// Reference to the uSyncFileService - /// - protected readonly SyncFileService syncFileService; - - /// - /// Reference to the Event service used to handle locking - /// - protected readonly uSyncEventService _mutexService; - - /// - /// List of dependency checkers for this Handler - /// - protected readonly IList> dependencyCheckers; - - /// - /// List of change trackers for this handler - /// - protected readonly IList> trackers; - - /// - /// The Serializer to use for importing/exporting items - /// - protected ISyncSerializer serializer; - - /// - /// Runtime cache for caching lookups - /// - protected readonly IAppPolicyCache runtimeCache; - - /// - /// Alias of the handler, used when getting settings from the configuration file - /// - public string Alias { get; private set; } - - /// - /// Name of handler, displayed to user during reports/imports - /// - public string Name { get; private set; } - - /// - /// name of the folder inside the uSync folder where items are stored - /// - public string DefaultFolder { get; private set; } - - /// - /// priority order items are imported in - /// - /// - /// to import before anything else go below USYNC_RESERVED_LOWER (1000) - /// to import after uSync has done all the other things go past USYNC_RESERVED_UPPER (2000) - /// - public int Priority { get; private set; } - - /// - /// Icon displayed on the screen while the import happens. - /// - public string Icon { get; private set; } - - /// - /// does this handler require two passes at the import (e.g data-types import once, and then again after doc-types) - /// - protected bool IsTwoPass = false; - - /// - /// the object type of the item being processed. - /// - public string ItemType { get; protected set; } = typeof(TObject).Name; - - /// - /// Is the handler enabled - /// - public bool Enabled { get; set; } = true; - - - /// - /// the default configuration for this handler - /// - public HandlerSettings DefaultConfig { get; set; } - - /// - /// the root folder for the handler (based on the settings) - /// - [Obsolete("we should be using the array of folders, will be removed in v15")] - protected string rootFolder { get; set; } - - /// - /// the root folders to use for the handler (based on settings). - /// - protected string[] rootFolders { get; set; } - - /// - /// the UDIEntityType for the handler objects - /// - public string EntityType { get; protected set; } - - /// - /// Name of the type (object) - /// - public string TypeName { get; protected set; } // we calculate these now based on the entityType ? - - /// - /// UmbracoObjectType of items handled by this handler - /// - protected UmbracoObjectTypes itemObjectType { get; set; } = UmbracoObjectTypes.Unknown; - - /// - /// UmbracoObjectType of containers manged by this handler - /// - protected UmbracoObjectTypes itemContainerType = UmbracoObjectTypes.Unknown; - - /// - /// The type of the handler - /// - protected string handlerType; - - /// - /// SyncItem factory reference - /// - protected readonly ISyncItemFactory itemFactory; - - /// - /// Reference to the uSyncConfigService - /// - protected readonly uSyncConfigService uSyncConfig; - - /// - /// Umbraco's shortStringHelper - /// - protected readonly IShortStringHelper shortStringHelper; - - /// - /// Constructor, base for all handlers - /// - public SyncHandlerRoot( - ILogger> logger, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory itemFactory) - { - this.uSyncConfig = uSyncConfig; - - this.logger = logger; - this.shortStringHelper = shortStringHelper; - this.itemFactory = itemFactory; - - this.serializer = this.itemFactory.GetSerializers().FirstOrDefault(); - this.trackers = this.itemFactory.GetTrackers().ToList(); - this.dependencyCheckers = this.itemFactory.GetCheckers().ToList(); - - this.syncFileService = syncFileService; - this._mutexService = mutexService; - - var currentHandlerType = GetType(); - var meta = currentHandlerType.GetCustomAttribute(false); - if (meta == null) - throw new InvalidOperationException($"The Handler {handlerType} requires a {typeof(SyncHandlerAttribute)}"); - - handlerType = currentHandlerType.ToString(); - Name = meta.Name; - Alias = meta.Alias; - DefaultFolder = meta.Folder; - Priority = meta.Priority; - IsTwoPass = meta.IsTwoPass; - Icon = string.IsNullOrWhiteSpace(meta.Icon) ? "icon-umb-content" : meta.Icon; - EntityType = meta.EntityType; - - TypeName = serializer.ItemType; - - this.itemObjectType = uSyncObjectType.ToUmbracoObjectType(EntityType); - this.itemContainerType = uSyncObjectType.ToContainerUmbracoObjectType(EntityType); - - GetDefaultConfig(); - - if (uSyncConfig.Settings.CacheFolderKeys) - { - this.runtimeCache = appCaches.RuntimeCache; - } - else - { - logger.LogInformation("No caching of handler key lookups (CacheFolderKeys = false)"); - this.runtimeCache = NoAppCache.Instance; - } - } + public int Priority { get; private set; } - private void GetDefaultConfig() - { - var defaultSet = uSyncConfig.GetDefaultSetSettings(); - this.DefaultConfig = defaultSet.GetHandlerSettings(this.Alias); + /// + /// Icon displayed on the screen while the import happens. + /// + public string Icon { get; private set; } - if (defaultSet.DisabledHandlers.InvariantContains(this.Alias)) - this.DefaultConfig.Enabled = false; + /// + /// does this handler require two passes at the import (e.g data-types import once, and then again after doc-types) + /// + protected bool IsTwoPass = false; - rootFolder = uSyncConfig.GetRootFolder(); + /// + /// the object type of the item being processed. + /// + public string ItemType { get; protected set; } = typeof(TObject).Name; - rootFolders = uSyncConfig.GetFolders(); - } + /// + /// Is the handler enabled + /// + public bool Enabled { get; set; } = true; - #region Importing - /// - /// Import everything from a given folder, using the supplied configuration settings. - /// - public IEnumerable ImportAll(string folder, HandlerSettings config, bool force, SyncUpdateCallback callback = null) - => ImportAll([folder], config, new uSyncImportOptions - { - Flags = force ? SerializerFlags.Force : SerializerFlags.None, - Callbacks = new uSyncCallbacks(null, callback) - }); + /// + /// the default configuration for this handler + /// + public HandlerSettings DefaultConfig { get; set; } - /// - /// import everything from a collection of folders, using the supplied config. - /// - /// - /// allows us to 'merge' a collection of folders down and perform an import against them (without first having to actually merge the folders on disk) - /// - public IEnumerable ImportAll(string[] folders, HandlerSettings config, uSyncImportOptions options) - { - var cacheKey = PrepCaches(); - runtimeCache.ClearByKey(cacheKey); + /// + /// the root folder for the handler (based on the settings) + /// + [Obsolete("we should be using the array of folders, will be removed in v15")] + protected string rootFolder { get; set; } - options.Callbacks?.Update?.Invoke("Calculating import order", 1, 9); + /// + /// the root folders to use for the handler (based on settings). + /// + protected string[] rootFolders { get; set; } - var items = GetMergedItems(folders); + /// + /// the UDIEntityType for the handler objects + /// + public string EntityType { get; protected set; } - options.Callbacks?.Update?.Invoke($"Processing {items.Count} items", 2, 9); + /// + /// Name of the type (object) + /// + public string TypeName { get; protected set; } // we calculate these now based on the entityType ? - // create the update list with items.count space. this is the max size we need this list. - List actions = new List(items.Count); - List> updates = new List>(items.Count); - List cleanMarkers = []; + /// + /// UmbracoObjectType of items handled by this handler + /// + protected UmbracoObjectTypes itemObjectType { get; set; } = UmbracoObjectTypes.Unknown; - int count = 0; - int total = items.Count; + /// + /// UmbracoObjectType of containers manged by this handler + /// + protected UmbracoObjectTypes itemContainerType = UmbracoObjectTypes.Unknown; - foreach (var item in items) - { - count++; + /// + /// The type of the handler + /// + protected string handlerType; - options.Callbacks?.Update?.Invoke($"Importing {Path.GetFileNameWithoutExtension(item.Path)}", count, total); + /// + /// SyncItem factory reference + /// + protected readonly ISyncItemFactory itemFactory; - var result = ImportElement(item.Node, item.Path, config, options); - foreach (var attempt in result) - { - if (attempt.Success) - { - if (attempt.Change == ChangeType.Clean) - { - cleanMarkers.Add(item.Path); - } - else if (attempt.Item is not null && attempt.Item is TObject update) - { - updates.Add(new ImportedItem(item.Node,update)); - } - } + /// + /// Reference to the uSyncConfigService + /// + protected readonly uSyncConfigService uSyncConfig; - if (attempt.Change != ChangeType.Clean) - actions.Add(attempt); - } - } + /// + /// Umbraco's shortStringHelper + /// + protected readonly IShortStringHelper shortStringHelper; - // clean up memory we didn't use in the update list. - updates.TrimExcess(); + /// + /// Constructor, base for all handlers + /// + public SyncHandlerRoot( + ILogger> logger, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory itemFactory) + { + this.uSyncConfig = uSyncConfig; - // bulk save? - if (updates.Count > 0) - { - if (options.Flags.HasFlag(SerializerFlags.DoNotSave)) - { - serializer.Save(updates.Select(x => x.Item)); - } + this.logger = logger; + this.shortStringHelper = shortStringHelper; + this.itemFactory = itemFactory; - PerformSecondPassImports(updates, actions, config, options.Callbacks?.Update); - } + var _serializer = this.itemFactory.GetSerializers().FirstOrDefault(); + if (_serializer is null) + throw new KeyNotFoundException($"No Serializer found for handler {this.Alias}"); - if (actions.All(x => x.Success) && cleanMarkers.Count > 0) - { - PerformImportClean(cleanMarkers, actions, config, options.Callbacks?.Update); - } + this.serializer = _serializer; + this.trackers = this.itemFactory.GetTrackers().ToList(); + this.dependencyCheckers = this.itemFactory.GetCheckers().ToList(); - CleanCaches(cacheKey); - options.Callbacks?.Update?.Invoke("Done", 3, 3); - return actions; - } + this.syncFileService = syncFileService; + this._mutexService = mutexService; + + var currentHandlerType = GetType(); + var meta = currentHandlerType.GetCustomAttribute(false); + if (meta == null) + throw new InvalidOperationException($"The Handler {handlerType} requires a {typeof(SyncHandlerAttribute)}"); + + handlerType = currentHandlerType.ToString(); + Name = meta.Name; + Alias = meta.Alias; + DefaultFolder = meta.Folder; + Priority = meta.Priority; + IsTwoPass = meta.IsTwoPass; + Icon = string.IsNullOrWhiteSpace(meta.Icon) ? "icon-umb-content" : meta.Icon; + EntityType = meta.EntityType; + + TypeName = serializer.ItemType; - /// - /// get all items for the report/import process. - /// - /// - /// - public IReadOnlyList FetchAllNodes(string[] folders) - => GetMergedItems(folders); + this.itemObjectType = uSyncObjectType.ToUmbracoObjectType(EntityType); + this.itemContainerType = uSyncObjectType.ToContainerUmbracoObjectType(EntityType); - /// - /// method to get the merged folders, handlers that care about orders should override this. - /// - protected virtual IReadOnlyList GetMergedItems(string[] folders) + this.DefaultConfig = GetDefaultConfig(); + rootFolder = uSyncConfig.GetRootFolder(); + rootFolders = uSyncConfig.GetFolders(); + + if (uSyncConfig.Settings.CacheFolderKeys) { - var baseTracker = trackers.FirstOrDefault() as ISyncTrackerBase; - return syncFileService.MergeFolders(folders, uSyncConfig.Settings.DefaultExtension, baseTracker).ToArray(); + this.runtimeCache = appCaches.RuntimeCache; } - - private void PerformImportClean(List cleanMarkers, List actions, HandlerSettings config, SyncUpdateCallback callback) + else { - foreach (var item in cleanMarkers.Select((filePath, Index) => new { filePath, Index })) - { - var folderName = Path.GetFileName(item.filePath); - callback?.Invoke($"Cleaning {folderName}", item.Index, cleanMarkers.Count); - - var cleanActions = CleanFolder(item.filePath, false, config.UseFlatStructure); - if (cleanActions.Any()) - { - actions.AddRange(cleanActions); - } - else - { - // nothing to delete, we report this as a no change - actions.Add(uSyncAction.SetAction( - success: true, - name: $"Folder {Path.GetFileName(item.filePath)}", - change: ChangeType.NoChange, - filename: syncFileService.GetSiteRelativePath(item.filePath))); - } - } - // remove the actual cleans (they will have been replaced by the deletes - actions.RemoveAll(x => x.Change == ChangeType.Clean); + logger.LogInformation("No caching of handler key lookups (CacheFolderKeys = false)"); + this.runtimeCache = NoAppCache.Instance; } + } + + private HandlerSettings GetDefaultConfig() + { + var defaultSet = uSyncConfig.GetDefaultSetSettings(); + var config = defaultSet.GetHandlerSettings(this.Alias); + + if (defaultSet.DisabledHandlers.InvariantContains(this.Alias)) + config.Enabled = false; - /// - /// Import everything in a given (child) folder, based on setting - /// - [Obsolete("Import folder method not called directly from v13.1 will be removed in v15")] - protected virtual IEnumerable ImportFolder(string folder, HandlerSettings config, Dictionary updates, bool force, SyncUpdateCallback callback) + return config; + } + + #region Importing + + /// + /// Import everything from a given folder, using the supplied configuration settings. + /// + public IEnumerable ImportAll(string folder, HandlerSettings config, bool force, SyncUpdateCallback? callback = null) + => ImportAll([folder], config, new uSyncImportOptions { - List actions = new List(); - var files = GetImportFiles(folder); + Flags = force ? SerializerFlags.Force : SerializerFlags.None, + Callbacks = new uSyncCallbacks(null, callback) + }); - var flags = SerializerFlags.None; - if (force) flags |= SerializerFlags.Force; + /// + /// import everything from a collection of folders, using the supplied config. + /// + /// + /// allows us to 'merge' a collection of folders down and perform an import against them (without first having to actually merge the folders on disk) + /// + public IEnumerable ImportAll(string[] folders, HandlerSettings config, uSyncImportOptions options) + { + var cacheKey = PrepCaches(); + runtimeCache.ClearByKey(cacheKey); - var cleanMarkers = new List(); + options.Callbacks?.Update?.Invoke("Calculating import order", 1, 9); - int count = 0; - int total = files.Count(); - foreach (string file in files) - { - count++; + var items = GetMergedItems(folders); - callback?.Invoke($"Importing {Path.GetFileNameWithoutExtension(file)}", count, total); + options.Callbacks?.Update?.Invoke($"Processing {items.Count} items", 2, 9); - var result = Import(file, config, flags); - foreach (var attempt in result) - { - if (attempt.Success) - { - if (attempt.Change == ChangeType.Clean) - { - cleanMarkers.Add(file); - } - else if (attempt.Item != null && attempt.Item is TObject item) - { - updates.Add(file, item); - } - } + // create the update list with items.count space. this is the max size we need this list. + List actions = new List(items.Count); + List> updates = new List>(items.Count); + List cleanMarkers = []; - if (attempt.Change != ChangeType.Clean) - actions.Add(attempt); - } - } + int count = 0; + int total = items.Count; - // bulk save .. - if (flags.HasFlag(SerializerFlags.DoNotSave) && updates.Any()) - { - // callback?.Invoke($"Saving {updates.Count()} changes", 1, 1); - serializer.Save(updates.Select(x => x.Value)); - } + foreach (var item in items) + { + count++; - var folders = syncFileService.GetDirectories(folder); - foreach (var children in folders) - { - actions.AddRange(ImportFolder(children, config, updates, force, callback)); - } + options.Callbacks?.Update?.Invoke($"Importing {Path.GetFileNameWithoutExtension(item.Path)}", count, total); - if (actions.All(x => x.Success) && cleanMarkers.Count > 0) + var result = ImportElement(item.Node, item.Path, config, options); + foreach (var attempt in result) { - foreach (var item in cleanMarkers.Select((filePath, Index) => new { filePath, Index })) + if (attempt.Success) { - var folderName = Path.GetFileName(item.filePath); - callback?.Invoke($"Cleaning {folderName}", item.Index, cleanMarkers.Count); - - var cleanActions = CleanFolder(item.filePath, false, config.UseFlatStructure); - if (cleanActions.Any()) + if (attempt.Change == ChangeType.Clean) { - actions.AddRange(cleanActions); + cleanMarkers.Add(item.Path); } - else + else if (attempt.Item is not null && attempt.Item is TObject update) { - // nothing to delete, we report this as a no change - actions.Add(uSyncAction.SetAction( - success: true, - name: $"Folder {Path.GetFileName(item.filePath)}", - change: ChangeType.NoChange, filename: syncFileService.GetSiteRelativePath(item.filePath) - ) - ); + updates.Add(new ImportedItem(item.Node, update)); } } - // remove the actual cleans (they will have been replaced by the deletes - actions.RemoveAll(x => x.Change == ChangeType.Clean); - } - return actions; + if (attempt.Change != ChangeType.Clean) + actions.Add(attempt); + } } - /// - /// Import a single item, from the .config file supplied - /// - public virtual IEnumerable Import(string filePath, HandlerSettings config, SerializerFlags flags) + // clean up memory we didn't use in the update list. + updates.TrimExcess(); + + // bulk save? + if (updates.Count > 0) { - try - { - syncFileService.EnsureFileExists(filePath); - var node = syncFileService.LoadXElement(filePath); - return Import(node, filePath, config, flags); - } - catch (FileNotFoundException notFoundException) - { - return uSyncAction.Fail(Path.GetFileName(filePath), this.handlerType, this.ItemType, ChangeType.Fail, $"File not found {notFoundException.Message}", notFoundException) - .AsEnumerableOfOne(); - } - catch (Exception ex) + if (options.Flags.HasFlag(SerializerFlags.DoNotSave)) { - logger.LogWarning("{alias}: Import Failed : {exception}", this.Alias, ex.ToString()); - return uSyncAction.Fail(Path.GetFileName(filePath), this.handlerType, this.ItemType, ChangeType.Fail, $"Import Fail: {ex.Message}", new Exception(ex.Message, ex)) - .AsEnumerableOfOne(); + serializer.Save(updates.Select(x => x.Item)); } + + PerformSecondPassImports(updates, actions, config, options.Callbacks?.Update); } - /// - /// Import a single item based on already loaded XML - /// - public virtual IEnumerable Import(XElement node, string filename, HandlerSettings config, SerializerFlags flags) + if (actions.All(x => x.Success) && cleanMarkers.Count > 0) { - if (config.FailOnMissingParent) flags |= SerializerFlags.FailMissingParent; - return ImportElement(node, filename, config, new uSyncImportOptions { Flags = flags }); + PerformImportClean(cleanMarkers, actions, config, options.Callbacks?.Update); } - /// - /// Import a single item from a usync XML file - /// - virtual public IEnumerable Import(string file, HandlerSettings config, bool force) - { - var flags = SerializerFlags.OnePass; - if (force) flags |= SerializerFlags.Force; + CleanCaches(cacheKey); + options.Callbacks?.Update?.Invoke("Done", 3, 3); - return Import(file, config, flags); - } + return actions; + } + + /// + /// get all items for the report/import process. + /// + /// + /// + public IReadOnlyList FetchAllNodes(string[] folders) + => GetMergedItems(folders); + + /// + /// method to get the merged folders, handlers that care about orders should override this. + /// + protected virtual IReadOnlyList GetMergedItems(string[] folders) + { + var baseTracker = trackers.FirstOrDefault() as ISyncTrackerBase; + return syncFileService.MergeFolders(folders, uSyncConfig.Settings.DefaultExtension, baseTracker).ToArray(); + } - /// - /// Import a node, with settings and options - /// - /// - /// All Imports lead here - /// - virtual public IEnumerable ImportElement(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) + private void PerformImportClean(List cleanMarkers, List actions, HandlerSettings config, SyncUpdateCallback? callback) + { + foreach (var item in cleanMarkers.Select((filePath, Index) => new { filePath, Index })) { - if (!ShouldImport(node, settings)) - { - return uSyncAction.SetAction(true, node.GetAlias(), message: "Change blocked (based on configuration)") - .AsEnumerableOfOne(); - } + var folderName = Path.GetFileName(item.filePath); + callback?.Invoke($"Cleaning {folderName}", item.Index, cleanMarkers.Count); - if (_mutexService.FireItemStartingEvent(new uSyncImportingItemNotification(node, (ISyncHandler)this))) + var cleanActions = CleanFolder(item.filePath, false, config.UseFlatStructure); + if (cleanActions.Any()) { - // blocked - return uSyncActionHelper - .ReportAction(ChangeType.NoChange, node.GetAlias(), node.GetPath(), GetNameFromFileOrNode(filename, node), node.GetKey(), this.Alias, "Change stopped by delegate event") - .AsEnumerableOfOne(); + actions.AddRange(cleanActions); } - - try + else { - // 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); + // nothing to delete, we report this as a no change + actions.Add(uSyncAction.SetAction( + success: true, + name: $"Folder {Path.GetFileName(item.filePath)}", + change: ChangeType.NoChange, + filename: syncFileService.GetSiteRelativePath(item.filePath))); + } + } + // remove the actual cleans (they will have been replaced by the deletes + actions.RemoveAll(x => x.Change == ChangeType.Clean); + } - // get the item. - var attempt = DeserializeItem(node, serializerOptions); - var action = uSyncActionHelper.SetAction(attempt, GetNameFromFileOrNode(filename, node), node.GetKey(), this.Alias, IsTwoPass); + /// + /// Import everything in a given (child) folder, based on setting + /// + [Obsolete("Import folder method not called directly from v13.1 will be removed in v15")] + protected virtual IEnumerable ImportFolder(string folder, HandlerSettings config, Dictionary updates, bool force, SyncUpdateCallback? callback) + { + List actions = new List(); + var files = GetImportFiles(folder); - // add item if we have it. - if (attempt.Item != null) action.Item = attempt.Item; + var flags = SerializerFlags.None; + if (force) flags |= SerializerFlags.Force; - // add details if we have them - if (attempt.Details != null && attempt.Details.Any()) action.Details = attempt.Details; + var cleanMarkers = new List(); - // this might not be the place to do this because, two pass items are imported at another point too. - _mutexService.FireItemCompletedEvent(new uSyncImportedItemNotification(node, attempt.Change)); + int count = 0; + int total = files.Count(); + foreach (string file in files) + { + count++; + callback?.Invoke($"Importing {Path.GetFileNameWithoutExtension(file)}", count, total); - return action.AsEnumerableOfOne(); - } - catch (Exception ex) + var result = Import(file, config, flags); + foreach (var attempt in result) { - logger.LogWarning("{alias}: Import Failed : {exception}", this.Alias, ex.ToString()); - return uSyncAction.Fail(Path.GetFileName(filename), this.handlerType, this.ItemType, ChangeType.Fail, - $"{this.Alias} Import Fail: {ex.Message}", new Exception(ex.Message)) - .AsEnumerableOfOne(); + if (attempt.Success) + { + if (attempt.Change == ChangeType.Clean) + { + cleanMarkers.Add(file); + } + else if (attempt.Item != null && attempt.Item is TObject item) + { + updates.Add(file, item); + } + } + + if (attempt.Change != ChangeType.Clean) + actions.Add(attempt); } + } + // bulk save .. + if (flags.HasFlag(SerializerFlags.DoNotSave) && updates.Any()) + { + // callback?.Invoke($"Saving {updates.Count()} changes", 1, 1); + serializer.Save(updates.Select(x => x.Value)); } + var folders = syncFileService.GetDirectories(folder); + foreach (var children in folders) + { + actions.AddRange(ImportFolder(children, config, updates, force, callback)); + } - /// - /// Works through a list of items that have been processed and performs the second import pass on them. - /// - private void PerformSecondPassImports(List> importedItems, List actions, HandlerSettings config, SyncUpdateCallback callback = null) + if (actions.All(x => x.Success) && cleanMarkers.Count > 0) { - foreach (var item in importedItems.Select((update, Index) => new { update, Index })) + foreach (var item in cleanMarkers.Select((filePath, Index) => new { filePath, Index })) { - var itemKey = item.update.Node.GetKey(); + var folderName = Path.GetFileName(item.filePath); + callback?.Invoke($"Cleaning {folderName}", item.Index, cleanMarkers.Count); - callback?.Invoke($"Second Pass {item.update.Node.GetKey()}", item.Index, importedItems.Count); - var attempt = ImportSecondPass(item.update.Node, item.update.Item, config, callback); - if (attempt.Success) + var cleanActions = CleanFolder(item.filePath, false, config.UseFlatStructure); + if (cleanActions.Any()) { - // if the second attempt has a message on it, add it to the first attempt. - if (!string.IsNullOrWhiteSpace(attempt.Message) || attempt.Details?.Any() == true) - { - uSyncAction action = actions.FirstOrDefault(x => $"{x.key}_{x.HandlerAlias}" == $"{itemKey}_{this.Alias}", new uSyncAction { key = Guid.Empty }); - if (action.key != Guid.Empty) - { - actions.Remove(action); - action.Message += attempt.Message ?? ""; - - if (attempt.Details?.Any() == true) - { - var details = action.Details.ToList(); - details.AddRange(attempt.Details); - action.Details = details; - } - actions.Add(action); - } - } - if (attempt.Change > ChangeType.NoChange && !attempt.Saved && attempt.Item != null) - { - serializer.Save(attempt.Item.AsEnumerableOfOne()); - } + actions.AddRange(cleanActions); } else { - uSyncAction action = actions.FirstOrDefault(x => $"{x.key}_{x.HandlerAlias}" == $"{itemKey}_{this.Alias}", new uSyncAction { key = Guid.Empty }); - if (action.key != Guid.Empty) - { - actions.Remove(action); - action.Success = attempt.Success; - action.Message = $"Second Pass Fail: {attempt.Message}"; - action.Exception = attempt.Exception; - actions.Add(action); - } + // nothing to delete, we report this as a no change + actions.Add(uSyncAction.SetAction( + success: true, + name: $"Folder {Path.GetFileName(item.filePath)}", + change: ChangeType.NoChange, filename: syncFileService.GetSiteRelativePath(item.filePath) + ) + ); } } + // remove the actual cleans (they will have been replaced by the deletes + actions.RemoveAll(x => x.Change == ChangeType.Clean); } - /// - /// Perform a second pass import on an item - /// - virtual public IEnumerable ImportSecondPass(uSyncAction action, HandlerSettings settings, uSyncImportOptions options) + return actions; + } + + /// + /// Import a single item, from the .config file supplied + /// + public virtual IEnumerable Import(string filePath, HandlerSettings config, SerializerFlags flags) + { + try { - if (!IsTwoPass) return Enumerable.Empty(); + syncFileService.EnsureFileExists(filePath); + var node = syncFileService.LoadXElement(filePath); + return Import(node, filePath, config, flags); + } + catch (FileNotFoundException notFoundException) + { + return uSyncAction.Fail(Path.GetFileName(filePath), this.handlerType, this.ItemType, ChangeType.Fail, $"File not found {notFoundException.Message}", notFoundException) + .AsEnumerableOfOne(); + } + catch (Exception ex) + { + logger.LogWarning("{alias}: Import Failed : {exception}", this.Alias, ex.ToString()); + return uSyncAction.Fail(Path.GetFileName(filePath), this.handlerType, this.ItemType, ChangeType.Fail, $"Import Fail: {ex.Message}", new Exception(ex.Message, ex)) + .AsEnumerableOfOne(); + } + } - try - { - var fileName = action.FileName; + /// + /// Import a single item based on already loaded XML + /// + public virtual IEnumerable Import(XElement node, string filename, HandlerSettings config, SerializerFlags flags) + { + if (config.FailOnMissingParent) flags |= SerializerFlags.FailMissingParent; + return ImportElement(node, filename, config, new uSyncImportOptions { Flags = flags }); + } - if (!syncFileService.FileExists(fileName)) - return Enumerable.Empty(); + /// + /// Import a single item from a usync XML file + /// + virtual public IEnumerable Import(string file, HandlerSettings config, bool force) + { + var flags = SerializerFlags.OnePass; + if (force) flags |= SerializerFlags.Force; - var node = syncFileService.LoadXElement(fileName); - var item = GetFromService(node.GetKey()); - if (item == null) return Enumerable.Empty(); + return Import(file, config, flags); + } - // merge the options from the handler and any import options into our serializer options. - var serializerOptions = new SyncSerializerOptions(options?.Flags ?? SerializerFlags.None, settings.Settings, options?.UserId ?? -1); - serializerOptions.MergeSettings(options?.Settings); + /// + /// Import a node, with settings and options + /// + /// + /// All Imports lead here + /// + virtual public IEnumerable ImportElement(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) + { + if (!ShouldImport(node, settings)) + { + return uSyncAction.SetAction(true, node.GetAlias(), message: "Change blocked (based on configuration)") + .AsEnumerableOfOne(); + } - // do the second pass on this item - var result = DeserializeItemSecondPass(item, node, serializerOptions); + if (_mutexService.FireItemStartingEvent(new uSyncImportingItemNotification(node, (ISyncHandler)this))) + { + // blocked + return uSyncActionHelper + .ReportAction(ChangeType.NoChange, node.GetAlias(), node.GetPath(), GetNameFromFileOrNode(filename, node), node.GetKey(), this.Alias, "Change stopped by delegate event") + .AsEnumerableOfOne(); + } - return uSyncActionHelper.SetAction(result, syncFileService.GetSiteRelativePath(fileName), node.GetKey(), this.Alias).AsEnumerableOfOne(); - } - catch (Exception ex) - { - logger.LogWarning($"Second Import Failed: {ex}"); - return uSyncAction.Fail(action.Name, this.handlerType, action.ItemType, ChangeType.ImportFail, "Second import failed", ex).AsEnumerableOfOne(); - } + try + { + // 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); + + // get the item. + var attempt = DeserializeItem(node, serializerOptions); + var action = uSyncActionHelper.SetAction(attempt, GetNameFromFileOrNode(filename, node), node.GetKey(), this.Alias, IsTwoPass); + + // add item if we have it. + if (attempt.Item != null) action.Item = attempt.Item; + + // add details if we have them + if (attempt.Details != null && attempt.Details.Any()) action.Details = attempt.Details; + + // this might not be the place to do this because, two pass items are imported at another point too. + _mutexService.FireItemCompletedEvent(new uSyncImportedItemNotification(node, attempt.Change)); + + + return action.AsEnumerableOfOne(); + } + catch (Exception ex) + { + logger.LogWarning("{alias}: Import Failed : {exception}", this.Alias, ex.ToString()); + return uSyncAction.Fail(Path.GetFileName(filename), this.handlerType, this.ItemType, ChangeType.Fail, + $"{this.Alias} Import Fail: {ex.Message}", new Exception(ex.Message)) + .AsEnumerableOfOne(); } + } - /// - /// Perform a 'second pass' import on a single item. - /// - [Obsolete("Call method with node element to reduce disk IO, will be removed in v15")] - virtual public SyncAttempt ImportSecondPass(string file, TObject item, HandlerSettings config, SyncUpdateCallback callback) + + /// + /// Works through a list of items that have been processed and performs the second import pass on them. + /// + private void PerformSecondPassImports(List> importedItems, List actions, HandlerSettings config, SyncUpdateCallback? callback = null) + { + foreach (var item in importedItems.Select((update, Index) => new { update, Index })) { - if (IsTwoPass) + var itemKey = item.update.Node.GetKey(); + + callback?.Invoke($"Second Pass {item.update.Node.GetKey()}", item.Index, importedItems.Count); + var attempt = ImportSecondPass(item.update.Node, item.update.Item, config, callback); + if (attempt.Success) { - try + // if the second attempt has a message on it, add it to the first attempt. + if (!string.IsNullOrWhiteSpace(attempt.Message) || attempt.Details?.Any() == true) { - syncFileService.EnsureFileExists(file); - - var flags = SerializerFlags.None; + uSyncAction action = actions.FirstOrDefault(x => $"{x.key}_{x.HandlerAlias}" == $"{itemKey}_{this.Alias}", new uSyncAction { key = Guid.Empty }); + if (action.key != Guid.Empty) + { + actions.Remove(action); + action.Message += attempt.Message ?? ""; - var node = syncFileService.LoadXElement(file); - return DeserializeItemSecondPass(item, node, new SyncSerializerOptions(flags, config.Settings)); + if (attempt.Details?.Any() == true) + { + var details = action.Details?.ToList() ?? []; + details.AddRange(attempt.Details); + action.Details = details; + } + actions.Add(action); + } } - catch (Exception ex) + if (attempt.Change > ChangeType.NoChange && !attempt.Saved && attempt.Item != null) { - logger.LogWarning($"Second Import Failed: {ex.ToString()}"); - return SyncAttempt.Fail(GetItemAlias(item), item, ChangeType.Fail, ex.Message, ex); + serializer.Save(attempt.Item.AsEnumerableOfOne()); + } + } + else + { + uSyncAction action = actions.FirstOrDefault(x => $"{x.key}_{x.HandlerAlias}" == $"{itemKey}_{this.Alias}", new uSyncAction { key = Guid.Empty }); + if (action.key != Guid.Empty) + { + actions.Remove(action); + action.Success = attempt.Success; + action.Message = $"Second Pass Fail: {attempt.Message}"; + action.Exception = attempt.Exception; + actions.Add(action); } } - - return SyncAttempt.Succeed(GetItemAlias(item), ChangeType.NoChange); } + } + + /// + /// Perform a second pass import on an item + /// + virtual public IEnumerable ImportSecondPass(uSyncAction action, HandlerSettings settings, uSyncImportOptions options) + { + if (!IsTwoPass) return Enumerable.Empty(); + + try + { + var fileName = action.FileName; + + if (fileName is null || syncFileService.FileExists(fileName) is false) + return Enumerable.Empty(); + + var node = syncFileService.LoadXElement(fileName); + var item = GetFromService(node.GetKey()); + if (item == null) return Enumerable.Empty(); + + // merge the options from the handler and any import options into our serializer options. + var serializerOptions = new SyncSerializerOptions(options?.Flags ?? SerializerFlags.None, settings.Settings, options?.UserId ?? -1); + serializerOptions.MergeSettings(options?.Settings); + + // do the second pass on this item + var result = DeserializeItemSecondPass(item, node, serializerOptions); - /// - /// Perform a 'second pass' import on a single item. - /// - virtual public SyncAttempt ImportSecondPass(XElement node, TObject item, HandlerSettings config, SyncUpdateCallback callback) + return uSyncActionHelper.SetAction(result, syncFileService.GetSiteRelativePath(fileName), node.GetKey(), this.Alias).AsEnumerableOfOne(); + } + catch (Exception ex) { - if (IsTwoPass is false) - return SyncAttempt.Succeed(GetItemAlias(item), ChangeType.NoChange); + logger.LogWarning($"Second Import Failed: {ex}"); + return uSyncAction.Fail(action.Name, this.handlerType, action.ItemType, ChangeType.ImportFail, "Second import failed", ex).AsEnumerableOfOne(); + } + } + + /// + /// Perform a 'second pass' import on a single item. + /// + [Obsolete("Call method with node element to reduce disk IO, will be removed in v15")] + virtual public SyncAttempt ImportSecondPass(string file, TObject item, HandlerSettings config, SyncUpdateCallback callback) + { + if (IsTwoPass) + { try { - return DeserializeItemSecondPass(item, node, new SyncSerializerOptions(SerializerFlags.None, config.Settings)); + syncFileService.EnsureFileExists(file); + + var flags = SerializerFlags.None; + + var node = syncFileService.LoadXElement(file); + return DeserializeItemSecondPass(item, node, new SyncSerializerOptions(flags, config.Settings)); } catch (Exception ex) { @@ -689,689 +672,723 @@ virtual public SyncAttempt ImportSecondPass(XElement node, TObject item } } - /// - /// given a folder we calculate what items we can remove, because they are - /// not in one the files in the folder. - /// - protected virtual IEnumerable CleanFolder(string cleanFile, bool reportOnly, bool flat) + return SyncAttempt.Succeed(GetItemAlias(item), ChangeType.NoChange); + } + + /// + /// Perform a 'second pass' import on a single item. + /// + virtual public SyncAttempt ImportSecondPass(XElement node, TObject item, HandlerSettings config, SyncUpdateCallback? callback) + { + if (IsTwoPass is false) + return SyncAttempt.Succeed(GetItemAlias(item), ChangeType.NoChange); + + try + { + return DeserializeItemSecondPass(item, node, new SyncSerializerOptions(SerializerFlags.None, config.Settings)); + } + catch (Exception ex) { - var folder = Path.GetDirectoryName(cleanFile); - if (!syncFileService.DirectoryExists(folder)) return Enumerable.Empty(); + logger.LogWarning($"Second Import Failed: {ex.ToString()}"); + return SyncAttempt.Fail(GetItemAlias(item), item, ChangeType.Fail, ex.Message, ex); + } + } + /// + /// given a folder we calculate what items we can remove, because they are + /// not in one the files in the folder. + /// + protected virtual IEnumerable CleanFolder(string cleanFile, bool reportOnly, bool flat) + { + var folder = Path.GetDirectoryName(cleanFile); + if (string.IsNullOrWhiteSpace(folder) is true || syncFileService.DirectoryExists(folder) is false) + return Enumerable.Empty(); - // get the keys for every item in this folder. + // get the keys for every item in this folder. - // this would works on the flat folder structure too, - // there we are being super defensive, so if an item - // is anywhere in the folder it won't get removed - // even if the folder is wrong - // be a little slower (not much though) + // this would works on the flat folder structure too, + // there we are being super defensive, so if an item + // is anywhere in the folder it won't get removed + // even if the folder is wrong + // be a little slower (not much though) - // we cache this, (it is cleared on an ImportAll) - var keys = GetFolderKeys(folder, flat); - if (keys.Count > 0) - { - // move parent to here, we only need to check it if there are files. - var parent = GetCleanParent(cleanFile); - if (parent == null) return Enumerable.Empty(); + // we cache this, (it is cleared on an ImportAll) + var keys = GetFolderKeys(folder, flat); + if (keys.Count > 0) + { + // move parent to here, we only need to check it if there are files. + var parent = GetCleanParent(cleanFile); + if (parent == null) return Enumerable.Empty(); - logger.LogDebug("Got parent with {alias} from clean file {file}", GetItemAlias(parent), Path.GetFileName(cleanFile)); + logger.LogDebug("Got parent with {alias} from clean file {file}", GetItemAlias(parent), Path.GetFileName(cleanFile)); - // keys should aways have at least one entry (the key from cleanFile) - // if it doesn't then something might have gone wrong. - // because we are being defensive when it comes to deletes, - // we only then do deletes when we know we have loaded some keys! - return DeleteMissingItems(parent, keys, reportOnly); - } - else - { - logger.LogWarning("Failed to get the keys for items in the folder, there might be a disk issue {folder}", folder); - return Enumerable.Empty(); - } + // keys should aways have at least one entry (the key from cleanFile) + // if it doesn't then something might have gone wrong. + // because we are being defensive when it comes to deletes, + // we only then do deletes when we know we have loaded some keys! + return DeleteMissingItems(parent, keys, reportOnly); } - - /// - /// 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) + else { - var cacheKey = $"{GetCacheKeyBase()}_{folder.GetHashCode()}"; - runtimeCache.ClearByKey(cacheKey); - runtimeCache.GetCacheItem(cacheKey, () => folderKeys); + logger.LogWarning("Failed to get the keys for items in the folder, there might be a disk issue {folder}", folder); + return Enumerable.Empty(); } + } - /// - /// Get the GUIDs for all items in a folder - /// - /// - /// This is disk intensive, (checking the .config files all the time) - /// so we cache it, and if we are using the flat folder structure, then - /// we only do it once, so its quicker. - /// - protected IList GetFolderKeys(string folder, bool flat) - { - // We only need to load all the keys once per handler (if all items are in a folder that key will be used). - var folderKey = folder.GetHashCode(); + /// + /// 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); + } - var cacheKey = $"{GetCacheKeyBase()}_{folderKey}"; + /// + /// Get the GUIDs for all items in a folder + /// + /// + /// This is disk intensive, (checking the .config files all the time) + /// so we cache it, and if we are using the flat folder structure, then + /// we only do it once, so its quicker. + /// + protected IList GetFolderKeys(string folder, bool flat) + { + // We only need to load all the keys once per handler (if all items are in a folder that key will be used). + var folderKey = folder.GetHashCode(); + var cacheKey = $"{GetCacheKeyBase()}_{folderKey}"; - 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(); + return runtimeCache.GetCacheItem(cacheKey, () => + { + logger.LogDebug("Getting Folder Keys : {cacheKey}", cacheKey); - foreach (var file in files) - { - var node = XElement.Load(file); - var key = node.GetKey(); - if (key != Guid.Empty && !keys.Contains(key)) - { - keys.Add(key); - } - } + // 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(); - logger.LogDebug("Loaded {count} keys from {folder} [{cacheKey}]", keys.Count, folder, cacheKey); - - return keys; - - }, null); - } - - /// - /// Get the parent item of the clean file (so we can check if the folder has any versions of this item in it) - /// - protected TObject GetCleanParent(string file) - { - var node = XElement.Load(file); - var key = node.GetKey(); - if (key == Guid.Empty) return default; - return GetFromService(key); - } - - /// - /// remove an items that are not listed in the GUIDs to keep - /// - /// parent item that all keys will be under - /// list of GUIDs of items we don't want to delete - /// will just report what would happen (doesn't do the delete) - /// list of delete actions - protected abstract IEnumerable DeleteMissingItems(TObject parent, IEnumerable keysToKeep, bool reportOnly); - - /// - /// Remove an items that are not listed in the GUIDs to keep. - /// - /// parent item that all keys will be under - /// list of GUIDs of items we don't want to delete - /// will just report what would happen (doesn't do the delete) - /// list of delete actions - protected virtual IEnumerable DeleteMissingItems(int parentId, IEnumerable keysToKeep, bool reportOnly) - => Enumerable.Empty(); - - /// - /// Get the files we are going to import from a folder. - /// - protected virtual IEnumerable GetImportFiles(string folder) - => syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}").OrderBy(x => x); - - /// - /// check to see if this element should be imported as part of the process. - /// - virtual protected bool ShouldImport(XElement node, HandlerSettings config) - { - // if createOnly is on, then we only create things that are not already there. - // this lookup is slow (relatively) so we only do it if we have to. - if (config.GetSetting(Core.uSyncConstants.DefaultSettings.CreateOnly, Core.uSyncConstants.DefaultSettings.CreateOnly_Default) - || config.GetSetting(Core.uSyncConstants.DefaultSettings.OneWay, Core.uSyncConstants.DefaultSettings.CreateOnly_Default)) + foreach (var file in files) { - var item = serializer.FindItem(node); - if (item != null) + var node = XElement.Load(file); + var key = node.GetKey(); + if (key != Guid.Empty && !keys.Contains(key)) { - logger.LogDebug("CreateOnly: Item {alias} already exist not importing it.", node.GetAlias()); - return false; + keys.Add(key); } } + logger.LogDebug("Loaded {count} keys from {folder} [{cacheKey}]", keys.Count, folder, cacheKey); + + return keys; + + }, null) ?? []; + } + + /// + /// Get the parent item of the clean file (so we can check if the folder has any versions of this item in it) + /// + protected TObject? GetCleanParent(string file) + { + var node = XElement.Load(file); + var key = node.GetKey(); + if (key == Guid.Empty) return default; + return GetFromService(key); + } + + /// + /// remove an items that are not listed in the GUIDs to keep + /// + /// parent item that all keys will be under + /// list of GUIDs of items we don't want to delete + /// will just report what would happen (doesn't do the delete) + /// list of delete actions + protected abstract IEnumerable DeleteMissingItems(TObject parent, IEnumerable keysToKeep, bool reportOnly); + + /// + /// Remove an items that are not listed in the GUIDs to keep. + /// + /// parent item that all keys will be under + /// list of GUIDs of items we don't want to delete + /// will just report what would happen (doesn't do the delete) + /// list of delete actions + protected virtual IEnumerable DeleteMissingItems(int parentId, IEnumerable keysToKeep, bool reportOnly) + => Enumerable.Empty(); + + /// + /// Get the files we are going to import from a folder. + /// + protected virtual IEnumerable GetImportFiles(string folder) + => syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}").OrderBy(x => x); - // Ignore alias setting. - // if its set the thing with this alias is ignored. - var ignore = config.GetSetting("IgnoreAliases", string.Empty); - if (!string.IsNullOrWhiteSpace(ignore)) + /// + /// check to see if this element should be imported as part of the process. + /// + virtual protected bool ShouldImport(XElement node, HandlerSettings config) + { + // if createOnly is on, then we only create things that are not already there. + // this lookup is slow (relatively) so we only do it if we have to. + if (config.GetSetting(Core.uSyncConstants.DefaultSettings.CreateOnly, Core.uSyncConstants.DefaultSettings.CreateOnly_Default) + || config.GetSetting(Core.uSyncConstants.DefaultSettings.OneWay, Core.uSyncConstants.DefaultSettings.CreateOnly_Default)) + { + var item = serializer.FindItem(node); + if (item != null) { - var ignoreList = ignore.ToDelimitedList(); - if (ignoreList.InvariantContains(node.GetAlias())) - { - logger.LogDebug("Ignore: Item {alias} is in the ignore list", node.GetAlias()); - return false; - } + logger.LogDebug("CreateOnly: Item {alias} already exist not importing it.", node.GetAlias()); + return false; } + } - return true; + // Ignore alias setting. + // if its set the thing with this alias is ignored. + var ignore = config.GetSetting("IgnoreAliases", string.Empty); + if (!string.IsNullOrWhiteSpace(ignore)) + { + var ignoreList = ignore.ToDelimitedList(); + if (ignoreList.InvariantContains(node.GetAlias())) + { + logger.LogDebug("Ignore: Item {alias} is in the ignore list", node.GetAlias()); + return false; + } } - /// - /// Check to see if this element should be exported. - /// - virtual protected bool ShouldExport(XElement node, HandlerSettings config) => true; + return true; + } - #endregion - #region Exporting + /// + /// Check to see if this element should be exported. + /// + virtual protected bool ShouldExport(XElement node, HandlerSettings config) => true; - /// - /// Export all items to a give folder on the disk - /// - virtual public IEnumerable ExportAll(string folder, HandlerSettings config, SyncUpdateCallback callback) - => ExportAll([folder], config, callback); + #endregion - /// - /// Export all items to a give folder on the disk - /// - virtual public IEnumerable ExportAll(string[] folders, HandlerSettings config, SyncUpdateCallback callback) - { - // we don't clean the folder out on an export all. - // because the actions (renames/deletes) live in the folder - // - // there will have to be a different clean option - // syncFileService.CleanFolder(folder); + #region Exporting - return ExportAll(default, folders, config, callback); - } + /// + /// Export all items to a give folder on the disk + /// + virtual public IEnumerable ExportAll(string folder, HandlerSettings config, SyncUpdateCallback? callback) + => ExportAll([folder], config, callback); + + /// + /// Export all items to a give folder on the disk + /// + virtual public IEnumerable ExportAll(string[] folders, HandlerSettings config, SyncUpdateCallback? callback) + { + // we don't clean the folder out on an export all. + // because the actions (renames/deletes) live in the folder + // + // there will have to be a different clean option + // syncFileService.CleanFolder(folder); - /// - /// export all items underneath a given container - /// - virtual public IEnumerable ExportAll(TContainer parent, string folder, HandlerSettings config, SyncUpdateCallback callback) - => ExportAll(parent, [folder], config, callback); + return ExportAll(default, folders, config, callback); + } - /// - /// Export all items to a give folder on the disk - /// - virtual public IEnumerable ExportAll(TContainer parent, string[] folders, HandlerSettings config, SyncUpdateCallback callback) - { - var actions = new List(); + /// + /// export all items underneath a given container + /// + virtual public IEnumerable ExportAll(TContainer? parent, string folder, HandlerSettings config, SyncUpdateCallback? callback) + => ExportAll(parent, [folder], config, callback); + + /// + /// Export all items to a give folder on the disk + /// + virtual public IEnumerable ExportAll(TContainer? parent, string[] folders, HandlerSettings config, SyncUpdateCallback? callback) + { + var actions = new List(); - if (itemContainerType != UmbracoObjectTypes.Unknown) + if (itemContainerType != UmbracoObjectTypes.Unknown) + { + var containers = GetFolders(parent); + foreach (var container in containers) { - var containers = GetFolders(parent); - foreach (var container in containers) - { - actions.AddRange(ExportAll(container, folders, config, callback)); - } + actions.AddRange(ExportAll(container, folders, config, callback)); } + } - var items = GetChildItems(parent).ToList(); - foreach (var item in items.Select((Value, Index) => new { Value, Index })) + var items = GetChildItems(parent).ToList(); + foreach (var item in items.Select((Value, Index) => new { Value, Index })) + { + TObject? concreteType; + if (item.Value is TObject t) { - TObject concreteType; - if (item.Value is TObject t) - { - concreteType = t; - } - else - { - concreteType = GetFromService(item.Value); - } - if (concreteType != null) - { // only export the items (not the containers). - callback?.Invoke(GetItemName(concreteType), item.Index, items.Count); - actions.AddRange(Export(concreteType, folders, config)); - } - actions.AddRange(ExportAll(item.Value, folders, config, callback)); + concreteType = t; } - - return actions; + else + { + concreteType = GetFromService(item.Value); + } + if (concreteType is not null) + { // only export the items (not the containers). + callback?.Invoke(GetItemName(concreteType), item.Index, items.Count); + actions.AddRange(Export(concreteType, folders, config)); + } + actions.AddRange(ExportAll(item.Value, folders, config, callback)); } - /// - /// Fetch all child items beneath a given container - /// - abstract protected IEnumerable GetChildItems(TContainer parent); + return actions; + } + + /// + /// Fetch all child items beneath a given container + /// + abstract protected IEnumerable GetChildItems(TContainer? parent); - /// - /// Fetch all child items beneath a given folder - /// - /// - /// - abstract protected IEnumerable GetFolders(TContainer parent); + /// + /// Fetch all child items beneath a given folder + /// + /// + /// + abstract protected IEnumerable GetFolders(TContainer? parent); - /// - /// Does this container have any children - /// - public bool HasChildren(TContainer item) - => GetFolders(item).Any() || GetChildItems(item).Any(); + /// + /// Does this container have any children + /// + public bool HasChildren(TContainer item) + => GetFolders(item).Any() || GetChildItems(item).Any(); - /// - /// Export a single item based on it's ID - /// - public IEnumerable Export(int id, string folder, HandlerSettings settings) - => Export(id, [folder], settings); + /// + /// Export a single item based on it's ID + /// + public IEnumerable Export(int id, string folder, HandlerSettings settings) + => Export(id, [folder], settings); - /// - /// Export an item based on its id, observing root behavior. - /// - public IEnumerable Export(int id, string[] folders, HandlerSettings settings) + /// + /// Export an item based on its id, observing root behavior. + /// + public IEnumerable Export(int id, string[] folders, HandlerSettings settings) + { + var item = this.GetFromService(id); + if (item is null) { - var item = this.GetFromService(id); - return this.Export(item, folders, settings); + return uSyncAction.Fail( + id.ToString(), this.handlerType, this.ItemType, + ChangeType.Export, "Unable to find item", + new KeyNotFoundException($"Item of {id} cannot be found")) + .AsEnumerableOfOne(); } + return this.Export(item, folders, settings); + } - /// - /// Export an single item from a given UDI value - /// - public IEnumerable Export(Udi udi, string folder, HandlerSettings settings) - => Export(udi, [folder], settings); + /// + /// Export an single item from a given UDI value + /// + public IEnumerable Export(Udi udi, string folder, HandlerSettings settings) + => Export(udi, [folder], settings); - /// - /// Export an single item from a given UDI value - /// - public IEnumerable Export(Udi udi, string[] folders, HandlerSettings settings) - { - var item = FindByUdi(udi); - if (item != null) - return Export(item, folders, settings); + /// + /// Export an single item from a given UDI value + /// + public IEnumerable Export(Udi udi, string[] folders, HandlerSettings settings) + { + var item = FindByUdi(udi); + if (item != null) + return Export(item, folders, settings); - return uSyncAction.Fail(nameof(udi), this.handlerType, this.ItemType, ChangeType.Fail, $"Item not found {udi}", - new KeyNotFoundException(nameof(udi))) - .AsEnumerableOfOne(); - } + return uSyncAction.Fail(nameof(udi), this.handlerType, this.ItemType, ChangeType.Fail, $"Item not found {udi}", + new KeyNotFoundException(nameof(udi))) + .AsEnumerableOfOne(); + } - /// - /// Export a given item to disk - /// - virtual public IEnumerable Export(TObject item, string folder, HandlerSettings config) - => Export(item, [folder], config); + /// + /// Export a given item to disk + /// + virtual public IEnumerable Export(TObject item, string folder, HandlerSettings config) + => Export(item, [folder], config); - /// - /// Export a given item to disk - /// - virtual public IEnumerable Export(TObject item, string[] folders, HandlerSettings config) - { - if (item == null) - return uSyncAction.Fail(nameof(item), this.handlerType, this.ItemType, ChangeType.Fail, "Item not set", - new ArgumentNullException(nameof(item))).AsEnumerableOfOne(); + /// + /// Export a given item to disk + /// + virtual public IEnumerable Export(TObject item, string[] folders, HandlerSettings config) + { + if (item == null) + return uSyncAction.Fail(nameof(item), this.handlerType, this.ItemType, ChangeType.Fail, "Item not set", + new ArgumentNullException(nameof(item))).AsEnumerableOfOne(); - if (_mutexService.FireItemStartingEvent(new uSyncExportingItemNotification(item, (ISyncHandler)this))) - { - return uSyncActionHelper - .ReportAction(ChangeType.NoChange, GetItemName(item), string.Empty, string.Empty, GetItemKey(item), this.Alias, - "Change stopped by delegate event") - .AsEnumerableOfOne(); - } + if (_mutexService.FireItemStartingEvent(new uSyncExportingItemNotification(item, (ISyncHandler)this))) + { + return uSyncActionHelper + .ReportAction(ChangeType.NoChange, GetItemName(item), string.Empty, string.Empty, GetItemKey(item), this.Alias, + "Change stopped by delegate event") + .AsEnumerableOfOne(); + } - var targetFolder = folders.Last(); + var targetFolder = folders.Last(); - var filename = GetPath(targetFolder, item, config.GuidNames, config.UseFlatStructure) - .ToAppSafeFileName(); + var filename = GetPath(targetFolder, item, config.GuidNames, config.UseFlatStructure) + .ToAppSafeFileName(); - // - if (IsLockedAtRoot(folders, filename.Substring(targetFolder.Length+1))) - { - // if we have lock roots on, then this item will not export - // because exporting would mean the root was no longer used. - return uSyncAction.SetAction(true, syncFileService.GetSiteRelativePath(filename), - type: typeof(TObject).ToString(), - change: ChangeType.NoChange, - message: "Not exported (would overwrite root value)", - filename: filename).AsEnumerableOfOne(); - } + // + if (IsLockedAtRoot(folders, filename.Substring(targetFolder.Length + 1))) + { + // if we have lock roots on, then this item will not export + // because exporting would mean the root was no longer used. + return uSyncAction.SetAction(true, syncFileService.GetSiteRelativePath(filename), + type: typeof(TObject).ToString(), + change: ChangeType.NoChange, + message: "Not exported (would overwrite root value)", + filename: filename).AsEnumerableOfOne(); + } - var attempt = Export_DoExport(item, filename, folders, config); + var attempt = Export_DoExport(item, filename, folders, config); - if (attempt.Change > ChangeType.NoChange) - _mutexService.FireItemCompletedEvent(new uSyncExportedItemNotification(attempt.Item, ChangeType.Export)); + if (attempt.Change > ChangeType.NoChange) + _mutexService.FireItemCompletedEvent(new uSyncExportedItemNotification(attempt.Item, ChangeType.Export)); - return uSyncActionHelper.SetAction(attempt, syncFileService.GetSiteRelativePath(filename), GetItemKey(item), this.Alias).AsEnumerableOfOne(); - } + return uSyncActionHelper.SetAction(attempt, syncFileService.GetSiteRelativePath(filename), GetItemKey(item), this.Alias).AsEnumerableOfOne(); + } - /// - /// Do the meat of the export - /// - /// - /// inheriting this method, means you don't have to repeat all the checks in child handlers. - /// - protected virtual SyncAttempt Export_DoExport(TObject item, string filename, string[] folders, HandlerSettings config) + /// + /// Do the meat of the export + /// + /// + /// inheriting this method, means you don't have to repeat all the checks in child handlers. + /// + protected virtual SyncAttempt Export_DoExport(TObject item, string filename, string[] folders, HandlerSettings config) + { + var attempt = SerializeItem(item, new SyncSerializerOptions(config.Settings)); + if (attempt.Success && attempt.Item is not null) { - var attempt = SerializeItem(item, new SyncSerializerOptions(config.Settings)); - if (attempt.Success) + if (ShouldExport(attempt.Item, config)) { - if (ShouldExport(attempt.Item, config)) - { - // only write the file to disk if it should be exported. - syncFileService.SaveXElement(attempt.Item, filename); + // 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 + if (config.CreateClean && HasChildren(item)) { - return SyncAttempt.Succeed(Path.GetFileName(filename), ChangeType.NoChange, "Not Exported (Based on configuration)"); + CreateCleanFile(GetItemKey(item), filename); } } - - return attempt; + else + { + return SyncAttempt.Succeed(Path.GetFileName(filename), ChangeType.NoChange, "Not Exported (Based on configuration)"); + } } - /// - /// Checks to see if this item is locked at the root level (meaning we shouldn't save it) - /// - protected bool IsLockedAtRoot(string[] folders, string path) - { - if (folders.Length <= 1) return false; + return attempt; + } - if (ExistsInFolders(folders[..^1], path.TrimStart(['\\', '/']))) - { - return uSyncConfig.Settings.LockRoot || uSyncConfig.Settings.LockRootTypes.InvariantContains(EntityType); - } + /// + /// Checks to see if this item is locked at the root level (meaning we shouldn't save it) + /// + protected bool IsLockedAtRoot(string[] folders, string path) + { + if (folders.Length <= 1) return false; - return false; + if (ExistsInFolders(folders[..^1], path.TrimStart(['\\', '/']))) + { + return uSyncConfig.Settings.LockRoot || uSyncConfig.Settings.LockRootTypes.InvariantContains(EntityType); + } + + return false; - bool ExistsInFolders(string[] folders, string path) + bool ExistsInFolders(string[] folders, string path) + { + foreach (var folder in folders) { - foreach (var folder in folders) + if (syncFileService.FileExists(Path.Combine(folder, path.TrimStart(['\\', '/'])))) { - if (syncFileService.FileExists(Path.Combine(folder, path.TrimStart(['\\', '/'])))) - { - return true; - } + return true; } - - return false; } - } - /// - /// 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; + return false; + } + } - /// - /// create a clean file, which is used as a marker, when performing remote deletes. - /// - protected void CreateCleanFile(Guid key, string filename) - { - if (string.IsNullOrWhiteSpace(filename) || key == Guid.Empty) - return; + /// + /// 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; - var folder = Path.GetDirectoryName(filename); - var name = Path.GetFileNameWithoutExtension(filename); + /// + /// create a clean file, which is used as a marker, when performing remote deletes. + /// + protected void CreateCleanFile(Guid key, string filename) + { + if (string.IsNullOrWhiteSpace(filename) || key == Guid.Empty) + return; - var cleanPath = Path.Combine(folder, $"{name}_clean.config"); + var folder = Path.GetDirectoryName(filename); + var name = Path.GetFileNameWithoutExtension(filename); - var node = XElementExtensions.MakeEmpty(key, SyncActionType.Clean, $"clean {name} children"); - node.Add(new XAttribute("itemType", serializer.ItemType)); - syncFileService.SaveXElement(node, cleanPath); - } + if (string.IsNullOrEmpty(folder)) return; - #endregion + var cleanPath = Path.Combine(folder, $"{name}_clean.config"); - #region Reporting + var node = XElementExtensions.MakeEmpty(key, SyncActionType.Clean, $"clean {name} children"); + node.Add(new XAttribute("itemType", serializer.ItemType)); + syncFileService.SaveXElement(node, cleanPath); + } - /// - /// Run a report based on a given folder - /// - public IEnumerable Report(string folder, HandlerSettings config, SyncUpdateCallback callback) - => Report([folder], config, callback); + #endregion - /// - /// Run a report based on a set of folders. - /// - public IEnumerable Report(string[] folders, HandlerSettings config, SyncUpdateCallback callback) - { - List actions = []; + #region Reporting - var cacheKey = PrepCaches(); + /// + /// Run a report based on a given folder + /// + public IEnumerable Report(string folder, HandlerSettings config, SyncUpdateCallback? callback) + => Report([folder], config, callback); - callback?.Invoke("Organising import structure", 1, 3); + /// + /// Run a report based on a set of folders. + /// + public IEnumerable Report(string[] folders, HandlerSettings config, SyncUpdateCallback? callback) + { + List actions = []; - var items = GetMergedItems(folders); + var cacheKey = PrepCaches(); - int count = 0; + callback?.Invoke("Organising import structure", 1, 3); - foreach (var item in items) - { - count++; - callback?.Invoke(Path.GetFileNameWithoutExtension(item.Path), count, items.Count); - actions.AddRange(ReportElement(item.Node, item.FileName, config)); - } + var items = GetMergedItems(folders); - callback?.Invoke("Validating Report", 2, 3); - var validationActions = ReportMissingParents(actions.ToArray()); - actions.AddRange(ReportDeleteCheck(uSyncConfig.GetRootFolder(), validationActions)); + int count = 0; - CleanCaches(cacheKey); - callback?.Invoke("Done", 3, 3); - return actions; + foreach (var item in items) + { + count++; + callback?.Invoke(Path.GetFileNameWithoutExtension(item.Path), count, items.Count); + actions.AddRange(ReportElement(item.Node, item.FileName, config)); } - private List ValidateReport(string folder, List actions) - { - // Alters the existing list, by changing the type as needed. - var validationActions = ReportMissingParents(actions.ToArray()); + callback?.Invoke("Validating Report", 2, 3); + var validationActions = ReportMissingParents(actions.ToArray()); + actions.AddRange(ReportDeleteCheck(uSyncConfig.GetRootFolder(), validationActions)); - // adds new actions - for delete clashes. - validationActions.AddRange(ReportDeleteCheck(folder, validationActions)); + CleanCaches(cacheKey); + callback?.Invoke("Done", 3, 3); + return actions; + } - return validationActions; - } + private List ValidateReport(string folder, List actions) + { + // Alters the existing list, by changing the type as needed. + var validationActions = ReportMissingParents(actions.ToArray()); - /// - /// Check to returned report to see if there is a delete and an update for the same item - /// because if there is then we have issues. - /// - protected virtual IEnumerable ReportDeleteCheck(string folder, IEnumerable actions) - { - var duplicates = new List(); + // adds new actions - for delete clashes. + validationActions.AddRange(ReportDeleteCheck(folder, validationActions)); - // delete checks. - foreach (var deleteAction in actions.Where(x => x.Change != ChangeType.NoChange && x.Change == ChangeType.Delete)) - { - // todo: this is only matching by key, but non-tree based serializers also delete by alias. - // so this check actually has to be booted back down to the serializer. - if (actions.Any(x => x.Change != ChangeType.Delete && DoActionsMatch(x, deleteAction))) - { - var duplicateAction = uSyncActionHelper.ReportActionFail(deleteAction.Name, - $"Duplicate! {deleteAction.Name} exists both as delete and import action"); + return validationActions; + } - // create a detail message to tell people what has happened. - duplicateAction.DetailMessage = "uSync detected a duplicate actions, where am item will be both created and deleted."; - var details = new List(); + /// + /// Check to returned report to see if there is a delete and an update for the same item + /// because if there is then we have issues. + /// + protected virtual IEnumerable ReportDeleteCheck(string folder, IEnumerable actions) + { + var duplicates = new List(); - // add the delete message to the list of changes - var filename = Path.GetFileName(deleteAction.FileName); - var relativePath = deleteAction.FileName.Substring(folder.Length); + // delete checks. + foreach (var deleteAction in actions.Where(x => x.Change != ChangeType.NoChange && x.Change == ChangeType.Delete)) + { + // todo: this is only matching by key, but non-tree based serializers also delete by alias. + // so this check actually has to be booted back down to the serializer. + if (actions.Any(x => x.Change != ChangeType.Delete && DoActionsMatch(x, deleteAction))) + { + var duplicateAction = uSyncActionHelper.ReportActionFail(deleteAction.Name, + $"Duplicate! {deleteAction.Name} exists both as delete and import action"); - details.Add(uSyncChange.Delete(filename, $"Delete: {deleteAction.Name} ({filename}", relativePath)); + // create a detail message to tell people what has happened. + duplicateAction.DetailMessage = "uSync detected a duplicate actions, where am item will be both created and deleted."; + var details = new List(); - // add all the duplicates to the list of changes. - foreach (var dup in actions.Where(x => x.Change != ChangeType.Delete && DoActionsMatch(x, deleteAction))) - { - var dupFilename = Path.GetFileName(dup.FileName); - var dupRelativePath = dup.FileName.Substring(folder.Length); - - details.Add( - uSyncChange.Update( - path: dupFilename, - name: $"{dup.Change} : {dup.Name} ({dupFilename})", - oldValue: "", - newValue: dupRelativePath)); - } + // add the delete message to the list of changes + var filename = Path.GetFileName(deleteAction.FileName) ?? string.Empty; + var relativePath = deleteAction.FileName?.Substring(folder.Length) ?? string.Empty; + + details.Add(uSyncChange.Delete(filename, $"Delete: {deleteAction.Name} ({filename}", relativePath)); - duplicateAction.Details = details; - duplicates.Add(duplicateAction); + // add all the duplicates to the list of changes. + foreach (var dup in actions.Where(x => x.Change != ChangeType.Delete && DoActionsMatch(x, deleteAction))) + { + var dupFilename = Path.GetFileName(dup.FileName) ?? string.Empty; + var dupRelativePath = dup.FileName?.Substring(folder.Length) ?? string.Empty; + + details.Add( + uSyncChange.Update( + path: dupFilename, + name: $"{dup.Change} : {dup.Name} ({dupFilename})", + oldValue: "", + newValue: dupRelativePath)); } - } - return duplicates; + duplicateAction.Details = details; + duplicates.Add(duplicateAction); + } } - /// - /// check to see if an action matches, another action. - /// - /// - /// how two actions match can vary based on handler, in the most part they are matched by key - /// but some items will also check based on the name. - /// - /// when we are dealing with handlers where things can have the same - /// name (tree items, such as content or media), this function has - /// to be overridden to remove the name check. - /// - protected virtual bool DoActionsMatch(uSyncAction a, uSyncAction b) - { - if (a.key == b.key) return true; - if (a.Name.Equals(b.Name, StringComparison.InvariantCultureIgnoreCase)) return true; - return false; - } + return duplicates; + } - /// - /// check if a node matches a item - /// - /// - /// Like above we want to match on key and alias, but only if the alias is unique. - /// however the GetItemAlias function is overridden by tree based handlers to return a unique - /// alias (the key again), so we don't get false positives. - /// - protected virtual bool DoItemsMatch(XElement node, TObject item) - { - if (GetItemKey(item) == node.GetKey()) return true; + /// + /// check to see if an action matches, another action. + /// + /// + /// how two actions match can vary based on handler, in the most part they are matched by key + /// but some items will also check based on the name. + /// + /// when we are dealing with handlers where things can have the same + /// name (tree items, such as content or media), this function has + /// to be overridden to remove the name check. + /// + protected virtual bool DoActionsMatch(uSyncAction a, uSyncAction b) + { + if (a.key == b.key) return true; + if (a.Name.Equals(b.Name, StringComparison.InvariantCultureIgnoreCase)) return true; + return false; + } - // yes this is an or, we've done it explicitly, so you can tell! - if (node.GetAlias().Equals(this.GetItemAlias(item), StringComparison.InvariantCultureIgnoreCase)) return true; + /// + /// check if a node matches a item + /// + /// + /// Like above we want to match on key and alias, but only if the alias is unique. + /// however the GetItemAlias function is overridden by tree based handlers to return a unique + /// alias (the key again), so we don't get false positives. + /// + protected virtual bool DoItemsMatch(XElement node, TObject item) + { + if (GetItemKey(item) == node.GetKey()) return true; - return false; - } + // yes this is an or, we've done it explicitly, so you can tell! + if (node.GetAlias().Equals(this.GetItemAlias(item), StringComparison.InvariantCultureIgnoreCase)) return true; - /// - /// Check report for any items that are missing their parent items - /// - /// - /// The serializers will report if an item is missing a parent item within umbraco, - /// but because the serializer isn't aware of the wider import (all the other items) - /// it can't say if the parent is in the import. - /// - /// This method checks for the parent of an item in the wider list of items being - /// imported. - /// - private List ReportMissingParents(uSyncAction[] actions) - { - for (int i = 0; i < actions.Length; i++) - { - if (actions[i].Change != ChangeType.ParentMissing) continue; + return false; + } + + /// + /// Check report for any items that are missing their parent items + /// + /// + /// The serializers will report if an item is missing a parent item within umbraco, + /// but because the serializer isn't aware of the wider import (all the other items) + /// it can't say if the parent is in the import. + /// + /// This method checks for the parent of an item in the wider list of items being + /// imported. + /// + private List ReportMissingParents(uSyncAction[] actions) + { + for (int i = 0; i < actions.Length; i++) + { + if (actions[i].Change != ChangeType.ParentMissing || actions[i].FileName is null) continue; - var node = XElement.Load(actions[i].FileName); - var guid = node.GetParentKey(); + var node = XElement.Load(actions[i].FileName!); + var guid = node.GetParentKey(); - if (guid != Guid.Empty) + if (guid != Guid.Empty) + { + if (actions.Any(x => x.key == guid && (x.Change < ChangeType.Fail || x.Change == ChangeType.ParentMissing))) { - if (actions.Any(x => x.key == guid && (x.Change < ChangeType.Fail || x.Change == ChangeType.ParentMissing))) - { - logger.LogDebug("Found existing key in actions {item}", actions[i].Name); - actions[i].Change = ChangeType.Create; - } - else - { - logger.LogWarning("{item} is missing a parent", actions[i].Name); - } + logger.LogDebug("Found existing key in actions {item}", actions[i].Name); + actions[i].Change = ChangeType.Create; + } + else + { + logger.LogWarning("{item} is missing a parent", actions[i].Name); } } - - return actions.ToList(); } - /// - /// Run a report on a given folder - /// - public virtual IEnumerable ReportFolder(string folder, HandlerSettings config, SyncUpdateCallback callback) - { + return actions.ToList(); + } - List actions = new List(); + /// + /// Run a report on a given folder + /// + public virtual IEnumerable ReportFolder(string folder, HandlerSettings config, SyncUpdateCallback? callback) + { - var files = GetImportFiles(folder).ToList(); + List actions = new List(); - int count = 0; + var files = GetImportFiles(folder).ToList(); - logger.LogDebug("ReportFolder: {folder} ({count} files)", folder, files.Count); + int count = 0; - foreach (string file in files) - { - count++; - callback?.Invoke(Path.GetFileNameWithoutExtension(file), count, files.Count); + logger.LogDebug("ReportFolder: {folder} ({count} files)", folder, files.Count); - actions.AddRange(ReportItem(file, config)); - } + foreach (string file in files) + { + count++; + callback?.Invoke(Path.GetFileNameWithoutExtension(file), count, files.Count); - foreach (var children in syncFileService.GetDirectories(folder)) - { - actions.AddRange(ReportFolder(children, config, callback)); - } + actions.AddRange(ReportItem(file, config)); + } - return actions; + foreach (var children in syncFileService.GetDirectories(folder)) + { + actions.AddRange(ReportFolder(children, config, callback)); } - /// - /// Report on any changes for a single XML node. - /// - protected virtual IEnumerable ReportElement(XElement node, string filename, HandlerSettings config) - => ReportElement(node, filename, config ?? this.DefaultConfig, new uSyncImportOptions()); + return actions; + } + + /// + /// Report on any changes for a single XML node. + /// + protected virtual IEnumerable ReportElement(XElement node, string filename, HandlerSettings? config) + => ReportElement(node, filename, config ?? this.DefaultConfig, new uSyncImportOptions()); - /// - /// Report an Element - /// - public IEnumerable ReportElement(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) + /// + /// Report an Element + /// + public IEnumerable ReportElement(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) + { + try { - try + // starting reporting notification + // this lets us intercept a report and + // shortcut the checking (sometimes). + if (_mutexService.FireItemStartingEvent(new uSyncReportingItemNotification(node))) { - // starting reporting notification - // this lets us intercept a report and - // shortcut the checking (sometimes). - if (_mutexService.FireItemStartingEvent(new uSyncReportingItemNotification(node))) - { - return uSyncActionHelper - .ReportAction(ChangeType.NoChange, node.GetAlias(), node.GetPath(), GetNameFromFileOrNode(filename, node), node.GetKey(), this.Alias, - "Change stopped by delegate event") - .AsEnumerableOfOne(); - } + return uSyncActionHelper + .ReportAction(ChangeType.NoChange, node.GetAlias(), node.GetPath(), GetNameFromFileOrNode(filename, node), node.GetKey(), this.Alias, + "Change stopped by delegate event") + .AsEnumerableOfOne(); + } - var actions = new List(); + var actions = new List(); - // get the serializer options - var serializerOptions = new SyncSerializerOptions(options.Flags, settings.Settings, options.UserId); - serializerOptions.MergeSettings(options.Settings); + // get the serializer options + var serializerOptions = new SyncSerializerOptions(options.Flags, settings.Settings, options.UserId); + serializerOptions.MergeSettings(options.Settings); - // check if this item is current (the provided XML and exported XML match) - var change = IsItemCurrent(node, serializerOptions); + // check if this item is current (the provided XML and exported XML match) + var change = IsItemCurrent(node, serializerOptions); - var action = uSyncActionHelper - .ReportAction(change.Change, node.GetAlias(), node.GetPath(), GetNameFromFileOrNode(filename, node), node.GetKey(), this.Alias, ""); + var action = uSyncActionHelper + .ReportAction(change.Change, node.GetAlias(), node.GetPath(), GetNameFromFileOrNode(filename, node), node.GetKey(), this.Alias, ""); - action.Message = ""; + action.Message = ""; - if (action.Change == ChangeType.Clean) - { - actions.AddRange(CleanFolder(filename, true, settings.UseFlatStructure)); - } - else if (action.Change > ChangeType.NoChange) + if (action.Change == ChangeType.Clean) + { + actions.AddRange(CleanFolder(filename, true, settings.UseFlatStructure)); + } + else if (action.Change > ChangeType.NoChange) + { + if (change.CurrentNode is not null) { action.Details = GetChanges(node, change.CurrentNode, serializerOptions); if (action.Change != ChangeType.Create && (action.Details == null || action.Details.Count() == 0)) @@ -1383,804 +1400,806 @@ public IEnumerable ReportElement(XElement node, string filename, Ha { action.Message = $"{action.Change}"; } - actions.Add(action); - } - else - { - actions.Add(action); } - - // tell other things we have reported this item. - _mutexService.FireItemCompletedEvent(new uSyncReportedItemNotification(node, action.Change)); - - return actions; + actions.Add(action); } - catch (FormatException fex) + else { - return uSyncActionHelper - .ReportActionFail(Path.GetFileName(node.GetAlias()), $"format error {fex.Message}") - .AsEnumerableOfOne(); + actions.Add(action); } - } - private uSyncChange MakeRawChange(XElement node, XElement current, SyncSerializerOptions options) - { - if (current != null) - return uSyncChange.Update(node.GetAlias(), "Raw XML", current.ToString(), node.ToString()); + // tell other things we have reported this item. + _mutexService.FireItemCompletedEvent(new uSyncReportedItemNotification(node, action.Change)); - return uSyncChange.NoChange(node.GetAlias(), node.GetAlias()); + return actions; + } + catch (FormatException fex) + { + return uSyncActionHelper + .ReportActionFail(Path.GetFileName(node.GetAlias()), $"format error {fex.Message}") + .AsEnumerableOfOne(); } + } + + private uSyncChange MakeRawChange(XElement node, XElement current, SyncSerializerOptions options) + { + if (current != null) + return uSyncChange.Update(node.GetAlias(), "Raw XML", current.ToString(), node.ToString()); + + return uSyncChange.NoChange(node.GetAlias(), node.GetAlias()); + } - /// - /// Run a report on a single file. - /// - protected IEnumerable ReportItem(string file, HandlerSettings config) + /// + /// Run a report on a single file. + /// + protected IEnumerable ReportItem(string file, HandlerSettings config) + { + try { - try - { - var node = syncFileService.LoadXElement(file); + var node = syncFileService.LoadXElement(file); - if (ShouldImport(node, config)) - { - return ReportElement(node, file, config); - } - else - { - return uSyncActionHelper.ReportAction(ChangeType.NoChange, node.GetAlias(), node.GetPath(), syncFileService.GetSiteRelativePath(file), node.GetKey(), - this.Alias, "Will not be imported (Based on configuration)") - .AsEnumerableOfOne(); - } + if (ShouldImport(node, config)) + { + return ReportElement(node, file, config); } - catch (Exception ex) + else { - return uSyncActionHelper - .ReportActionFail(Path.GetFileName(file), $"Reporting error {ex.Message}") - .AsEnumerableOfOne(); + return uSyncActionHelper.ReportAction(ChangeType.NoChange, node.GetAlias(), node.GetPath(), syncFileService.GetSiteRelativePath(file), node.GetKey(), + this.Alias, "Will not be imported (Based on configuration)") + .AsEnumerableOfOne(); } - + } + catch (Exception ex) + { + return uSyncActionHelper + .ReportActionFail(Path.GetFileName(file), $"Reporting error {ex.Message}") + .AsEnumerableOfOne(); } + } - private IEnumerable GetChanges(XElement node, XElement currentNode, SyncSerializerOptions options) - => itemFactory.GetChanges(node, currentNode, options); - - #endregion - #region Notification Events + private IEnumerable GetChanges(XElement node, XElement currentNode, SyncSerializerOptions options) + => itemFactory.GetChanges(node, currentNode, options); - /// - /// calculate if this handler should process the events. - /// - /// - /// will check if uSync is paused, the handler is enabled or the action is set. - /// - protected bool ShouldProcessEvent() - { - if (_mutexService.IsPaused) return false; - if (!DefaultConfig.Enabled) return false; + #endregion + #region Notification Events - var group = !string.IsNullOrWhiteSpace(DefaultConfig.Group) ? DefaultConfig.Group : this.Group; + /// + /// calculate if this handler should process the events. + /// + /// + /// will check if uSync is paused, the handler is enabled or the action is set. + /// + protected bool ShouldProcessEvent() + { + if (_mutexService.IsPaused) return false; + if (!DefaultConfig.Enabled) return false; - if (uSyncConfig.Settings.ExportOnSave.InvariantContains("All") || - uSyncConfig.Settings.ExportOnSave.InvariantContains(group)) - { - return HandlerActions.Save.IsValidAction(DefaultConfig.Actions); - } - return false; - } + var group = !string.IsNullOrWhiteSpace(DefaultConfig.Group) ? DefaultConfig.Group : this.Group; - /// - /// Handle an Umbraco Delete notification - /// - public virtual void Handle(DeletedNotification notification) + if (uSyncConfig.Settings.ExportOnSave.InvariantContains("All") || + uSyncConfig.Settings.ExportOnSave.InvariantContains(group)) { - if (!ShouldProcessEvent()) return; - - foreach (var item in notification.DeletedEntities) - { - try - { - var handlerFolders = GetDefaultHandlerFolders(); - ExportDeletedItem(item, handlerFolders, DefaultConfig); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to create delete marker"); - notification.Messages.Add(new EventMessage("uSync", $"Failed to mark as deleted : {ex.Message}", EventMessageType.Warning)); - } - } + return HandlerActions.Save.IsValidAction(DefaultConfig.Actions); } - /// - /// Handle the Umbraco Saved notification for items. - /// - /// - public virtual void Handle(SavedNotification notification) - { - if (!ShouldProcessEvent()) return; - if (notification.State.TryGetValue(uSync.EventPausedKey, out var paused) && paused is true) - return; + return false; + } - var handlerFolders = GetDefaultHandlerFolders(); + /// + /// Handle an Umbraco Delete notification + /// + public virtual void Handle(DeletedNotification notification) + { + if (!ShouldProcessEvent()) return; - foreach (var item in notification.SavedEntities) + foreach (var item in notification.DeletedEntities) + { + try { - try - { - var attempts = Export(item, handlerFolders, DefaultConfig); - foreach (var attempt in attempts.Where(x => x.Success)) - { - this.CleanUp(item, attempt.FileName, handlerFolders.Last()); - } - } - catch (Exception ex) - { - logger.LogWarning(ex, "Failed to create uSync export file"); - notification.Messages.Add(new EventMessage("uSync", $"Failed to create export file : {ex.Message}", EventMessageType.Warning)); - } + var handlerFolders = GetDefaultHandlerFolders(); + ExportDeletedItem(item, handlerFolders, DefaultConfig); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to create delete marker"); + notification.Messages.Add(new EventMessage("uSync", $"Failed to mark as deleted : {ex.Message}", EventMessageType.Warning)); } } + } + + /// + /// Handle the Umbraco Saved notification for items. + /// + /// + public virtual void Handle(SavedNotification notification) + { + if (!ShouldProcessEvent()) return; + if (notification.State.TryGetValue(uSync.EventPausedKey, out var paused) && paused is true) + return; + + var handlerFolders = GetDefaultHandlerFolders(); - /// - /// Handle the Umbraco moved notification for items. - /// - /// - public virtual void Handle(MovedNotification notification) + foreach (var item in notification.SavedEntities) { try { - if (!ShouldProcessEvent()) return; - HandleMove(notification.MoveInfoCollection); + var attempts = Export(item, handlerFolders, DefaultConfig); + foreach (var attempt in attempts.Where(x => x.Success)) + { + if (attempt.FileName is null) continue; + this.CleanUp(item, attempt.FileName, handlerFolders.Last()); + } } catch (Exception ex) { - logger.LogWarning(ex, "Failed to export move operation"); - notification.Messages.Add(new EventMessage("uSync", $"Failed to export move : {ex.Message}", EventMessageType.Warning)); + logger.LogWarning(ex, "Failed to create uSync export file"); + notification.Messages.Add(new EventMessage("uSync", $"Failed to create export file : {ex.Message}", EventMessageType.Warning)); } + } + } + /// + /// Handle the Umbraco moved notification for items. + /// + /// + public virtual void Handle(MovedNotification notification) + { + try + { + if (!ShouldProcessEvent()) return; + HandleMove(notification.MoveInfoCollection); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to export move operation"); + notification.Messages.Add(new EventMessage("uSync", $"Failed to export move : {ex.Message}", EventMessageType.Warning)); } - /// - /// Process a collection of move events - /// - /// - /// This has been separated out, because we also call this code when a handler supports - /// recycle bin events - /// - protected void HandleMove(IEnumerable> moveInfoCollection) + } + + /// + /// Process a collection of move events + /// + /// + /// This has been separated out, because we also call this code when a handler supports + /// recycle bin events + /// + protected void HandleMove(IEnumerable> moveInfoCollection) + { + foreach (var item in moveInfoCollection) { - foreach (var item in moveInfoCollection) + var handlerFolders = GetDefaultHandlerFolders(); + var attempts = Export(item.Entity, handlerFolders, DefaultConfig); + + if (!this.DefaultConfig.UseFlatStructure) { - var handlerFolders = GetDefaultHandlerFolders(); - var attempts = Export(item.Entity, handlerFolders, DefaultConfig); + // moves only need cleaning up if we are not using flat, because + // with flat the file will always be in the same folder. - if (!this.DefaultConfig.UseFlatStructure) + foreach (var attempt in attempts.Where(x => x.Success is true)) { - // moves only need cleaning up if we are not using flat, because - // with flat the file will always be in the same folder. - - foreach (var attempt in attempts.Where(x => x.Success)) - { - this.CleanUp(item.Entity, attempt.FileName, handlerFolders.Last()); - } + if (attempt.FileName is null) continue; + this.CleanUp(item.Entity, attempt.FileName, handlerFolders.Last()); } } } + } + + /// + /// Export any deletes items to disk + /// + /// + /// Deleted items get 'empty' files on disk so we know they where deleted + /// + protected virtual void ExportDeletedItem(TObject item, string folder, HandlerSettings config) + => ExportDeletedItem(item, [folder], config); - /// - /// Export any deletes items to disk - /// - /// - /// Deleted items get 'empty' files on disk so we know they where deleted - /// - protected virtual void ExportDeletedItem(TObject item, string folder, HandlerSettings config) - => ExportDeletedItem(item, [folder], config); + /// + /// Export any deletes items to disk + /// + /// + /// Deleted items get 'empty' files on disk so we know they where deleted + /// + protected virtual void ExportDeletedItem(TObject item, string[] folders, HandlerSettings config) + { + if (item == null) return; - /// - /// Export any deletes items to disk - /// - /// - /// Deleted items get 'empty' files on disk so we know they where deleted - /// - protected virtual void ExportDeletedItem(TObject item, string[] folders, HandlerSettings config) - { - if (item == null) return; + var targetFolder = folders.Last(); - var targetFolder = folders.Last(); + var filename = GetPath(targetFolder, item, config.GuidNames, config.UseFlatStructure) + .ToAppSafeFileName(); - var filename = GetPath(targetFolder, item, config.GuidNames, config.UseFlatStructure) - .ToAppSafeFileName(); + if (IsLockedAtRoot(folders, filename.Substring(targetFolder.Length + 1))) + { + // don't do anything this thing exists at a higher level. ! + return; + } - if (IsLockedAtRoot(folders, filename.Substring(targetFolder.Length + 1))) - { - // don't do anything this thing exists at a higher level. ! - return; - } - - var attempt = serializer.SerializeEmpty(item, SyncActionType.Delete, string.Empty); - if (ShouldExport(attempt.Item, config)) + var attempt = serializer.SerializeEmpty(item, SyncActionType.Delete, string.Empty); + if (attempt.Item is not null && ShouldExport(attempt.Item, config) is true) + { + if (attempt.Success && attempt.Change != ChangeType.NoChange) { - if (attempt.Success && attempt.Change != ChangeType.NoChange) - { - syncFileService.SaveXElement(attempt.Item, filename); + syncFileService.SaveXElement(attempt.Item, filename); - // so check - it shouldn't (under normal operation) - // be possible for a clash to exist at delete, because nothing else - // will have changed (like name or location) + // so check - it shouldn't (under normal operation) + // be possible for a clash to exist at delete, because nothing else + // will have changed (like name or location) - // we only then do this if we are not using flat structure. - if (!DefaultConfig.UseFlatStructure) - this.CleanUp(item, filename, Path.Combine(folders.Last(), this.DefaultFolder)); - } + // we only then do this if we are not using flat structure. + if (!DefaultConfig.UseFlatStructure) + this.CleanUp(item, filename, Path.Combine(folders.Last(), this.DefaultFolder)); } } + } - /// - /// get all the possible folders for this handlers - /// - protected string[] GetDefaultHandlerFolders() - => rootFolders.Select(f => Path.Combine(f, DefaultFolder)).ToArray(); + /// + /// get all the possible folders for this handlers + /// + protected string[] GetDefaultHandlerFolders() + => rootFolders.Select(f => Path.Combine(f, DefaultFolder)).ToArray(); - /// - /// Cleans up the handler folder, removing duplicate files for this item - /// - /// - /// e.g if someone renames a thing (and we are using the name in the file) - /// this will clean anything else in the folder that has that key / alias - /// - protected virtual void CleanUp(TObject item, string newFile, string folder) - { - var physicalFile = syncFileService.GetAbsPath(newFile); + /// + /// Cleans up the handler folder, removing duplicate files for this item + /// + /// + /// e.g if someone renames a thing (and we are using the name in the file) + /// this will clean anything else in the folder that has that key / alias + /// + protected virtual void CleanUp(TObject item, string newFile, string folder) + { + var physicalFile = syncFileService.GetAbsPath(newFile); - var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}"); + var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}"); - foreach (string file in files) + foreach (string file in files) + { + // compare the file paths. + if (!syncFileService.PathMatches(physicalFile, file)) // This is not the same file, as we are saving. { - // compare the file paths. - if (!syncFileService.PathMatches(physicalFile, file)) // This is not the same file, as we are saving. + try { - try - { - var node = syncFileService.LoadXElement(file); + var node = syncFileService.LoadXElement(file); + + // if this XML file matches the item we have just saved. - // if this XML file matches the item we have just saved. + if (!node.IsEmptyItem() || node.GetEmptyAction() != SyncActionType.Rename) + { + // the node isn't empty, or its not a rename (because all clashes become renames) - if (!node.IsEmptyItem() || node.GetEmptyAction() != SyncActionType.Rename) + if (DoItemsMatch(node, item)) { - // the node isn't empty, or its not a rename (because all clashes become renames) + logger.LogDebug("Duplicate {file} of {alias}, saving as rename", Path.GetFileName(file), this.GetItemAlias(item)); - if (DoItemsMatch(node, item)) + var attempt = serializer.SerializeEmpty(item, SyncActionType.Rename, node.GetAlias()); + if (attempt.Success && attempt.Item is not null) { - logger.LogDebug("Duplicate {file} of {alias}, saving as rename", Path.GetFileName(file), this.GetItemAlias(item)); - - var attempt = serializer.SerializeEmpty(item, SyncActionType.Rename, node.GetAlias()); - if (attempt.Success) - { - syncFileService.SaveXElement(attempt.Item, file); - } + syncFileService.SaveXElement(attempt.Item, file); } } } - catch (Exception ex) - { - logger.LogWarning("Error during cleanup of existing files {message}", ex.Message); - // cleanup should fail silently ? - because it can impact on normal Umbraco operations? - } + } + catch (Exception ex) + { + logger.LogWarning("Error during cleanup of existing files {message}", ex.Message); + // cleanup should fail silently ? - because it can impact on normal Umbraco operations? } } + } - var folders = syncFileService.GetDirectories(folder); - foreach (var children in folders) - { - CleanUp(item, newFile, children); - } + var folders = syncFileService.GetDirectories(folder); + foreach (var children in folders) + { + CleanUp(item, newFile, children); } + } - #endregion - - // 98% of the time the serializer can do all these calls for us, - // but for blueprints, we want to get different items, (but still use the - // content serializer) so we override them. - - - /// - /// Fetch an item via the Serializer - /// - protected virtual TObject GetFromService(int id) => serializer.FindItem(id); - - /// - /// Fetch an item via the Serializer - /// - protected virtual TObject GetFromService(Guid key) => serializer.FindItem(key); - - /// - /// Fetch an item via the Serializer - /// - protected virtual TObject GetFromService(string alias) => serializer.FindItem(alias); - - /// - /// Delete an item via the Serializer - /// - protected virtual void DeleteViaService(TObject item) => serializer.DeleteItem(item); - - /// - /// Get the alias of an item from the Serializer - /// - protected string GetItemAlias(TObject item) => serializer.ItemAlias(item); - - /// - /// Get the Key of an item from the Serializer - /// - protected Guid GetItemKey(TObject item) => serializer.ItemKey(item); - - /// - /// Get a container item from the Umbraco service. - /// - abstract protected TObject GetFromService(TContainer item); - - /// - /// Get a container item from the Umbraco service. - /// - virtual protected TContainer GetContainer(Guid key) => default; - - /// - /// Get a container item from the Umbraco service. - /// - virtual protected TContainer GetContainer(int id) => default; - - /// - /// Get the file path to use for an item - /// - /// Item to derive path for - /// should we use the key value in the path - /// should the file be flat and ignore any sub folders? - /// relative file path to use for an item - virtual protected string GetItemPath(TObject item, bool useGuid, bool isFlat) - => useGuid ? GetItemKey(item).ToString() : GetItemFileName(item); - - /// - /// Get the file name to use for an item - /// - virtual protected string GetItemFileName(TObject item) - => GetItemAlias(item).ToSafeFileName(shortStringHelper); - - /// - /// Get the name of a supplied item - /// - abstract protected string GetItemName(TObject item); - - /// - /// Calculate the relative Physical path value for any item - /// - /// - /// this is where a item is saved on disk in relation to the uSync folder - /// - virtual protected string GetPath(string folder, TObject item, bool GuidNames, bool isFlat) - { - if (isFlat && GuidNames) return Path.Combine(folder, $"{GetItemKey(item)}.{this.uSyncConfig.Settings.DefaultExtension}"); - var path = Path.Combine(folder, $"{this.GetItemPath(item, GuidNames, isFlat)}.{this.uSyncConfig.Settings.DefaultExtension}"); - - // if this is flat but not using GUID filenames, then we check for clashes. - if (isFlat && !GuidNames) return CheckAndFixFileClash(path, item); - return path; - } - - - /// - /// Get a clean filename that doesn't clash with any existing items. - /// - /// - /// clashes we want to resolve can occur when the safeFilename for an item - /// matches with the safe file name for something else. e.g - /// 1 Special Doc-type - /// 2 Special Doc-type - /// - /// Will both resolve to SpecialDocType.Config - /// - /// the first item to be written to disk for a clash will get the 'normal' name - /// all subsequent items will get the appended name. - /// - /// this can be completely sidestepped by using GUID filenames. - /// - virtual protected string CheckAndFixFileClash(string path, TObject item) - { - if (syncFileService.FileExists(path)) - { - var node = syncFileService.LoadXElement(path); + #endregion - if (node == null) return path; - if (GetItemKey(item) == node.GetKey()) return path; - if (GetXmlMatchString(node) == GetItemMatchString(item)) return path; + // 98% of the time the serializer can do all these calls for us, + // but for blueprints, we want to get different items, (but still use the + // content serializer) so we override them. - // get here we have a clash, we should append something - var append = GetItemKey(item).ToShortKeyString(8); // (this is the shortened GUID like media folders do) - return Path.Combine(Path.GetDirectoryName(path), - Path.GetFileNameWithoutExtension(path) + "_" + append + Path.GetExtension(path)); - } - return path; - } + /// + /// Fetch an item via the Serializer + /// + protected virtual TObject? GetFromService(int id) => serializer.FindItem(id); - /// - /// a string we use to match this item, with other (where there are levels) - /// - /// - /// this works because unless it's content/media you can't actually have - /// clashing aliases at different levels in the folder structure. - /// - /// So just checking the alias works, for content we overwrite these two functions. - /// - protected virtual string GetItemMatchString(TObject item) => GetItemAlias(item); + /// + /// Fetch an item via the Serializer + /// + protected virtual TObject? GetFromService(Guid key) => serializer.FindItem(key); - /// - /// Calculate the matching item string from the loaded uSync XML element - /// - protected virtual string GetXmlMatchString(XElement node) => node.GetAlias(); + /// + /// Fetch an item via the Serializer + /// + protected virtual TObject? GetFromService(string alias) => serializer.FindItem(alias); - /// - /// Rename an item - /// - /// - /// This doesn't get called, because renames generally are handled in the serialization because we match by key. - /// - virtual public uSyncAction Rename(TObject item) => new uSyncAction(); + /// + /// Delete an item via the Serializer + /// + protected virtual void DeleteViaService(TObject item) => serializer.DeleteItem(item); + /// + /// Get the alias of an item from the Serializer + /// + protected string GetItemAlias(TObject item) => serializer.ItemAlias(item); - /// - /// Group a handler belongs too (default will be settings) - /// - public virtual string Group { get; protected set; } = uSyncConstants.Groups.Settings; + /// + /// Get the Key of an item from the Serializer + /// + protected Guid GetItemKey(TObject item) => serializer.ItemKey(item); - /// - /// Serialize an item to XML based on a given UDI value - /// - public SyncAttempt GetElement(Udi udi) - { - var element = FindByUdi(udi); - if (element != null) - return SerializeItem(element, new SyncSerializerOptions()); + /// + /// Get a container item from the Umbraco service. + /// + abstract protected TObject? GetFromService(TContainer? item); - return SyncAttempt.Fail(udi.ToString(), ChangeType.Fail, "Item not found"); - } + /// + /// Get a container item from the Umbraco service. + /// + virtual protected TContainer? GetContainer(Guid key) => default; + /// + /// Get a container item from the Umbraco service. + /// + virtual protected TContainer? GetContainer(int id) => default; - private TObject FindByUdi(Udi udi) - { - switch (udi) - { - case GuidUdi guidUdi: - return GetFromService(guidUdi.Guid); - case StringUdi stringUdi: - return GetFromService(stringUdi.Id); - } + /// + /// Get the file path to use for an item + /// + /// Item to derive path for + /// should we use the key value in the path + /// should the file be flat and ignore any sub folders? + /// relative file path to use for an item + virtual protected string GetItemPath(TObject item, bool useGuid, bool isFlat) + => useGuid ? GetItemKey(item).ToString() : GetItemFileName(item); - return default; - } + /// + /// Get the file name to use for an item + /// + virtual protected string GetItemFileName(TObject item) + => GetItemAlias(item).ToSafeFileName(shortStringHelper); + + /// + /// Get the name of a supplied item + /// + abstract protected string GetItemName(TObject item); + + /// + /// Calculate the relative Physical path value for any item + /// + /// + /// this is where a item is saved on disk in relation to the uSync folder + /// + virtual protected string GetPath(string folder, TObject item, bool GuidNames, bool isFlat) + { + if (isFlat && GuidNames) return Path.Combine(folder, $"{GetItemKey(item)}.{this.uSyncConfig.Settings.DefaultExtension}"); + var path = Path.Combine(folder, $"{this.GetItemPath(item, GuidNames, isFlat)}.{this.uSyncConfig.Settings.DefaultExtension}"); - /// - /// Calculate any dependencies for any given item based on loaded dependency checkers - /// - /// - /// uSync contains no dependency checkers by default - uSync.Complete will load checkers - /// when installed. - /// - public IEnumerable GetDependencies(Guid key, DependencyFlags flags) + // if this is flat but not using GUID filenames, then we check for clashes. + if (isFlat && !GuidNames) return CheckAndFixFileClash(path, item); + return path; + } + + + /// + /// Get a clean filename that doesn't clash with any existing items. + /// + /// + /// clashes we want to resolve can occur when the safeFilename for an item + /// matches with the safe file name for something else. e.g + /// 1 Special Doc-type + /// 2 Special Doc-type + /// + /// Will both resolve to SpecialDocType.Config + /// + /// the first item to be written to disk for a clash will get the 'normal' name + /// all subsequent items will get the appended name. + /// + /// this can be completely sidestepped by using GUID filenames. + /// + virtual protected string CheckAndFixFileClash(string path, TObject item) + { + if (syncFileService.FileExists(path)) { - if (key == Guid.Empty) - { - return GetContainerDependencies(default, flags); - } - else - { - var item = this.GetFromService(key); - if (item == null) - { - var container = this.GetContainer(key); - if (container != null) - { - return GetContainerDependencies(container, flags); - } - return Enumerable.Empty(); - } + var node = syncFileService.LoadXElement(path); - return GetDependencies(item, flags); - } + if (node == null) return path; + if (GetItemKey(item) == node.GetKey()) return path; + if (GetXmlMatchString(node) == GetItemMatchString(item)) return path; + + // get here we have a clash, we should append something + var append = GetItemKey(item).ToShortKeyString(8); // (this is the shortened GUID like media folders do) + return Path.Combine(Path.GetDirectoryName(path) ?? string.Empty, + Path.GetFileNameWithoutExtension(path) + "_" + append + Path.GetExtension(path)); } - /// - /// Calculate any dependencies for any given item based on loaded dependency checkers - /// - /// - /// uSync contains no dependency checkers by default - uSync.Complete will load checkers - /// when installed. - /// - public IEnumerable GetDependencies(int id, DependencyFlags flags) + return path; + } + + /// + /// a string we use to match this item, with other (where there are levels) + /// + /// + /// this works because unless it's content/media you can't actually have + /// clashing aliases at different levels in the folder structure. + /// + /// So just checking the alias works, for content we overwrite these two functions. + /// + protected virtual string GetItemMatchString(TObject item) => GetItemAlias(item); + + /// + /// Calculate the matching item string from the loaded uSync XML element + /// + protected virtual string GetXmlMatchString(XElement node) => node.GetAlias(); + + /// + /// Rename an item + /// + /// + /// This doesn't get called, because renames generally are handled in the serialization because we match by key. + /// + virtual public uSyncAction Rename(TObject item) => new uSyncAction(); + + + /// + /// Group a handler belongs too (default will be settings) + /// + public virtual string Group { get; protected set; } = uSyncConstants.Groups.Settings; + + /// + /// Serialize an item to XML based on a given UDI value + /// + public SyncAttempt GetElement(Udi udi) + { + var element = FindByUdi(udi); + if (element != null) + return SerializeItem(element, new SyncSerializerOptions()); + + return SyncAttempt.Fail(udi.ToString(), ChangeType.Fail, "Item not found"); + } + + + private TObject? FindByUdi(Udi udi) + { + switch (udi) { - // get them from the root. - if (id == -1) return GetContainerDependencies(default, flags); + case GuidUdi guidUdi: + return GetFromService(guidUdi.Guid); + case StringUdi stringUdi: + return GetFromService(stringUdi.Id); + } + + return default; + } - var item = this.GetFromService(id); + /// + /// Calculate any dependencies for any given item based on loaded dependency checkers + /// + /// + /// uSync contains no dependency checkers by default - uSync.Complete will load checkers + /// when installed. + /// + public IEnumerable GetDependencies(Guid key, DependencyFlags flags) + { + if (key == Guid.Empty) + { + return GetContainerDependencies(default, flags); + } + else + { + var item = this.GetFromService(key); if (item == null) { - var container = this.GetContainer(id); + var container = this.GetContainer(key); if (container != null) { return GetContainerDependencies(container, flags); } - return Enumerable.Empty(); } + return GetDependencies(item, flags); } + } - private bool HasDependencyCheckers() - => dependencyCheckers != null && dependencyCheckers.Count > 0; - + /// + /// Calculate any dependencies for any given item based on loaded dependency checkers + /// + /// + /// uSync contains no dependency checkers by default - uSync.Complete will load checkers + /// when installed. + /// + public IEnumerable GetDependencies(int id, DependencyFlags flags) + { + // get them from the root. + if (id == -1) return GetContainerDependencies(default, flags); - /// - /// Calculate any dependencies for any given item based on loaded dependency checkers - /// - /// - /// uSync contains no dependency checkers by default - uSync.Complete will load checkers - /// when installed. - /// - protected IEnumerable GetDependencies(TObject item, DependencyFlags flags) + var item = this.GetFromService(id); + if (item == null) { - if (item == null || !HasDependencyCheckers()) return Enumerable.Empty(); - - var dependencies = new List(); - foreach (var checker in dependencyCheckers) + var container = this.GetContainer(id); + if (container != null) { - dependencies.AddRange(checker.GetDependencies(item, flags)); + return GetContainerDependencies(container, flags); } - return dependencies; + + return Enumerable.Empty(); } + return GetDependencies(item, flags); + } + + private bool HasDependencyCheckers() + => dependencyCheckers != null && dependencyCheckers.Count > 0; + + + /// + /// Calculate any dependencies for any given item based on loaded dependency checkers + /// + /// + /// uSync contains no dependency checkers by default - uSync.Complete will load checkers + /// when installed. + /// + protected IEnumerable GetDependencies(TObject item, DependencyFlags flags) + { + if (item == null || !HasDependencyCheckers()) return Enumerable.Empty(); - /// - /// Calculate any dependencies for any given item based on loaded dependency checkers - /// - /// - /// uSync contains no dependency checkers by default - uSync.Complete will load checkers - /// when installed. - /// - private IEnumerable GetContainerDependencies(TContainer parent, DependencyFlags flags) + var dependencies = new List(); + foreach (var checker in dependencyCheckers) { - if (!HasDependencyCheckers()) return Enumerable.Empty(); + dependencies.AddRange(checker.GetDependencies(item, flags)); + } + return dependencies; + } + + /// + /// Calculate any dependencies for any given item based on loaded dependency checkers + /// + /// + /// uSync contains no dependency checkers by default - uSync.Complete will load checkers + /// when installed. + /// + private IEnumerable GetContainerDependencies(TContainer? parent, DependencyFlags flags) + { + if (!HasDependencyCheckers()) return Enumerable.Empty(); - var dependencies = new List(); + var dependencies = new List(); - var containers = GetFolders(parent); - if (containers != null && containers.Any()) + var containers = GetFolders(parent); + if (containers != null && containers.Any()) + { + foreach (var container in containers) { - foreach (var container in containers) - { - dependencies.AddRange(GetContainerDependencies(container, flags)); - } + dependencies.AddRange(GetContainerDependencies(container, flags)); } + } - var children = GetChildItems(parent); - if (children != null && children.Any()) + var children = GetChildItems(parent); + if (children != null && children.Any()) + { + foreach (var child in children) { - foreach (var child in children) + var childItem = GetFromService(child); + if (childItem != null) { - var childItem = GetFromService(child); - if (childItem != null) + foreach (var checker in dependencyCheckers) { - foreach (var checker in dependencyCheckers) - { - dependencies.AddRange(checker.GetDependencies(childItem, flags)); - } + dependencies.AddRange(checker.GetDependencies(childItem, flags)); } } } - - return dependencies.SafeDistinctBy(x => x.Udi.ToString()).OrderByDescending(x => x.Order); } - #region Serializer Calls + return dependencies.DistinctBy(x => x.Udi?.ToString() ?? x.Name).OrderByDescending(x => x.Order); + } + + #region Serializer Calls - /// - /// call the serializer to get an items xml. - /// - protected SyncAttempt SerializeItem(TObject item, SyncSerializerOptions options) - => serializer.Serialize(item, options); + /// + /// call the serializer to get an items xml. + /// + protected SyncAttempt SerializeItem(TObject item, SyncSerializerOptions options) + => serializer.Serialize(item, options); - /// - /// - /// turn the xml into an item (and optionally save it to umbraco). - /// - protected SyncAttempt DeserializeItem(XElement node, SyncSerializerOptions options) - => serializer.Deserialize(node, options); + /// + /// + /// turn the xml into an item (and optionally save it to umbraco). + /// + protected SyncAttempt DeserializeItem(XElement node, SyncSerializerOptions options) + => serializer.Deserialize(node, options); - /// - /// perform a second pass on an item you are importing. - /// - protected SyncAttempt DeserializeItemSecondPass(TObject item, XElement node, SyncSerializerOptions options) - => serializer.DeserializeSecondPass(item, node, options); + /// + /// perform a second pass on an item you are importing. + /// + protected SyncAttempt DeserializeItemSecondPass(TObject item, XElement node, SyncSerializerOptions options) + => serializer.DeserializeSecondPass(item, node, options); - private SyncChangeInfo IsItemCurrent(XElement node, SyncSerializerOptions options) - { - var change = new SyncChangeInfo(); - change.CurrentNode = SerializeFromNode(node, options); - change.Change = serializer.IsCurrent(node, change.CurrentNode, options); - return change; - } - private XElement SerializeFromNode(XElement node, SyncSerializerOptions options) + private SyncChangeInfo IsItemCurrent(XElement node, SyncSerializerOptions options) + { + var change = new SyncChangeInfo(); + change.CurrentNode = SerializeFromNode(node, options); + change.Change = serializer.IsCurrent(node, change.CurrentNode, options); + return change; + } + private XElement? SerializeFromNode(XElement node, SyncSerializerOptions options) + { + var item = serializer.FindItem(node); + if (item != null) { - var item = serializer.FindItem(node); - if (item != null) + var cultures = node.GetCultures(); + if (!string.IsNullOrWhiteSpace(cultures)) { - var cultures = node.GetCultures(); - if (!string.IsNullOrWhiteSpace(cultures)) - { - // the cultures we serialize should match any in the file. - // this means we then only check the same values at each end. - options.Settings[Core.uSyncConstants.CultureKey] = cultures; - } - - var attempt = this.SerializeItem(item, options); - if (attempt.Success) return attempt.Item; + // the cultures we serialize should match any in the file. + // this means we then only check the same values at each end. + options.Settings[Core.uSyncConstants.CultureKey] = cultures; } - return null; + var attempt = this.SerializeItem(item, options); + if (attempt.Success) return attempt.Item; } - private class SyncChangeInfo - { - public ChangeType Change { get; set; } - public XElement CurrentNode { get; set; } - } + return null; + } - /// - /// Find an items UDI value based on the values in the uSync XML node - /// - public Udi FindFromNode(XElement node) - { - var item = serializer.FindItem(node); - if (item != null) - return Udi.Create(this.EntityType, serializer.ItemKey(item)); + private class SyncChangeInfo + { + public ChangeType Change { get; set; } + public XElement? CurrentNode { get; set; } + } - return null; - } + /// + /// Find an items UDI value based on the values in the uSync XML node + /// + public Udi? FindFromNode(XElement node) + { + var item = serializer.FindItem(node); + if (item != null) + return Udi.Create(this.EntityType, serializer.ItemKey(item)); - /// - /// Calculate the current status of an item compared to the XML in a potential import - /// - public ChangeType GetItemStatus(XElement node) - { - var serializerOptions = new SyncSerializerOptions(SerializerFlags.None, this.DefaultConfig.Settings); - return this.IsItemCurrent(node, serializerOptions).Change; - } + return null; + } - #endregion + /// + /// Calculate the current status of an item compared to the XML in a potential import + /// + public ChangeType GetItemStatus(XElement node) + { + var serializerOptions = new SyncSerializerOptions(SerializerFlags.None, this.DefaultConfig.Settings); + return this.IsItemCurrent(node, serializerOptions).Change; + } - private string GetNameFromFileOrNode(string filename, XElement node) - { - if (string.IsNullOrWhiteSpace(filename) is true) return node.GetAlias(); - return syncFileService.GetSiteRelativePath(filename); - } + #endregion + private string GetNameFromFileOrNode(string filename, XElement node) + { + if (string.IsNullOrWhiteSpace(filename) is true) return node.GetAlias(); + return syncFileService.GetSiteRelativePath(filename); + } - /// - /// get thekey for any caches we might call (thread based cache value) - /// - /// - protected string GetCacheKeyBase() - => $"keycache_{this.Alias}_{Thread.CurrentThread.ManagedThreadId}"; - private string PrepCaches() - { - if (this.serializer is ISyncCachedSerializer cachedSerializer) - cachedSerializer.InitializeCache(); + /// + /// get thekey for any caches we might call (thread based cache value) + /// + /// + protected string GetCacheKeyBase() + => $"keycache_{this.Alias}_{Thread.CurrentThread.ManagedThreadId}"; - // make sure the runtime cache is clean. - var key = GetCacheKeyBase(); + private string PrepCaches() + { + if (this.serializer is ISyncCachedSerializer cachedSerializer) + cachedSerializer.InitializeCache(); - // this also clears the folder cache - as its a starts with call. - runtimeCache.ClearByKey(key); - return key; - } + // make sure the runtime cache is clean. + var key = GetCacheKeyBase(); - private void CleanCaches(string cacheKey) - { - runtimeCache.ClearByKey(cacheKey); + // this also clears the folder cache - as its a starts with call. + runtimeCache.ClearByKey(key); + return key; + } - if (this.serializer is ISyncCachedSerializer cachedSerializer) - cachedSerializer.DisposeCache(); + private void CleanCaches(string cacheKey) + { + runtimeCache.ClearByKey(cacheKey); - } + if (this.serializer is ISyncCachedSerializer cachedSerializer) + cachedSerializer.DisposeCache(); - #region roots notifications + } - /// - /// check roots isn't blocking the save - /// - public virtual void Handle(SavingNotification notification) - { - if (ShouldBlockRootChanges(notification.SavedEntities)) - { - notification.Cancel = true; - notification.Messages.Add(GetCancelMessageForRoots()); - } - } + #region roots notifications - /// - /// check roots isn't blocking the move - /// - public virtual void Handle(MovingNotification notification) + /// + /// check roots isn't blocking the save + /// + public virtual void Handle(SavingNotification notification) + { + if (ShouldBlockRootChanges(notification.SavedEntities)) { - if (ShouldBlockRootChanges(notification.MoveInfoCollection.Select(x => x.Entity))) - { - notification.Cancel = true; - notification.Messages.Add(GetCancelMessageForRoots()); - } + notification.Cancel = true; + notification.Messages.Add(GetCancelMessageForRoots()); } + } - /// - /// check roots isn't blocking the delete - /// - public virtual void Handle(DeletingNotification notification) + /// + /// check roots isn't blocking the move + /// + public virtual void Handle(MovingNotification notification) + { + if (ShouldBlockRootChanges(notification.MoveInfoCollection.Select(x => x.Entity))) { - if (ShouldBlockRootChanges(notification.DeletedEntities)) - { - notification.Cancel = true; - notification.Messages.Add(GetCancelMessageForRoots()); - } + notification.Cancel = true; + notification.Messages.Add(GetCancelMessageForRoots()); } + } - /// - /// should we block this event based on the existance or root objects. - /// - protected bool ShouldBlockRootChanges(IEnumerable items) + /// + /// check roots isn't blocking the delete + /// + public virtual void Handle(DeletingNotification notification) + { + if (ShouldBlockRootChanges(notification.DeletedEntities)) { - if (!ShouldProcessEvent()) return false; + notification.Cancel = true; + notification.Messages.Add(GetCancelMessageForRoots()); + } + } - if (uSyncConfig.Settings.LockRoot == false) return false; + /// + /// should we block this event based on the existance or root objects. + /// + protected bool ShouldBlockRootChanges(IEnumerable items) + { + if (!ShouldProcessEvent()) return false; - if (!HasRootFolders()) return false; + if (uSyncConfig.Settings.LockRoot == false) return false; - foreach (var item in items) - { - if (RootItemExists(item)) - return true; - } + if (!HasRootFolders()) return false; - return false; + foreach (var item in items) + { + if (RootItemExists(item)) + return true; } - /// - /// get the message we use for cancellations - /// - protected EventMessage GetCancelMessageForRoots() - => new EventMessage("Blocked", "You cannot make this change, root level items are locked", EventMessageType.Error); + return false; + } + + /// + /// get the message we use for cancellations + /// + protected EventMessage GetCancelMessageForRoots() + => new EventMessage("Blocked", "You cannot make this change, root level items are locked", EventMessageType.Error); - private bool HasRootFolders() - => syncFileService.AnyFolderExists(uSyncConfig.GetFolders()[..^1]); + private bool HasRootFolders() + => syncFileService.AnyFolderExists(uSyncConfig.GetFolders()[..^1]); - private bool RootItemExists(TObject item) + private bool RootItemExists(TObject item) + { + foreach (var folder in uSyncConfig.GetFolders()[..^1]) { - foreach (var folder in uSyncConfig.GetFolders()[..^1]) - { - var filename = GetPath( - Path.Combine(folder, DefaultFolder), - item, - DefaultConfig.GuidNames, - DefaultConfig.UseFlatStructure) - .ToAppSafeFileName(); - - if (syncFileService.FileExists(filename)) - return true; + var filename = GetPath( + Path.Combine(folder, DefaultFolder), + item, + DefaultConfig.GuidNames, + DefaultConfig.UseFlatStructure) + .ToAppSafeFileName(); - } + if (syncFileService.FileExists(filename)) + return true; - return false; } - #endregion + return false; } + + #endregion } \ No newline at end of file diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerTreeBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerTreeBase.cs index 1c6dae8f..504bfd4c 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerTreeBase.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerTreeBase.cs @@ -1,8 +1,8 @@  -using Microsoft.Extensions.Logging; - using System.Xml.Linq; +using Microsoft.Extensions.Logging; + using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -12,46 +12,44 @@ using uSync.BackOffice.Services; using uSync.Core; -namespace uSync.BackOffice.SyncHandlers +namespace uSync.BackOffice.SyncHandlers; + +/// +/// handlers that have a tree +/// +/// for flat processing these need to preload all the files, to workout what order +/// they go in, but that is ok because all treeSerializers store the level in the +/// top attribute. +/// +public abstract class SyncHandlerTreeBase : SyncHandlerLevelBase + where TObject : ITreeEntity + where TService : IService { - /// - /// handlers that have a tree - /// - /// for flat processing these need to preload all the files, to workout what order - /// they go in, but that is ok because all treeSerializers store the level in the - /// top attribute. - /// - public abstract class SyncHandlerTreeBase : SyncHandlerLevelBase - where TObject : ITreeEntity - where TService : IService + /// + protected SyncHandlerTreeBase( + ILogger> logger, + IEntityService entityService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + SyncFileService syncFileService, + uSyncEventService mutexService, + uSyncConfigService uSyncConfig, + ISyncItemFactory syncItemFactory) + : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) + { } + + /// + protected override string GetItemName(TObject item) => item.Name ?? item.Id.ToString(); + + /// + protected override bool DoItemsMatch(XElement node, TObject item) { - /// - protected SyncHandlerTreeBase( - ILogger> logger, - IEntityService entityService, - AppCaches appCaches, - IShortStringHelper shortStringHelper, - SyncFileService syncFileService, - uSyncEventService mutexService, - uSyncConfigService uSyncConfig, - ISyncItemFactory syncItemFactory) - : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfig, syncItemFactory) - { } - - /// - protected override string GetItemName(TObject item) => item.Name; - - /// - protected override bool DoItemsMatch(XElement node, TObject item) - { - if (item.Key == node.GetKey()) return true; - - // in a tree items can have the same alias in different places. - // so we only do this match on key. - return false; - - - } - } + if (item.Key == node.GetKey()) return true; + + // in a tree items can have the same alias in different places. + // so we only do this match on key. + return false; + + } } diff --git a/uSync.BackOffice/WebhookEvents/uSyncItemImportedWebhookEvent.cs b/uSync.BackOffice/WebhookEvents/uSyncItemImportedWebhookEvent.cs index f413d616..68241f65 100644 --- a/uSync.BackOffice/WebhookEvents/uSyncItemImportedWebhookEvent.cs +++ b/uSync.BackOffice/WebhookEvents/uSyncItemImportedWebhookEvent.cs @@ -11,7 +11,7 @@ namespace uSync.BackOffice.WebhookEvents; /// webhook event for when a single item has been imported /// [WebhookEvent("uSync item imported")] -public class uSyncItemImportedWebhookEvent : +public class uSyncItemImportedWebhookEvent : WebhookEventBase { /// @@ -19,7 +19,7 @@ public uSyncItemImportedWebhookEvent( IWebhookFiringService webhookFiringService, IWebhookService webhookService, IOptionsMonitor webhookSettings, - IServerRoleAccessor serverRoleAccessor) + IServerRoleAccessor serverRoleAccessor) : base(webhookFiringService, webhookService, webhookSettings, serverRoleAccessor) { } diff --git a/uSync.BackOffice/packages.lock.json b/uSync.BackOffice/packages.lock.json index 13dd8a4c..f67f2463 100644 --- a/uSync.BackOffice/packages.lock.json +++ b/uSync.BackOffice/packages.lock.json @@ -296,30 +296,19 @@ }, "Microsoft.AspNetCore.Http.Features": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "6sVnhFwtsjEVL09FsYpAttQ3Og6Jxg1dQFLF9XQUThi1myq64imjhj1swd92TXMLCp5wmt8szDixZXXdx64qhg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "5.0.0", - "System.IO.Pipelines": "5.0.0" - } - }, - "Microsoft.AspNetCore.JsonPatch": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "Zq13zrOOnDs6PZRlu3sXVEZ1QGbJj7Fw48UtC/ZYIWZ18T8Jkjo7OodzYXSaJgDAXAtDoakvo83N8Mjx7EI9Gg==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "YWNvdHGCHGWKILgEzUDe6soozYnknlSB3IY092zxjdgLoaCPRte2lnbRRS7Nt0lEFbsFjN/Eo2fCI5yusPK0iQ==", + "resolved": "1.0.2", + "contentHash": "9l/Y/CO3q8tET3w+dDiByREH8lRtpd14cMevwMV5nw2a/avJ5qcE3VVIE5U5hesec2phTT6udQEgwjHmdRRbig==", "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "8.0.1", - "Newtonsoft.Json": "13.0.3", - "Newtonsoft.Json.Bson": "1.0.2" + "Microsoft.Extensions.Primitives": "1.0.1", + "System.Collections": "4.0.11", + "System.ComponentModel": "4.0.1", + "System.Linq": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Net.WebSockets": "4.0.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Claims": "4.0.1", + "System.Security.Cryptography.X509Certificates": "4.1.0", + "System.Security.Principal": "4.0.1" } }, "Microsoft.AspNetCore.Mvc.Razor.Extensions": { @@ -385,11 +374,6 @@ "Microsoft.CodeAnalysis.Common": "4.0.0" } }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -509,15 +493,6 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, - "Microsoft.Extensions.FileProviders.Composite": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "0IoXXfkgKpYJB1t2lC0jPXAxuaywRNc9y2Mq96ZZNKBthL38vusa2UK73+Bm6Kq/9a5xNHJS6NhsSN+i5TEtkA==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "5.0.0", - "Microsoft.Extensions.Primitives": "5.0.0" - } - }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Transitive", "resolved": "8.0.1", @@ -805,14 +780,6 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "Newtonsoft.Json.Bson": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", - "dependencies": { - "Newtonsoft.Json": "12.0.1" - } - }, "NPoco": { "type": "Transitive", "resolved": "5.7.1", @@ -822,11 +789,6 @@ "System.Reflection.Emit.Lightweight": "4.7.0" } }, - "NUglify": { - "type": "Transitive", - "resolved": "1.20.2", - "contentHash": "vz/SjCdpxr0Jp09VzMeezid7rwbXimik2QO1dzxzDcN3bXGJloDGDVh0zoD6DA23y6yrRzxv1ZKJ3kKzV3rqyA==" - }, "OpenIddict.Abstractions": { "type": "Transitive", "resolved": "4.10.1", @@ -1078,47 +1040,6 @@ "Serilog": "2.8.0" } }, - "Smidge": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "AnRsxwg4Av7jxa0MkQMbLqdIrWbVZRVQ0KfnO4Mh19Old7lay179QvBnaOPFxAEWnIl4jHiZW8izesJp6TknVw==", - "dependencies": { - "Smidge.Core": "4.3.0" - } - }, - "Smidge.Core": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "B6m6uGpJrOKaJ68eE9clAzZUcURszTNHfoYa4razb3KUJtRXB5fmZvts8+0ffT0/tO09Vu2O/KFfiSZMp6X8Jw==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "5.0.0", - "Microsoft.Extensions.Configuration": "5.0.0", - "Microsoft.Extensions.Configuration.Json": "5.0.0", - "Microsoft.Extensions.FileProviders.Composite": "5.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "5.0.0", - "Microsoft.Extensions.Logging.Abstractions": "5.0.0", - "Microsoft.Extensions.Options": "5.0.0" - } - }, - "Smidge.InMemory": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fKyR6ICS0YoQLX0D4dIIYTwQEM1IZb8ChYhqLGpVyJ7GiOAawsXt4ZcVnH0XT+ggan2+JzQlLiXGcCdXnb16Xg==", - "dependencies": { - "Dazinator.Extensions.FileProviders": "2.0.0", - "Smidge.Core": "4.3.0", - "System.Text.Encodings.Web": "5.0.1" - } - }, - "Smidge.Nuglify": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kx5Ulh+o5zLI0Al0POs0nYPldUArErmrAxxccrrxl77MWWrDM3KS5IRWuKDtC42/sZKSzapmJIOwJ8r/1foMCg==", - "dependencies": { - "Nuglify": "1.20.2", - "Smidge": "4.3.0" - } - }, "System.AppContext": { "type": "Transitive", "resolved": "4.3.0", @@ -1395,11 +1316,6 @@ "System.Runtime": "4.3.0" } }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q==" - }, "System.Linq": { "type": "Transitive", "resolved": "4.3.0", @@ -1498,6 +1414,17 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.Net.WebSockets": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2KJo8hir6Edi9jnMDAMhiJoI691xRBmKcbNpwjrvpIMOCTYOtBpSsSEGBxBDV7PKbasJNaFp1+PZz1D7xS41Hg==", + "dependencies": { + "Microsoft.Win32.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, "System.ObjectModel": { "type": "Transitive", "resolved": "4.3.0", @@ -1689,6 +1616,20 @@ "System.Security.Principal.Windows": "5.0.0" } }, + "System.Security.Claims": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, "System.Security.Cryptography.Algorithms": { "type": "Transitive", "resolved": "4.3.0", @@ -1849,6 +1790,14 @@ "System.Security.Cryptography.Pkcs": "8.0.0" } }, + "System.Security.Principal": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, "System.Security.Principal.Windows": { "type": "Transitive", "resolved": "5.0.0", @@ -2007,9 +1956,10 @@ }, "Umbraco.Cms.Core": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "jNV8u0ZekZsmIuStkwLrhMxztT7eHhCHwyOpMMbNOdzCbqBQUuOb3MDcz4RUXJwS/BlXqfoTu4l7Pxdz7oDj/g==", + "resolved": "14.0.0-beta001", + "contentHash": "W+GVz5RhAntIV40cQ6xifO/DWvfP1PopHoomqwgrI214aMajIWTEc64i+Lkd+lpdDCmk2bnDiyIt/sx7mI03IQ==", "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Caching.Memory": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Embedded": "8.0.1", @@ -2024,18 +1974,18 @@ }, "Umbraco.Cms.Examine.Lucene": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "VkctBavadScmuSwgzj8hxKJKhA7giJnpmQyiV9jxVvF9SW375nkjTtQk3itEZcotjY9AJJJkbGfZjiKbhlgNvg==", + "resolved": "14.0.0-beta001", + "contentHash": "DDNsvIMKH3w41Ml8ZFHNq3eGbx51jo9rrfpEfZ2Zak/qhtCdf8CotV4Ic13XHvxyx4reIseekEUuL1nVnXhbcg==", "dependencies": { "Examine": "3.2.0", "System.Security.Cryptography.Xml": "8.0.0", - "Umbraco.Cms.Infrastructure": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Infrastructure": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.Cms.Infrastructure": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "zB+wjCSoMCN8jKUva1yoc19oeDpAhMNhjMLo98L0Agb+lJsMJNHyxHf95i97v4E654UfibSPsda0xw26HeJcSQ==", + "resolved": "14.0.0-beta001", + "contentHash": "uO6pwXe49pTCJoPFvS0rB2WBKXDF7mdJzX2z2jeHbwqHd9oZiNOo+mveLjwa1O/uPiSX9M+EerUpzgXzXDJlHQ==", "dependencies": { "Examine.Core": "3.2.0", "HtmlAgilityPack": "1.11.57", @@ -2049,7 +1999,6 @@ "Microsoft.Extensions.Identity.Stores": "8.0.1", "MiniProfiler.Shared": "4.3.8", "NPoco": "5.7.1", - "Newtonsoft.Json": "13.0.3", "OpenIddict.Abstractions": "4.10.1", "Serilog": "3.1.1", "Serilog.Enrichers.Process": "2.0.2", @@ -2062,57 +2011,44 @@ "Serilog.Sinks.Async": "1.5.0", "Serilog.Sinks.File": "5.0.0", "Serilog.Sinks.Map": "1.0.2", - "Umbraco.Cms.Core": "[14.0.0--preview006, 15.0.0)", + "Umbraco.Cms.Core": "[14.0.0-beta001, 15.0.0)", "ncrontab": "3.3.3" } }, "Umbraco.Cms.PublishedCache.NuCache": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "nkK6gGPysMsokfgLeir497YoVgb51qEKPxdtoaT75ENvoXoppo3i+3+WEUQjg73FcuH4lsbB+S8ncqONbPZm2A==", + "resolved": "14.0.0-beta001", + "contentHash": "r7KDkEjVxeVmgNcLmS+D5MnHmxaEhHUUIdy3gIY9c/z/sGwmmiW2WRCquprVe+xLJh7yc7YsYuLSQQ0mO72RFw==", "dependencies": { "K4os.Compression.LZ4": "1.3.6", "MessagePack": "2.5.140", "Umbraco.CSharpTest.Net.Collections": "15.0.0", - "Umbraco.Cms.Infrastructure": "[14.0.0--preview006, 15.0.0)" - } - }, - "Umbraco.Cms.Web.BackOffice": { - "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "CNliwq77WLyh5D5B31qIf3TzEIkSXWGLEscTDw39SGIkzl1FMP9+Own9zrkhelzuG5qt/Z+iyRPppb/Tq7Tirw==", - "dependencies": { - "Newtonsoft.Json": "13.0.3", - "Serilog.AspNetCore": "8.0.1", - "Umbraco.Cms.Web.Common": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Infrastructure": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.Cms.Web.Common": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "hy15XuNvXKL9VkioLG8VWIwa7g2GT27oZ6uKZiXDPObSxBaqnjlKOQxTqWqIa6zEQ4wt3avWjlMK38sO6Dq0yg==", + "resolved": "14.0.0-beta001", + "contentHash": "Q1dP0q9dP1BmfWw/m69RkM0qyAhRW8ps48fPR68LJSs/wQIukOmvWlMxamyZ4lIq9fJXfzt+q88MI5aXIRCNhg==", "dependencies": { "Asp.Versioning.Mvc": "8.0.0", "Asp.Versioning.Mvc.ApiExplorer": "8.0.0", "Dazinator.Extensions.FileProviders": "2.0.0", - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "8.0.1", "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "8.0.1", "MiniProfiler.AspNetCore.Mvc": "4.3.8", "Serilog.AspNetCore": "8.0.1", - "Smidge.InMemory": "4.3.0", - "Smidge.Nuglify": "4.3.0", "System.Net.Http": "4.3.4", "System.Text.RegularExpressions": "4.3.1", - "Umbraco.Cms.Examine.Lucene": "[14.0.0--preview006, 15.0.0)", - "Umbraco.Cms.PublishedCache.NuCache": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Examine.Lucene": "[14.0.0-beta001, 15.0.0)", + "Umbraco.Cms.PublishedCache.NuCache": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.Cms.Web.Website": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "5GRN6PFBrLDRJWUl3T6EvsWBZmkTNxiEkaMfYhFTgdLrsKCcwOvIy1GUAS+mnA4mUeawLj9iFTlkLRRTfneLHQ==", + "resolved": "14.0.0-beta001", + "contentHash": "tP4eueXTCDtdl0DqMNMaXFSx92IFsrndHPLdnxlJc91LVEnr52Q9bhb1gqzsQJx1Lx3Z28lXJZbfk94PBT9gSg==", "dependencies": { - "Umbraco.Cms.Web.Common": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Web.Common": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.CSharpTest.Net.Collections": { @@ -2123,14 +2059,13 @@ "usync.community.contrib": { "type": "Project", "dependencies": { - "uSync.Core": "[13.0.0, )" + "uSync.Core": "[14.0.0, )" } }, "usync.core": { "type": "Project", "dependencies": { - "Umbraco.Cms.Web.BackOffice": "[14.0.0--preview006, )", - "Umbraco.Cms.Web.Website": "[14.0.0--preview006, )" + "Umbraco.Cms.Web.Website": "[14.0.0-beta001, )" } } } diff --git a/uSync.BackOffice/uSyncAction.cs b/uSync.BackOffice/uSyncAction.cs index 114e302f..58378bca 100644 --- a/uSync.BackOffice/uSyncAction.cs +++ b/uSync.BackOffice/uSyncAction.cs @@ -6,184 +6,176 @@ using uSync.Core; using uSync.Core.Models; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// A uSyncAction details what just happed when an Handler did something to an item +/// +public struct uSyncAction { /// - /// A uSyncAction details what just happed when an Handler did something to an item + /// Alias of the handler /// - public struct uSyncAction - { - /// - /// Alias of the handler - /// - public string HandlerAlias { get; set; } - - /// - /// Was the action a success - /// - public bool Success { get; set; } - - /// - /// Type name of the item the action was performed on - /// - public string ItemType { get; set; } - - /// - /// message to display along with action - /// - public string Message { get; set; } - - /// - /// exception encountered during action - /// - public Exception Exception { get; set; } - - /// - /// type of change performed - /// - [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] - public ChangeType Change { get; set; } - - /// - /// path name for the uSync file - /// - public string FileName { get; set; } - - /// - /// display name of the item - /// - public string Name { get; set; } - - /// - /// 9.2 a nice path for the thing (displayed). - /// - public string Path { get; set; } - - /// - /// this action still requires some processing - /// - public bool RequiresPostProcessing { get; set; } - - /// - /// boxed item - used on updates. - /// - [Newtonsoft.Json.JsonIgnore] - [System.Text.Json.Serialization.JsonIgnore] - public object Item { get; set; } - - /// - /// text that is shown on the details screen above any details. - /// - public string DetailMessage { get; set; } - - /// - /// list of detailed changes, so you can see what is changing. - /// - public IEnumerable Details { get; set; } - - /// - /// the GUID key value of the item - /// - public Guid key { get; set; } - - internal uSyncAction(bool success, string name, string type, ChangeType change, string message, Exception ex, string filename, string handlerAlias, bool postProcess = false) : this() - { - Success = success; - Name = name; - ItemType = type; - Message = message; - Change = change; - Exception = ex; - FileName = filename; - RequiresPostProcessing = postProcess; - - HandlerAlias = handlerAlias; - key = Guid.Empty; + public string? HandlerAlias { get; set; } - } + /// + /// Was the action a success + /// + public bool Success { get; set; } - internal uSyncAction(bool success, string name, string type, ChangeType change, string message, Exception ex, string filename, bool postProcess = false) - : this(success, name, type, change, message, ex, filename, null, postProcess) - { } - - /// - /// Create a uSync action with the supplied details. - /// - public static uSyncAction SetAction( - bool success, - string name, - string type = "", - ChangeType change = ChangeType.NoChange, - string message = null, - Exception ex = null, - string filename = null) - { - return new uSyncAction(success, name, type, change, message, ex, filename); - } + /// + /// Type name of the item the action was performed on + /// + public string ItemType { get; set; } - /// - /// create a fail uSyncAction object - /// - [Obsolete("Pass handler type with fail - Will remove in v13")] - public static uSyncAction Fail(string name, string type, ChangeType change, string message, Exception ex) - => new uSyncAction(false, name, type, change, message, ex, string.Empty); + /// + /// message to display along with action + /// + public string? Message { get; set; } - /// - /// create a fail uSyncAction object - /// - public static uSyncAction Fail(string name, string handlerType, string itemType, ChangeType change, string message, Exception ex) - => new uSyncAction(false, name, itemType, change, message, ex, string.Empty, handlerType); + /// + /// exception encountered during action + /// + public Exception? Exception { get; set; } + /// + /// type of change performed + /// + [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] + public ChangeType Change { get; set; } + + /// + /// path name for the uSync file + /// + public string? FileName { get; set; } + + /// + /// display name of the item + /// + public string Name { get; set; } + + /// + /// 9.2 a nice path for the thing (displayed). + /// + public string? Path { get; set; } + + /// + /// this action still requires some processing + /// + public bool RequiresPostProcessing { get; set; } + + /// + /// boxed item - used on updates. + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public object? Item { get; set; } + + /// + /// text that is shown on the details screen above any details. + /// + public string? DetailMessage { get; set; } + + /// + /// list of detailed changes, so you can see what is changing. + /// + public IEnumerable? Details { get; set; } + + /// + /// the GUID key value of the item + /// + public Guid key { get; set; } + + internal uSyncAction(bool success, string name, string type, ChangeType change, string? message, Exception? ex, string? filename, string? handlerAlias, bool postProcess = false) : this() + { + Success = success; + Name = name; + ItemType = type; + Message = message; + Change = change; + Exception = ex; + FileName = filename; + RequiresPostProcessing = postProcess; + + HandlerAlias = handlerAlias; + key = Guid.Empty; } + internal uSyncAction(bool success, string name, string type, ChangeType change, string? message, Exception? ex, string? filename, bool postProcess = false) + : this(success, name, type, change, message, ex, filename, null, postProcess) + { } + /// - /// uSync action extensions + /// Create a uSync action with the supplied details. /// - public struct uSyncActionHelper + public static uSyncAction SetAction( + bool success, + string name, + string type = "", + ChangeType change = ChangeType.NoChange, + string? message = null, + Exception? ex = null, + string? filename = null) { - /// - /// Create a new uSyncAction from a SyncAttempt - /// - public static uSyncAction SetAction(SyncAttempt attempt, string filename, Guid key, string handlerAlias, bool requirePostProcessing = true) - { - var action = new uSyncAction(attempt.Success, attempt.Name, attempt.ItemType, attempt.Change, attempt.Message, attempt.Exception, filename, handlerAlias, requirePostProcessing); - action.key = key; - if (attempt.Details != null && attempt.Details.Any()) - { - action.Details = attempt.Details; - } - return action; - } + return new uSyncAction(success, name, type, change, message, ex, filename); + } + + /// + /// create a fail uSyncAction object + /// + public static uSyncAction Fail(string name, string handlerType, string itemType, ChangeType change, string message, Exception ex) + => new uSyncAction(false, name, itemType, change, message, ex, string.Empty, handlerType); + + +} - /// - /// Create a new report action - /// - [Obsolete("Reporting with the Path gives better feedback to the user.")] - public static uSyncAction ReportAction(ChangeType changeType, string name, string file, Guid key, string handlerAlias, string message) +/// +/// uSync action extensions +/// +public struct uSyncActionHelper +{ + /// + /// Create a new uSyncAction from a SyncAttempt + /// + public static uSyncAction SetAction(SyncAttempt attempt, string filename, Guid key, string handlerAlias, bool requirePostProcessing = true) + { + var action = new uSyncAction(attempt.Success, attempt.Name, attempt.ItemType, attempt.Change, attempt.Message, attempt.Exception, filename, handlerAlias, requirePostProcessing); + action.key = key; + if (attempt.Details != null && attempt.Details.Any()) { - return new uSyncAction(true, name, typeof(T).Name, changeType, message, null, file, handlerAlias) - { - key = key - }; + action.Details = attempt.Details; } + return action; + } - /// - /// Create a new report action - /// - public static uSyncAction ReportAction(ChangeType changeType, string name, string path, string file, Guid key, string handlerAlias, string message) + /// + /// Create a new report action + /// + [Obsolete("Reporting with the Path gives better feedback to the user.")] + public static uSyncAction ReportAction(ChangeType changeType, string name, string file, Guid key, string handlerAlias, string message) + { + return new uSyncAction(true, name, typeof(T).Name, changeType, message, null, file, handlerAlias) { - return new uSyncAction(true, name, typeof(T).Name, changeType, message, null, file, handlerAlias) - { - key = key, - Path = path - }; - } + key = key + }; + } - /// - /// Create a new failed report action - /// - public static uSyncAction ReportActionFail(string name, string message) - => new uSyncAction(false, name, typeof(T).Name, ChangeType.Fail, message, null, string.Empty); + /// + /// Create a new report action + /// + public static uSyncAction ReportAction(ChangeType changeType, string name, string path, string file, Guid key, string handlerAlias, string message) + { + return new uSyncAction(true, name, typeof(T).Name, changeType, message, null, file, handlerAlias) + { + key = key, + Path = path + }; } + + /// + /// Create a new failed report action + /// + public static uSyncAction ReportActionFail(string name, string message) + => new uSyncAction(false, name, typeof(T).Name, ChangeType.Fail, message, null, string.Empty); } diff --git a/uSync.BackOffice/uSyncBackOffice.cs b/uSync.BackOffice/uSyncBackOffice.cs index 78ddddb2..970573ff 100644 --- a/uSync.BackOffice/uSyncBackOffice.cs +++ b/uSync.BackOffice/uSyncBackOffice.cs @@ -1,63 +1,62 @@ -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// global values for uSync +/// +public class uSync { /// - /// global values for uSync + /// name of the app + /// + internal const string Name = "uSync"; + + /// + /// a key we set on notifications, so you can tell if uSync processed them, + /// + public const string EventStateKey = "uSync.ProcessState"; + + /// + /// a key set on a notification to say uSync was paused while processing the item. + /// + public const string EventPausedKey = "uSync.PausedKey"; + + internal class Trees + { + internal const string uSync = "usync"; + internal const string Group = "sync"; + } + + internal class Sets + { + internal const string DefaultSet = "Default"; + } + + /// + /// configuration defaults /// - public class uSync + public class Configuration { + private const string uSyncConfigPrefix = "uSync:"; + /// - /// name of the app + /// where the configuration settings live /// - internal const string Name = "uSync"; + public static string ConfigSettings = uSyncConfigPrefix + "Settings"; /// - /// a key we set on notifications, so you can tell if uSync processed them, + /// path to the root of the default configuration sets /// - public const string EventStateKey = "uSync.ProcessState"; + public static string uSyncSetsConfig = uSyncConfigPrefix + "Sets"; /// - /// a key set on a notification to say uSync was paused while processing the item. + /// prefix used for sets in the config /// - public const string EventPausedKey = "uSync.PausedKey"; - - internal class Trees - { - internal const string uSync = "usync"; - internal const string Group = "sync"; - } - - internal class Sets - { - internal const string DefaultSet = "Default"; - } + public static string uSyncSetsConfigPrefix = uSyncSetsConfig + ":"; /// - /// configuration defaults + /// names option for the default set. /// - public class Configuration - { - private const string uSyncConfigPrefix = "uSync:"; - - /// - /// where the configuration settings live - /// - public static string ConfigSettings = uSyncConfigPrefix + "Settings"; - - /// - /// path to the root of the default configuration sets - /// - public static string uSyncSetsConfig = uSyncConfigPrefix + "Sets"; - - /// - /// prefix used for sets in the config - /// - public static string uSyncSetsConfigPrefix = uSyncSetsConfig + ":"; - - /// - /// names option for the default set. - /// - public static string ConfigDefaultSet = uSyncSetsConfigPrefix + uSync.Sets.DefaultSet; - - } + public static string ConfigDefaultSet = uSyncSetsConfigPrefix + uSync.Sets.DefaultSet; + } } diff --git a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs index c59a2821..58eb9177 100644 --- a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs +++ b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs @@ -9,250 +9,244 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Web.BackOffice.Authorization; using Umbraco.Cms.Web.Common.ApplicationBuilder; -using uSync.BackOffice.Authorization; using uSync.BackOffice.Boot; using uSync.BackOffice.Cache; using uSync.BackOffice.Configuration; -using uSync.BackOffice.Expansions; using uSync.BackOffice.Hubs; +using uSync.BackOffice.Legacy; using uSync.BackOffice.Notifications; using uSync.BackOffice.Services; using uSync.BackOffice.SyncHandlers; using uSync.BackOffice.SyncHandlers.Handlers; using uSync.Core; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// extensions to the IUmbracoBuilder object to add uSync to a site. +/// +public static class uSyncBackOfficeBuilderExtensions { /// - /// extensions to the IUmbracoBuilder object to add uSync to a site. + /// Add uSync to the site. /// - public static class uSyncBackOfficeBuilderExtensions + public static IUmbracoBuilder AdduSync(this IUmbracoBuilder builder, Action? defaultOptions = null) { - /// - /// Add uSync to the site. - /// - public static IUmbracoBuilder AdduSync(this IUmbracoBuilder builder, Action defaultOptions = default) - { - // if the uSyncConfig Service is registred then we assume this has been added before so we don't do it again. - if (builder.Services.FirstOrDefault(x => x.ServiceType == typeof(uSyncConfigService)) != null) - return builder; + // if the uSyncConfig Service is registered then we assume this has been added before so we don't do it again. + if (builder.Services.FirstOrDefault(x => x.ServiceType == typeof(uSyncConfigService)) != null) + return builder; - // load up the settings. - var options = builder.Services.AddOptions() - .Bind(builder.Config.GetSection(uSync.Configuration.ConfigSettings)); + // load up the settings. + var options = builder.Services.AddOptions() + .Bind(builder.Config.GetSection(uSync.Configuration.ConfigSettings)); - if (defaultOptions != default) - { - options.Configure(defaultOptions); - } - options.ValidateDataAnnotations(); + if (defaultOptions != default) + { + options.Configure(defaultOptions); + } + options.ValidateDataAnnotations(); - // default handler options, other people can load their own names handler options and - // they can be used throughout uSync (so complete will do this). - var handlerOptiosn = builder.Services.Configure(uSync.Sets.DefaultSet, - builder.Config.GetSection(uSync.Configuration.ConfigDefaultSet)); + // default handler options, other people can load their own names handler options and + // they can be used throughout uSync (so complete will do this). + var handlerOptions = builder.Services.Configure(uSync.Sets.DefaultSet, + builder.Config.GetSection(uSync.Configuration.ConfigDefaultSet)); - // Setup uSync core. - builder.AdduSyncCore(); + // Setup uSync core. + builder.AdduSyncCore(); - // Setup the back office. - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // Setup the back office. + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.WithCollectionBuilder() - .Add(() => builder.TypeLoader.GetTypes()); + builder.WithCollectionBuilder() + .Add(() => builder.TypeLoader.GetTypes()); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - // first boot should happen before any other bits of uSync export on a blank site. - builder.AdduSyncFirstBoot(); + // first boot should happen before any other bits of uSync export on a blank site. + builder.AdduSyncFirstBoot(); - // register for the notifications - builder.AddNotificationHandler(); - builder.AddHandlerNotifications(); + // register for the notifications + builder.AddNotificationHandler(); + builder.AddHandlerNotifications(); - builder.Services.AddSingleton(); - builder.Services.AddSignalR(); - builder.Services.AdduSyncSignalR(); + builder.Services.AddSingleton(); + builder.Services.AddSignalR(); + builder.Services.AdduSyncSignalR(); - builder.Services.AddAuthorization(o => CreatePolicies(o)); - builder.WithCollectionBuilder() - .Add(() => builder.TypeLoader.GetTypes()); + builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddAuthorization(o => CreatePolicies(o)); - _ = builder.Services.PostConfigure(options => + builder.Services.AddTransient(); + + _ = builder.Services.PostConfigure(options => + { + if (options.Folders == null || options.Folders.Length == 0) { - if (options.Folders == null || options.Folders.Length == 0) - { - options.Folders = ["uSync/Root/", "uSync/v14/"]; - } - }); + options.Folders = ["uSync/Root/", "uSync/v14/"]; + } + }); - return builder; - } + return builder; + } - /// - /// Adds the signalR hub route for uSync - /// - public static IServiceCollection AdduSyncSignalR(this IServiceCollection services) - { + /// + /// Adds the signalR hub route for uSync + /// + public static IServiceCollection AdduSyncSignalR(this IServiceCollection services) + { - services.Configure(options => - { - options.AddFilter(new UmbracoPipelineFilter( - "uSync", - applicationBuilder => { }, - applicationBuilder => { }, - applicationBuilder => + services.Configure(options => + { + options.AddFilter(new UmbracoPipelineFilter( + "uSync", + applicationBuilder => { }, + applicationBuilder => { }, + applicationBuilder => + { + applicationBuilder.UseEndpoints(e => { - applicationBuilder.UseEndpoints(e => - { - var hubRoutes = applicationBuilder.ApplicationServices.GetRequiredService(); - hubRoutes.CreateRoutes(e); - }); - } - )); - }); - - return services; - } + var hubRoutes = applicationBuilder.ApplicationServices.GetRequiredService(); + hubRoutes.CreateRoutes(e); + }); + } + )); + }); - internal static void AddHandlerNotifications(this IUmbracoBuilder builder) - { + return services; + } - // TODO: Would be nice if we could just register all the notifications in the handlers + internal static void AddHandlerNotifications(this IUmbracoBuilder builder) + { - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - // roots - pre-notifications for stopping things - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler() - - .AddNotificationHandler() - .AddNotificationHandler(); - - - // content ones - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - - // cache lifecycle manager - builder. - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(). - AddNotificationHandler(); - } + // TODO: Would be nice if we could just register all the notifications in the handlers + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + //builder.AddNotificationHandler(); + //builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + // roots - pre-notifications for stopping things + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler() + + .AddNotificationHandler() + .AddNotificationHandler(); + + + // content ones + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + // cache lifecycle manager + builder. + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(). + AddNotificationHandler(); + } - private static void CreatePolicies(AuthorizationOptions options, - string backofficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) - { - options.AddPolicy(SyncAuthorizationPolicies.TreeAccessuSync, policy => - { - policy.AuthenticationSchemes.Add(backofficeAuthenticationScheme); - policy.Requirements.Add(new TreeRequirement(uSync.Trees.uSync)); - }); - } + + private static void CreatePolicies(AuthorizationOptions options, + string backofficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) + { + //options.AddPolicy(SyncAuthorizationPolicies.TreeAccessuSync, policy => + //{ + // policy.AuthenticationSchemes.Add(backofficeAuthenticationScheme); + // policy.Requirements.Add(new TreeRequirement(uSync.Trees.uSync)); + //}); } } diff --git a/uSync.BackOffice/uSyncBackOfficeComposer.cs b/uSync.BackOffice/uSyncBackOfficeComposer.cs index 9c263585..f661170d 100644 --- a/uSync.BackOffice/uSyncBackOfficeComposer.cs +++ b/uSync.BackOffice/uSyncBackOfficeComposer.cs @@ -2,21 +2,20 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// default composer to startup uSync +/// +public class uSyncBackOfficeComposer : IComposer { - /// - /// default composer to startup uSync - /// - public class uSyncBackOfficeComposer : IComposer + /// + public void Compose(IUmbracoBuilder builder) { - /// - public void Compose(IUmbracoBuilder builder) - { - // the composers add uSync, but the extension methods - // will only add the values if uSync hasn't already - // been added, so you can for example add uSync to your - // startup.cs file. and then the composers don't fire - builder.AdduSync(); - } + // the composers add uSync, but the extension methods + // will only add the values if uSync hasn't already + // been added, so you can for example add uSync to your + // startup.cs file. and then the composers don't fire + builder.AdduSync(); } } diff --git a/uSync.BackOffice/uSyncImportPause.cs b/uSync.BackOffice/uSyncImportPause.cs index bb4263e6..0e2d40dd 100644 --- a/uSync.BackOffice/uSyncImportPause.cs +++ b/uSync.BackOffice/uSyncImportPause.cs @@ -2,45 +2,44 @@ using uSync.BackOffice.Services; -namespace uSync.BackOffice +namespace uSync.BackOffice; + +/// +/// wraps code running that might trigger events, pausing uSyncs capture of those events. +/// +public class uSyncImportPause : IDisposable { + private readonly uSyncEventService _mutexService; + /// - /// wraps code running that might trigger events, pausing uSyncs capture of those events. + /// Generate a pause object /// - public class uSyncImportPause : IDisposable + public uSyncImportPause(uSyncEventService mutexService) { - private readonly uSyncEventService _mutexService; + _mutexService = mutexService; + _mutexService.Pause(); + } - /// - /// Generate a pause object - /// - public uSyncImportPause(uSyncEventService mutexService) - { - _mutexService = mutexService; + /// + /// generate a pause object, but only pause if told to do so. + /// + public uSyncImportPause(uSyncEventService mutexService, bool pause) + { + _mutexService = mutexService; + if (pause) _mutexService.Pause(); - } - - /// - /// generate a pause object, but only pause if told to do so. - /// - public uSyncImportPause(uSyncEventService mutexService, bool pause) - { - _mutexService = mutexService; - if (pause) - _mutexService.Pause(); - } + } - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - /// - protected virtual void Dispose(bool disposing) - { - _mutexService.UnPause(); - } + /// + protected virtual void Dispose(bool disposing) + { + _mutexService.UnPause(); } } diff --git a/uSync.Backoffice.Management.Api/App_Plugins/uSync/boot/NoNodes.cshtml b/uSync.Backoffice.Management.Api/App_Plugins/uSync/boot/NoNodes.cshtml index ca76058f..1b737bb3 100644 --- a/uSync.Backoffice.Management.Api/App_Plugins/uSync/boot/NoNodes.cshtml +++ b/uSync.Backoffice.Management.Api/App_Plugins/uSync/boot/NoNodes.cshtml @@ -25,7 +25,7 @@ Umbraco: No published content - + \n " }, - "MacroErrors": { - "description": "Gets or sets a value for the macro error behaviour.\n ", - "default": "Inline", - "oneOf": [ - { - "$ref": "#/definitions/MacroErrorBehaviour" - } - ] - }, "ShowDeprecatedPropertyEditors": { "type": "boolean", "description": "Gets or sets a value indicating whether deprecated property editors should be shown.\n ", @@ -176,17 +167,17 @@ "LoginBackgroundImage": { "type": "string", "description": "Gets or sets a value for the path to the login screen background image.\n ", - "default": "assets/img/login.jpg" + "default": "login/login.jpg" }, "LoginLogoImage": { "type": "string", "description": "Gets or sets a value for the path to the login screen logo image\nshown on top of the background image set in LoginBackgroundImage.\n ", - "default": "assets/img/application/umbraco_logo_blue.svg" + "default": "login/logo_dark.svg" }, "LoginLogoImageAlternative": { "type": "string", "description": "Gets or sets a value for the path to the login screen logo image when shown on top\nof a light background (e.g. in mobile resolutions).\n ", - "default": "assets/img/application/umbraco_logo_blue.svg" + "default": "login/logo_light.svg" }, "HideBackOfficeLogo": { "type": "boolean", @@ -346,22 +337,6 @@ } } }, - "MacroErrorBehaviour": { - "type": "string", - "description": "", - "x-enumNames": [ - "Inline", - "Silent", - "Throw", - "Content" - ], - "enum": [ - "Inline", - "Silent", - "Throw", - "Content" - ] - }, "ContentVersionCleanupPolicySettings": { "type": "object", "description": "Model representing the global content version cleanup policy\n ", @@ -1366,6 +1341,12 @@ ], "description": "Gets or sets a value for the maximum request length in kb.\n ", "format": "int32" + }, + "TemporaryFileLifeTime": { + "type": "string", + "description": "Gets or sets the timespan temporary files are kept, before they are removed by a background task.", + "format": "duration", + "default": "1.00:00:00" } } }, @@ -1449,6 +1430,19 @@ "type": "boolean", "description": "Gets or sets a value indicating whether to allow concurrent logins.\n ", "default": false + }, + "BackOfficeHost": { + "type": [ + "null", + "string" + ], + "description": "Gets or sets a value of the back-office host URI. Use this when running the back-office client and the Management API on different hosts. Leave empty when running both on the same host.\n ", + "format": "uri" + }, + "AuthorizeCallbackPathName": { + "type": "string", + "description": "The path to use for authorization callback. Will be appended to the BackOfficeHost.\n ", + "default": "/umbraco" } } }, @@ -1651,6 +1645,13 @@ "type": "string", "description": "Invalid HTML elements for RichText Editor.\n ", "default": "font" + }, + "CloudApiKey": { + "type": [ + "null", + "string" + ], + "description": "Cloud API Key for TinyMCE. This is required to use TinyMCE premium plugins.\n " } } }, diff --git a/uSync.Tests/uSync.Tests.csproj b/uSync.Tests/uSync.Tests.csproj index a365d4cf..669dfce7 100644 --- a/uSync.Tests/uSync.Tests.csproj +++ b/uSync.Tests/uSync.Tests.csproj @@ -2,8 +2,9 @@ net8.0 - false + disable + @@ -15,8 +16,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/uSync.Tests/umbraco-package-schema.json b/uSync.Tests/umbraco-package-schema.json new file mode 100644 index 00000000..cb015591 --- /dev/null +++ b/uSync.Tests/umbraco-package-schema.json @@ -0,0 +1,5212 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ApiLoaderProperty": { + "type": [ + "string", + "object" + ] + }, + "BlockWorkspaceHasSettingsConditionConfig": { + "properties": { + "alias": { + "const": "Umb.Condition.BlockWorkspaceHasSettings", + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "CollectionAliasConditionConfig": { + "allOf": [ + { + "$ref": "#/definitions/UmbConditionConfigBase<\"Umb.Condition.CollectionAlias\">" + }, + { + "properties": { + "match": { + "description": "The collection that this extension should be available in", + "type": "string" + } + }, + "required": [ + "match" + ], + "type": "object" + } + ] + }, + "CollectionBulkActionPermissionConditionConfig": { + "allOf": [ + { + "$ref": "#/definitions/UmbConditionConfigBase<\"Umb.Condition.CollectionBulkActionPermission\">" + }, + { + "properties": { + "match": { + "type": "object" + } + }, + "required": [ + "match" + ], + "type": "object" + } + ] + }, + "ConditionTypes": { + "anyOf": [ + { + "$ref": "#/definitions/SwitchConditionConfig" + }, + { + "$ref": "#/definitions/CollectionAliasConditionConfig" + }, + { + "$ref": "#/definitions/CollectionBulkActionPermissionConditionConfig" + }, + { + "$ref": "#/definitions/SectionAliasConditionConfig" + }, + { + "$ref": "#/definitions/WorkspaceAliasConditionConfig" + }, + { + "$ref": "#/definitions/BlockWorkspaceHasSettingsConditionConfig" + }, + { + "$ref": "#/definitions/WorkspaceEntityTypeConditionConfig" + }, + { + "$ref": "#/definitions/UserPermissionConditionConfig" + }, + { + "$ref": "#/definitions/UmbConditionConfigBase" + } + ] + }, + "ConditionsDashboardCollection": { + "description": "The conditions for when the dashboard should be available", + "properties": { + "entityType": { + "description": "The entity type that the dashboard collection should be available for", + "examples": [ + "media" + ], + "type": "string" + }, + "sections": { + "description": "An array of section aliases that the dashboard collection should be available in", + "examples": [ + "Umb.Section.Content", + "Umb.Section.Settings" + ], + "items": { + "type": "string" + }, + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "entityType", + "sections" + ], + "type": "object" + }, + "ManifestBase": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestBlockEditorCustomView": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "bockEditorCustomView", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestBundle": { + "description": "This type of extension takes a JS module and registers all exported manifests from the pointed JS file.", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "bundle", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestCollection": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaCollection" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "collection", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestCollectionAction": { + "description": "An action to perform on an entity\nFor example for content you may wish to create a new document etc", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaCollectionAction" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "collectionAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestCollectionView": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaCollectionView", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "collectionView", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestCondition": { + "description": "This type of extension takes a JS module and registers all exported manifests from the pointed JS file.", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "condition", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestDashboard": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaDashboard", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "dashboard", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestDashboardCollection": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "conditions": { + "$ref": "#/definitions/ConditionsDashboardCollection" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaDashboardCollection" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "dashboardCollection", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "conditions", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestDynamicRootOrigin": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaDynamicRootOrigin" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "dynamicRootOrigin", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestDynamicRootQueryStep": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaDynamicRootQueryStep" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "dynamicRootQueryStep", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityAction": { + "description": "An action to perform on an entity\nFor example for content you may wish to create a new document etc", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaEntityAction" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionCreateFolderKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "folderCreate", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionFolderKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionDefaultKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "default", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionDefaultKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionDeleteFolderKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "folderDelete", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionFolderKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionDeleteKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "delete", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionDeleteKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionDuplicateKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "duplicate", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionDuplicateKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionMoveKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "move", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionMoveKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionReloadTreeItemChildrenKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "reloadTreeItemChildren", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionRenameKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionRenameKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "rename", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionRenameKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionTrashKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "trash", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionTrashKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityActionUpdateFolderKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "folderUpdate", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaEntityActionFolderKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityBulkAction": { + "description": "An action to perform on multiple entities\nFor example for content you may wish to move one or more documents in bulk", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaEntityBulkAction" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "entityBulkAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntityUserPermission": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaEntityUserPermission" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "entityUserPermission", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestEntryPoint": { + "description": "This type of extension gives full control and will simply load the specified JS file\nYou could have custom logic to decide which extensions to load/register by using extensionRegistry", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "entryPoint", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestExternalLoginProvider": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaExternalLoginProvider", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "externalLoginProvider", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestGlobalContext": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "globalContext", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestGranularUserPermission": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaGranularUserPermission", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "userGranularPermission", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestHeaderApp": { + "description": "Header apps are displayed in the top right corner of the backoffice\nThe two provided header apps are the search and the user menu", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "headerApp", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestHeaderAppButtonKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "button", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaHeaderAppButtonKind", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "headerApp", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestHealthCheck": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "$ref": "#/definitions/ApiLoaderProperty", + "description": "The API to load for this health check. This should implement or extend the `UmbHealthCheckContext` interface." + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaHealthCheck" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "healthCheck", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "api", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestItemStore": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "itemStore", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestLocalization": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaLocalization" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "localization", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestMenu": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "menu", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestMenuItem": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaMenuItem", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "menuItem", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestMenuItemTreeKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "tree", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaMenuItemTreeKind", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "menuItem", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestModal": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "modal", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestPackageView": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaPackageView", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "packageView", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestPropertyAction": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forPropertyEditorUis": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaPropertyAction" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "propertyAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forPropertyEditorUis", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestPropertyActionDefaultKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forPropertyEditorUis": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "default", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaPropertyActionDefaultKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "propertyAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forPropertyEditorUis", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestPropertyEditorSchema": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaPropertyEditorSchema" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "propertyEditorSchema", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestPropertyEditorUi": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaPropertyEditorUi", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "propertyEditorUi", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestRepository": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "repository", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestSection": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/UmbConditionConfigBase_1" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaSection", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "section", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestSectionSidebarApp": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "sectionSidebarApp", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestSectionSidebarAppMenuKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "menu", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaSectionSidebarAppMenuKind", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "sectionSidebarApp", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestSectionView": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaSectionView", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "sectionView", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestStore": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "store", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestTheme": { + "description": "Theme manifest for styling the backoffice of Umbraco such as dark, high contrast etc", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "css": { + "description": "The file location of the stylesheet file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "theme", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestTinyMcePlugin": { + "description": "The manifest for a TinyMCE plugin.\nThe plugin will be loaded into the TinyMCE editor.\nA plugin can add things like buttons, menu items, context menu items, etc. through the TinyMCE API.\nA plugin can also add custom commands to the editor.\nA plugin can also modify the behavior of the editor.", + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaTinyMcePlugin" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "tinyMcePlugin", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestTree": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaTree" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "tree", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestTreeItem": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forEntityTypes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "treeItem", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forEntityTypes", + "name", + "type" + ], + "type": "object" + }, + "ManifestTreeStore": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "treeStore", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestTypes": { + "anyOf": [ + { + "$ref": "#/definitions/ManifestDashboard" + }, + { + "$ref": "#/definitions/ManifestExternalLoginProvider" + }, + { + "$ref": "#/definitions/ManifestMenuItem" + }, + { + "$ref": "#/definitions/ManifestBase" + }, + { + "$ref": "#/definitions/ManifestCondition" + }, + { + "$ref": "#/definitions/ManifestEntryPoint" + }, + { + "$ref": "#/definitions/ManifestSectionSidebarAppMenuKind" + }, + { + "$ref": "#/definitions/ManifestSectionSidebarApp" + }, + { + "$ref": "#/definitions/ManifestModal" + }, + { + "$ref": "#/definitions/ManifestTreeItem" + }, + { + "$ref": "#/definitions/ManifestTree" + }, + { + "$ref": "#/definitions/ManifestBundle" + }, + { + "$ref": "#/definitions/ManifestBlockEditorCustomView" + }, + { + "$ref": "#/definitions/ManifestCollection" + }, + { + "$ref": "#/definitions/ManifestCollectionView" + }, + { + "$ref": "#/definitions/ManifestCollectionAction" + }, + { + "$ref": "#/definitions/ManifestDashboardCollection" + }, + { + "$ref": "#/definitions/ManifestDynamicRootOrigin" + }, + { + "$ref": "#/definitions/ManifestDynamicRootQueryStep" + }, + { + "$ref": "#/definitions/ManifestEntityAction" + }, + { + "$ref": "#/definitions/ManifestEntityActionDefaultKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionDeleteKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionRenameKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionReloadTreeItemChildrenKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionDuplicateKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionMoveKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionCreateFolderKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionUpdateFolderKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionDeleteFolderKind" + }, + { + "$ref": "#/definitions/ManifestEntityActionTrashKind" + }, + { + "$ref": "#/definitions/ManifestEntityBulkAction" + }, + { + "$ref": "#/definitions/ManifestGlobalContext" + }, + { + "$ref": "#/definitions/ManifestHeaderApp" + }, + { + "$ref": "#/definitions/ManifestHeaderAppButtonKind" + }, + { + "$ref": "#/definitions/ManifestHealthCheck" + }, + { + "$ref": "#/definitions/ManifestItemStore" + }, + { + "$ref": "#/definitions/ManifestMenu" + }, + { + "$ref": "#/definitions/ManifestMenuItemTreeKind" + }, + { + "$ref": "#/definitions/ManifestPackageView" + }, + { + "$ref": "#/definitions/ManifestPropertyAction" + }, + { + "$ref": "#/definitions/ManifestPropertyActionDefaultKind" + }, + { + "$ref": "#/definitions/ManifestPropertyEditorSchema" + }, + { + "$ref": "#/definitions/ManifestPropertyEditorUi" + }, + { + "$ref": "#/definitions/ManifestRepository" + }, + { + "$ref": "#/definitions/ManifestSection" + }, + { + "$ref": "#/definitions/ManifestSectionView" + }, + { + "$ref": "#/definitions/ManifestStore" + }, + { + "$ref": "#/definitions/ManifestTheme" + }, + { + "$ref": "#/definitions/ManifestTinyMcePlugin" + }, + { + "$ref": "#/definitions/ManifestLocalization" + }, + { + "$ref": "#/definitions/ManifestTreeStore" + }, + { + "$ref": "#/definitions/ManifestUserProfileApp" + }, + { + "$ref": "#/definitions/ManifestWorkspace" + }, + { + "$ref": "#/definitions/ManifestWorkspaceAction" + }, + { + "$ref": "#/definitions/ManifestWorkspaceActionDefaultKind" + }, + { + "$ref": "#/definitions/ManifestWorkspaceActionMenuItem" + }, + { + "$ref": "#/definitions/ManifestWorkspaceContext" + }, + { + "$ref": "#/definitions/ManifestWorkspaceFooterApp" + }, + { + "$ref": "#/definitions/ManifestWorkspaceView" + }, + { + "$ref": "#/definitions/ManifestEntityUserPermission" + }, + { + "$ref": "#/definitions/ManifestGranularUserPermission" + } + ] + }, + "ManifestUserProfileApp": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaUserProfileApp", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "userProfileApp", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestWorkspace": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaWorkspace" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "type": { + "const": "workspace", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestWorkspaceAction": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaWorkspaceAction" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "workspaceAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestWorkspaceActionDefaultKind": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "const": "default", + "description": "The kind of the extension, used to group extensions together", + "type": "string" + }, + "meta": { + "$ref": "#/definitions/MetaWorkspaceActionDefaultKind" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "workspaceAction", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "kind", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestWorkspaceActionMenuItem": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "forWorkspaceActions": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define which workspace actions this menu item should be shown for.", + "examples": "[\n['Umb.WorkspaceAction.Document.Save', 'Umb.WorkspaceAction.Document.SaveAndPublish'],\n\"Umb.WorkspaceAction.Document.Save\"\n]" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaWorkspaceActionMenuItem" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "workspaceActionMenuItem", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "forWorkspaceActions", + "meta", + "name", + "type" + ], + "type": "object" + }, + "ManifestWorkspaceContext": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "workspaceContext", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestWorkspaceFooterApp": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "api": { + "description": "The file location of the api javascript file to load", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "workspaceFooterApp", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "name", + "type" + ], + "type": "object" + }, + "ManifestWorkspaceView": { + "properties": { + "alias": { + "description": "The alias of the extension, ensure it is unique", + "type": "string" + }, + "conditions": { + "description": "Set the conditions for when the extension should be loaded", + "items": { + "$ref": "#/definitions/ConditionTypes" + }, + "type": "array" + }, + "element": { + "description": "The file location of the element javascript file to load", + "type": "string" + }, + "elementName": { + "description": "The HTML web component name to use such as 'my-dashboard'\nNote it is NOT , just the element name.", + "type": "string" + }, + "js": { + "description": "The file location of the javascript file to load", + "type": "string" + }, + "kind": { + "description": "The kind of the extension, used to group extensions together", + "examples": [ + "button" + ] + }, + "meta": { + "$ref": "#/definitions/MetaManifestWithView", + "description": "This contains properties specific to the type of extension" + }, + "name": { + "description": "The friendly name of the extension", + "type": "string" + }, + "overwrites": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "Define one or more extension aliases that this extension should overwrite." + }, + "type": { + "const": "workspaceView", + "description": "The type of extension such as dashboard etc...", + "type": "string" + }, + "weight": { + "description": "Extensions such as dashboards are ordered by weight with lower numbers being first in the list", + "type": "number" + } + }, + "required": [ + "alias", + "meta", + "name", + "type" + ], + "type": "object" + }, + "MetaCollection": { + "properties": { + "repositoryAlias": { + "type": "string" + } + }, + "required": [ + "repositoryAlias" + ], + "type": "object" + }, + "MetaCollectionAction": { + "properties": { + "href": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "label" + ], + "type": "object" + }, + "MetaCollectionView": { + "properties": { + "icon": { + "description": "An icon to represent the collection view", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "label": { + "description": "The friendly name of the collection view", + "type": "string" + }, + "pathName": { + "description": "The URL pathname for this collection view that can be deep linked to by sharing the url", + "type": "string" + } + }, + "required": [ + "icon", + "label", + "pathName" + ], + "type": "object" + }, + "MetaDashboard": { + "properties": { + "label": { + "description": "The displayed name (label) in the navigation.", + "type": "string" + }, + "pathname": { + "description": "This is the URL path part for this view. This is used for navigating or deep linking directly to the dashboard\nhttps://yoursite.com/section/settings/dashboard/my-dashboard-path", + "examples": [ + "my-dashboard-path" + ], + "type": "string" + } + }, + "type": "object" + }, + "MetaDashboardCollection": { + "properties": { + "label": { + "description": "Optional string to display as the label for the dashboard collection", + "type": "string" + }, + "pathname": { + "description": "The URL path for the dashboard which is used for navigating or deep linking directly to the dashboard", + "examples": [ + "media-management-dashboard", + "my-awesome-dashboard" + ], + "type": "string" + }, + "repositoryAlias": { + "description": "The alias of the repository that the dashboard collection is for", + "examples": [ + "Umb.Repository.Media" + ], + "type": "string" + } + }, + "required": [ + "pathname", + "repositoryAlias" + ], + "type": "object" + }, + "MetaDynamicRootOrigin": { + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "label": { + "type": "string" + }, + "originAlias": { + "type": "string" + } + }, + "required": [ + "originAlias" + ], + "type": "object" + }, + "MetaDynamicRootQueryStep": { + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "label": { + "type": "string" + }, + "queryStepAlias": { + "type": "string" + } + }, + "required": [ + "queryStepAlias" + ], + "type": "object" + }, + "MetaEntityAction": { + "type": "object" + }, + "MetaEntityActionDefaultKind": { + "properties": { + "icon": { + "description": "An icon to represent the action to be performed", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + } + }, + "required": [ + "icon", + "label" + ], + "type": "object" + }, + "MetaEntityActionDeleteKind": { + "properties": { + "detailRepositoryAlias": { + "type": "string" + }, + "icon": { + "description": "An icon to represent the action to be performed", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "itemRepositoryAlias": { + "type": "string" + }, + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + } + }, + "required": [ + "detailRepositoryAlias", + "icon", + "itemRepositoryAlias", + "label" + ], + "type": "object" + }, + "MetaEntityActionDuplicateKind": { + "properties": { + "duplicateRepositoryAlias": { + "type": "string" + }, + "icon": { + "description": "An icon to represent the action to be performed", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "itemRepositoryAlias": { + "type": "string" + }, + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + }, + "pickerModal": { + "anyOf": [ + { + "$ref": "#/definitions/UmbModalToken" + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "duplicateRepositoryAlias", + "icon", + "itemRepositoryAlias", + "label", + "pickerModal" + ], + "type": "object" + }, + "MetaEntityActionFolderKind": { + "properties": { + "folderRepositoryAlias": { + "type": "string" + }, + "icon": { + "description": "An icon to represent the action to be performed", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + } + }, + "required": [ + "folderRepositoryAlias", + "icon", + "label" + ], + "type": "object" + }, + "MetaEntityActionMoveKind": { + "properties": { + "icon": { + "description": "An icon to represent the action to be performed", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "itemRepositoryAlias": { + "type": "string" + }, + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + }, + "moveRepositoryAlias": { + "type": "string" + }, + "pickerModal": { + "anyOf": [ + { + "$ref": "#/definitions/UmbModalToken" + }, + { + "type": "string" + } + ] + } + }, + "required": [ + "icon", + "itemRepositoryAlias", + "label", + "moveRepositoryAlias", + "pickerModal" + ], + "type": "object" + }, + "MetaEntityActionRenameKind": { + "properties": { + "icon": { + "description": "An icon to represent the action to be performed", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "itemRepositoryAlias": { + "type": "string" + }, + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + }, + "renameRepositoryAlias": { + "type": "string" + } + }, + "required": [ + "icon", + "itemRepositoryAlias", + "label", + "renameRepositoryAlias" + ], + "type": "object" + }, + "MetaEntityActionTrashKind": { + "properties": { + "icon": { + "description": "An icon to represent the action to be performed", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "itemRepositoryAlias": { + "type": "string" + }, + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + }, + "trashRepositoryAlias": { + "type": "string" + } + }, + "required": [ + "icon", + "itemRepositoryAlias", + "label", + "trashRepositoryAlias" + ], + "type": "object" + }, + "MetaEntityBulkAction": { + "properties": { + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + } + }, + "type": "object" + }, + "MetaEntityUserPermission": { + "properties": { + "description": { + "type": "string" + }, + "descriptionKey": { + "type": "string" + }, + "group": { + "type": "string" + }, + "label": { + "type": "string" + }, + "labelKey": { + "type": "string" + }, + "verbs": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "verbs" + ], + "type": "object" + }, + "MetaExternalLoginProvider": { + "properties": { + "label": { + "type": "string" + }, + "pathname": { + "type": "string" + } + }, + "required": [ + "label", + "pathname" + ], + "type": "object" + }, + "MetaGranularUserPermission": { + "properties": { + "description": { + "type": "string" + }, + "descriptionKey": { + "type": "string" + }, + "label": { + "type": "string" + }, + "labelKey": { + "type": "string" + }, + "schemaType": { + "type": "string" + } + }, + "required": [ + "schemaType" + ], + "type": "object" + }, + "MetaHeaderAppButtonKind": { + "properties": { + "href": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "href", + "icon", + "label" + ], + "type": "object" + }, + "MetaHealthCheck": { + "properties": { + "label": { + "type": "string" + } + }, + "required": [ + "label" + ], + "type": "object" + }, + "MetaLocalization": { + "properties": { + "culture": { + "description": "The culture is a combination of a language and a country. The language is represented by an ISO 639-1 code and the country is represented by an ISO 3166-1 alpha-2 code.\nThe language and country are separated by a dash.\nThe value is used to describe the language of the translations according to the extension system\nand it will be set as the `lang` attribute on the `` element.", + "examples": [ + "en-us", + "en-gb", + "da-dk" + ], + "type": "string" + }, + "direction": { + "default": "ltr", + "description": "The value is used to describe the direction of the translations according to the extension system\nand it will be set as the `dir` attribute on the `` element. It defaults to `ltr`.", + "enum": [ + "ltr", + "rtl" + ], + "examples": [ + "ltr" + ], + "type": "string" + }, + "localizations": { + "$ref": "#/definitions/UmbLocalizationDictionary", + "description": "The localizations." + } + }, + "required": [ + "culture" + ], + "type": "object" + }, + "MetaManifestWithView": { + "properties": { + "icon": { + "type": "string" + }, + "label": { + "type": "string" + }, + "pathname": { + "type": "string" + } + }, + "required": [ + "icon", + "label", + "pathname" + ], + "type": "object" + }, + "MetaMenuItem": { + "properties": { + "entityType": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "label": { + "type": "string" + }, + "menus": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "label", + "menus" + ], + "type": "object" + }, + "MetaMenuItemTreeKind": { + "properties": { + "entityType": { + "type": "string" + }, + "hideTreeRoot": { + "type": "boolean" + }, + "icon": { + "type": "string" + }, + "label": { + "type": "string" + }, + "menus": { + "items": { + "type": "string" + }, + "type": "array" + }, + "treeAlias": { + "type": "string" + } + }, + "required": [ + "label", + "menus", + "treeAlias" + ], + "type": "object" + }, + "MetaPackageView": { + "properties": { + "packageName": { + "type": "string" + } + }, + "required": [ + "packageName" + ], + "type": "object" + }, + "MetaPropertyAction": { + "type": "object" + }, + "MetaPropertyActionDefaultKind": { + "properties": { + "icon": { + "description": "An icon to represent the action to be performed", + "examples": [ + "icon-box", + "icon-grid" + ], + "type": "string" + }, + "label": { + "description": "The friendly name of the action to perform", + "examples": [ + "Create", + "Create Content Template" + ], + "type": "string" + } + }, + "required": [ + "icon", + "label" + ], + "type": "object" + }, + "MetaPropertyEditorSchema": { + "properties": { + "defaultPropertyEditorUiAlias": { + "type": "string" + }, + "settings": { + "$ref": "#/definitions/PropertyEditorSettings" + } + }, + "required": [ + "defaultPropertyEditorUiAlias" + ], + "type": "object" + }, + "MetaPropertyEditorUi": { + "properties": { + "group": { + "default": "Common", + "description": "The group that this property editor UI belongs to, which will be used to group the property editor UIs in the property editor picker.\nIf not specified, the property editor UI will be grouped under \"Common\".", + "examples": [ + "Common", + "Content", + "Media" + ], + "type": "string" + }, + "icon": { + "type": "string" + }, + "label": { + "type": "string" + }, + "propertyEditorSchemaAlias": { + "description": "The alias of the property editor schema that this property editor UI is for.\nIf not specified, the property editor UI can only be used to configure other property editors.", + "examples": [ + "Umbraco.TextBox", + "Umbraco.TextArea", + "Umbraco.Label" + ], + "type": "string" + }, + "settings": { + "$ref": "#/definitions/PropertyEditorSettings" + }, + "supportsReadOnly": { + "type": "boolean" + } + }, + "required": [ + "group", + "icon", + "label" + ], + "type": "object" + }, + "MetaSection": { + "properties": { + "label": { + "type": "string" + }, + "pathname": { + "type": "string" + } + }, + "required": [ + "label", + "pathname" + ], + "type": "object" + }, + "MetaSectionSidebarAppMenuKind": { + "properties": { + "label": { + "type": "string" + }, + "menu": { + "type": "string" + } + }, + "required": [ + "label", + "menu" + ], + "type": "object" + }, + "MetaSectionView": { + "properties": { + "icon": { + "description": "The icon displayed for this view in the navigation.", + "examples": [ + "box" + ], + "type": "string" + }, + "label": { + "description": "The displayed name (label) in the navigation.", + "type": "string" + }, + "pathname": { + "description": "This is the URL path part for this view. This is used for navigating or deep linking directly to the view\nhttps://yoursite.com/section/settings/view/my-view-path", + "examples": [ + "my-view-path" + ], + "type": "string" + } + }, + "required": [ + "icon" + ], + "type": "object" + }, + "MetaTinyMcePlugin": { + "properties": { + "toolbar": { + "description": "If the plugin adds toolbar buttons, this property can be used to configure the buttons.\nThis configuration will be used on the Rich Text Editor configuration page.", + "items": { + "properties": { + "alias": { + "description": "The alias of the toolbar button that will be configured in the TinyMCE editor.", + "type": "string" + }, + "icon": { + "description": "The icon shown on the Rich Text Editor configuration page. The icon has to be a part of TinyMCE's icon set.", + "type": "string" + }, + "label": { + "description": "The label of the option shown on the Rich Text Editor configuration page.", + "type": "string" + } + }, + "required": [ + "alias", + "label" + ], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "MetaTree": { + "properties": { + "repositoryAlias": { + "type": "string" + } + }, + "required": [ + "repositoryAlias" + ], + "type": "object" + }, + "MetaUserProfileApp": { + "properties": { + "label": { + "type": "string" + }, + "pathname": { + "type": "string" + } + }, + "required": [ + "label", + "pathname" + ], + "type": "object" + }, + "MetaWorkspace": { + "properties": { + "entityType": { + "type": "string" + } + }, + "required": [ + "entityType" + ], + "type": "object" + }, + "MetaWorkspaceAction": { + "type": "object" + }, + "MetaWorkspaceActionDefaultKind": { + "properties": { + "color": { + "enum": [ + "", + "danger", + "default", + "positive", + "warning" + ], + "type": "string" + }, + "label": { + "type": "string" + }, + "look": { + "enum": [ + "", + "default", + "outline", + "placeholder", + "primary", + "secondary" + ], + "type": "string" + } + }, + "type": "object" + }, + "MetaWorkspaceActionMenuItem": { + "type": "object" + }, + "PropertyEditorSettings": { + "properties": { + "defaultData": { + "items": { + "$ref": "#/definitions/PropertyEditorSettingsDefaultData" + }, + "type": "array" + }, + "properties": { + "items": { + "$ref": "#/definitions/PropertyEditorSettingsProperty" + }, + "type": "array" + } + }, + "required": [ + "properties" + ], + "type": "object" + }, + "PropertyEditorSettingsDefaultData": { + "properties": { + "alias": { + "type": "string" + }, + "value": {} + }, + "required": [ + "alias", + "value" + ], + "type": "object" + }, + "PropertyEditorSettingsProperty": { + "properties": { + "alias": { + "type": "string" + }, + "config": { + "items": { + "properties": { + "alias": { + "type": "string" + }, + "value": {} + }, + "required": [ + "alias" + ], + "type": "object" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "label": { + "type": "string" + }, + "propertyEditorUiAlias": { + "type": "string" + } + }, + "required": [ + "alias", + "label", + "propertyEditorUiAlias" + ], + "type": "object" + }, + "SectionAliasConditionConfig": { + "allOf": [ + { + "$ref": "#/definitions/UmbConditionConfigBase<\"Umb.Condition.SectionAlias\">" + }, + { + "properties": { + "match": { + "description": "Define the section that this extension should be available in", + "type": "string" + } + }, + "required": [ + "match" + ], + "type": "object" + } + ] + }, + "SwitchConditionConfig": { + "allOf": [ + { + "$ref": "#/definitions/UmbConditionConfigBase<\"Umb.Condition.Switch\">" + }, + { + "properties": { + "frequency": { + "type": "string" + } + }, + "required": [ + "frequency" + ], + "type": "object" + } + ] + }, + "UmbConditionConfigBase<\"Umb.Condition.CollectionAlias\">": { + "properties": { + "alias": { + "const": "Umb.Condition.CollectionAlias", + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbConditionConfigBase<\"Umb.Condition.CollectionBulkActionPermission\">": { + "properties": { + "alias": { + "const": "Umb.Condition.CollectionBulkActionPermission", + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbConditionConfigBase<\"Umb.Condition.SectionAlias\">": { + "properties": { + "alias": { + "const": "Umb.Condition.SectionAlias", + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbConditionConfigBase<\"Umb.Condition.Switch\">": { + "properties": { + "alias": { + "const": "Umb.Condition.Switch", + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbConditionConfigBase<\"Umb.Condition.UserPermission\">": { + "properties": { + "alias": { + "const": "Umb.Condition.UserPermission", + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbConditionConfigBase<\"Umb.Condition.WorkspaceAlias\">": { + "properties": { + "alias": { + "const": "Umb.Condition.WorkspaceAlias", + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbConditionConfigBase<\"Umb.Condition.WorkspaceEntityType\">": { + "properties": { + "alias": { + "const": "Umb.Condition.WorkspaceEntityType", + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbConditionConfigBase": { + "properties": { + "alias": { + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbConditionConfigBase_1": { + "properties": { + "alias": { + "type": "string" + } + }, + "required": [ + "alias" + ], + "type": "object" + }, + "UmbLocalizationDictionary": { + "type": "object" + }, + "UmbModalConfig": { + "properties": { + "key": { + "type": "string" + }, + "size": { + "enum": [ + "full", + "large", + "medium", + "small" + ], + "type": "string" + }, + "type": { + "enum": [ + "dialog", + "sidebar" + ], + "type": "string" + } + }, + "type": "object" + }, + "UmbModalToken": { + "properties": { + "#alias": { + "type": "string" + }, + "#defaults": { + "$ref": "#/definitions/UmbModalTokenDefaults" + }, + "DATA": { + "description": "Get the data type of the token's data.", + "type": "{ModalDataType}" + }, + "VALUE": { + "description": "Get the value type of the token", + "type": "{ModalValueType}" + } + }, + "required": [ + "#alias", + "DATA", + "VALUE" + ], + "type": "object" + }, + "UmbModalTokenDefaults": { + "properties": { + "data": { + "additionalProperties": true, + "properties": {}, + "type": "object" + }, + "modal": { + "$ref": "#/definitions/UmbModalConfig" + }, + "value": {} + }, + "type": "object" + }, + "UmbracoPackageImportmap": { + "properties": { + "imports": { + "$ref": "#/definitions/UmbracoPackageImportmapValue", + "description": "This is used to define the module specifiers and their respective paths for the package to be used in the browser.", + "examples": [ + { + "library": "./path/to/library.js", + "library/*": "./path/to/library/*" + } + ], + "title": "A module specifier with a path for the importmap" + }, + "scopes": { + "$ref": "#/definitions/UmbracoPackageImportmapScopes", + "description": "This is used to define the scopes for the package to be used in the browser. It has to specify a path and a value that is an object with module specifiers and their respective paths.", + "examples": [ + { + "/path/to/library": { + "library": "./path/to/some/other/library.js" + } + } + ], + "title": "The importmap scopes for the package" + } + }, + "required": [ + "imports" + ], + "type": "object" + }, + "UmbracoPackageImportmapScopes": { + "additionalProperties": { + "$ref": "#/definitions/UmbracoPackageImportmapValue" + }, + "type": "object" + }, + "UmbracoPackageImportmapValue": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "UserPermissionConditionConfig": { + "allOf": [ + { + "$ref": "#/definitions/UmbConditionConfigBase<\"Umb.Condition.UserPermission\">" + }, + { + "properties": { + "match": { + "type": "string" + } + }, + "required": [ + "match" + ], + "type": "object" + } + ] + }, + "WorkspaceAliasConditionConfig": { + "allOf": [ + { + "$ref": "#/definitions/UmbConditionConfigBase<\"Umb.Condition.WorkspaceAlias\">" + }, + { + "properties": { + "match": { + "description": "Define the workspace that this extension should be available in", + "type": "string" + }, + "oneOf": { + "description": "Define one or more workspaces that this extension should be available in", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + ] + }, + "WorkspaceEntityTypeConditionConfig": { + "allOf": [ + { + "$ref": "#/definitions/UmbConditionConfigBase<\"Umb.Condition.WorkspaceEntityType\">" + }, + { + "properties": { + "match": { + "description": "Define the workspace that this extension should be available in", + "type": "string" + } + }, + "required": [ + "match" + ], + "type": "object" + } + ] + } + }, + "description": "Umbraco package manifest JSON", + "properties": { + "allowTelemetry": { + "default": true, + "title": "Decides if the package sends telemetry data for collection", + "type": "boolean" + }, + "extensions": { + "items": { + "$ref": "#/definitions/ManifestTypes" + }, + "title": "An array of Umbraco package manifest types that will be installed", + "type": "array" + }, + "id": { + "title": "The unique identifier of the Umbraco package", + "type": "string" + }, + "importmap": { + "$ref": "#/definitions/UmbracoPackageImportmap", + "description": "This is used to define the imports and the scopes for the package to be used in the browser. It will be combined with the global importmap.", + "title": "The importmap for the package" + }, + "name": { + "title": "The name of the Umbraco package", + "type": "string" + }, + "version": { + "examples": [ + "0.1.0" + ], + "title": "The version of the Umbraco package in the style of semver", + "type": "string" + } + }, + "required": [ + "extensions", + "name" + ], + "type": "object" +} + diff --git a/uSync/packages.lock.json b/uSync/packages.lock.json index 28e83c5c..66c399c2 100644 --- a/uSync/packages.lock.json +++ b/uSync/packages.lock.json @@ -355,20 +355,19 @@ }, "Microsoft.AspNetCore.Http.Features": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "6sVnhFwtsjEVL09FsYpAttQ3Og6Jxg1dQFLF9XQUThi1myq64imjhj1swd92TXMLCp5wmt8szDixZXXdx64qhg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "5.0.0", - "System.IO.Pipelines": "5.0.0" - } - }, - "Microsoft.AspNetCore.JsonPatch": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "Zq13zrOOnDs6PZRlu3sXVEZ1QGbJj7Fw48UtC/ZYIWZ18T8Jkjo7OodzYXSaJgDAXAtDoakvo83N8Mjx7EI9Gg==", + "resolved": "1.0.2", + "contentHash": "9l/Y/CO3q8tET3w+dDiByREH8lRtpd14cMevwMV5nw2a/avJ5qcE3VVIE5U5hesec2phTT6udQEgwjHmdRRbig==", "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Newtonsoft.Json": "13.0.3" + "Microsoft.Extensions.Primitives": "1.0.1", + "System.Collections": "4.0.11", + "System.ComponentModel": "4.0.1", + "System.Linq": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Net.WebSockets": "4.0.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Claims": "4.0.1", + "System.Security.Cryptography.X509Certificates": "4.1.0", + "System.Security.Principal": "4.0.1" } }, "Microsoft.AspNetCore.Metadata": { @@ -376,16 +375,6 @@ "resolved": "8.0.0", "contentHash": "OmuSztiZMitRTYlbMNDkBk3BinSsVcOApSNBAsrw+KYNJh6ALarPhWLlKdtvMgrKzpyCY06xtLAjTmQLURHSlQ==" }, - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "YWNvdHGCHGWKILgEzUDe6soozYnknlSB3IY092zxjdgLoaCPRte2lnbRRS7Nt0lEFbsFjN/Eo2fCI5yusPK0iQ==", - "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "8.0.1", - "Newtonsoft.Json": "13.0.3", - "Newtonsoft.Json.Bson": "1.0.2" - } - }, "Microsoft.AspNetCore.Mvc.Razor.Extensions": { "type": "Transitive", "resolved": "6.0.0", @@ -444,11 +433,6 @@ "Microsoft.CodeAnalysis.Common": "4.0.0" } }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "6.0.5", @@ -573,15 +557,6 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, - "Microsoft.Extensions.FileProviders.Composite": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "0IoXXfkgKpYJB1t2lC0jPXAxuaywRNc9y2Mq96ZZNKBthL38vusa2UK73+Bm6Kq/9a5xNHJS6NhsSN+i5TEtkA==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "5.0.0", - "Microsoft.Extensions.Primitives": "5.0.0" - } - }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Transitive", "resolved": "8.0.1", @@ -901,14 +876,6 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "Newtonsoft.Json.Bson": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", - "dependencies": { - "Newtonsoft.Json": "12.0.1" - } - }, "NPoco": { "type": "Transitive", "resolved": "5.7.1", @@ -918,11 +885,6 @@ "System.Reflection.Emit.Lightweight": "4.7.0" } }, - "NUglify": { - "type": "Transitive", - "resolved": "1.20.2", - "contentHash": "vz/SjCdpxr0Jp09VzMeezid7rwbXimik2QO1dzxzDcN3bXGJloDGDVh0zoD6DA23y6yrRzxv1ZKJ3kKzV3rqyA==" - }, "OpenIddict": { "type": "Transitive", "resolved": "4.10.1", @@ -1354,47 +1316,6 @@ "Serilog": "2.8.0" } }, - "Smidge": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "AnRsxwg4Av7jxa0MkQMbLqdIrWbVZRVQ0KfnO4Mh19Old7lay179QvBnaOPFxAEWnIl4jHiZW8izesJp6TknVw==", - "dependencies": { - "Smidge.Core": "4.3.0" - } - }, - "Smidge.Core": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "B6m6uGpJrOKaJ68eE9clAzZUcURszTNHfoYa4razb3KUJtRXB5fmZvts8+0ffT0/tO09Vu2O/KFfiSZMp6X8Jw==", - "dependencies": { - "Microsoft.AspNetCore.Http.Features": "5.0.0", - "Microsoft.Extensions.Configuration": "5.0.0", - "Microsoft.Extensions.Configuration.Json": "5.0.0", - "Microsoft.Extensions.FileProviders.Composite": "5.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "5.0.0", - "Microsoft.Extensions.Logging.Abstractions": "5.0.0", - "Microsoft.Extensions.Options": "5.0.0" - } - }, - "Smidge.InMemory": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fKyR6ICS0YoQLX0D4dIIYTwQEM1IZb8ChYhqLGpVyJ7GiOAawsXt4ZcVnH0XT+ggan2+JzQlLiXGcCdXnb16Xg==", - "dependencies": { - "Dazinator.Extensions.FileProviders": "2.0.0", - "Smidge.Core": "4.3.0", - "System.Text.Encodings.Web": "5.0.1" - } - }, - "Smidge.Nuglify": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kx5Ulh+o5zLI0Al0POs0nYPldUArErmrAxxccrrxl77MWWrDM3KS5IRWuKDtC42/sZKSzapmJIOwJ8r/1foMCg==", - "dependencies": { - "Nuglify": "1.20.2", - "Smidge": "4.3.0" - } - }, "Swashbuckle.AspNetCore": { "type": "Transitive", "resolved": "6.5.0", @@ -1806,6 +1727,17 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.Net.WebSockets": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2KJo8hir6Edi9jnMDAMhiJoI691xRBmKcbNpwjrvpIMOCTYOtBpSsSEGBxBDV7PKbasJNaFp1+PZz1D7xS41Hg==", + "dependencies": { + "Microsoft.Win32.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, "System.ObjectModel": { "type": "Transitive", "resolved": "4.3.0", @@ -1997,6 +1929,20 @@ "System.Security.Principal.Windows": "5.0.0" } }, + "System.Security.Claims": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, "System.Security.Cryptography.Algorithms": { "type": "Transitive", "resolved": "4.3.0", @@ -2157,6 +2103,14 @@ "System.Security.Cryptography.Pkcs": "8.0.0" } }, + "System.Security.Principal": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, "System.Security.Principal.Windows": { "type": "Transitive", "resolved": "5.0.0", @@ -2315,34 +2269,35 @@ }, "Umbraco.Cms.Api.Common": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "A6zX7Uj16hZJqXTMVPZ+oKcuwYWSh4BEYyRpAhg8HGcRWR0jhpzzZ44t0ppCRlkxPTuLS+dz1gFo783WK68QYw==", + "resolved": "14.0.0-beta001", + "contentHash": "bpbf97stWqg5jzCDJEaGzkWqTJO/mRz3jzVI2iA/b4H7+/M85OIZlJvZ46zBXNl0gGlke4JIIFCblIgkrGyq/Q==", "dependencies": { "Asp.Versioning.Mvc": "8.0.0", "Asp.Versioning.Mvc.ApiExplorer": "8.0.0", "OpenIddict.Abstractions": "4.10.1", "OpenIddict.AspNetCore": "4.10.1", "Swashbuckle.AspNetCore": "6.5.0", - "Umbraco.Cms.Core": "[14.0.0--preview006, 15.0.0)", - "Umbraco.Cms.Web.Common": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Core": "[14.0.0-beta001, 15.0.0)", + "Umbraco.Cms.Web.Common": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.Cms.Api.Management": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "CQF/1i/I7YVp9JLCHrmdt34mNYnO/C58RTt+n4wwQFy3HFl2FMs5+fhTt7xz18J8Q9e9bVCDozs/2xlGmnYKMA==", + "resolved": "14.0.0-beta001", + "contentHash": "NUc5WZ/+COvgED4or1PDT1B7BvvrC1KLwZ9EhHHDE2+VTFkp6uH77P90mEhSqMyl1oq3Db/uYrbUwOpeRCncLw==", "dependencies": { "JsonPatch.Net": "2.1.0", "Swashbuckle.AspNetCore": "6.5.0", - "Umbraco.Cms.Api.Common": "[14.0.0--preview006, 15.0.0)", - "Umbraco.Cms.Infrastructure": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Api.Common": "[14.0.0-beta001, 15.0.0)", + "Umbraco.Cms.Infrastructure": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.Cms.Core": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "jNV8u0ZekZsmIuStkwLrhMxztT7eHhCHwyOpMMbNOdzCbqBQUuOb3MDcz4RUXJwS/BlXqfoTu4l7Pxdz7oDj/g==", + "resolved": "14.0.0-beta001", + "contentHash": "W+GVz5RhAntIV40cQ6xifO/DWvfP1PopHoomqwgrI214aMajIWTEc64i+Lkd+lpdDCmk2bnDiyIt/sx7mI03IQ==", "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Caching.Memory": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Embedded": "8.0.1", @@ -2357,18 +2312,18 @@ }, "Umbraco.Cms.Examine.Lucene": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "VkctBavadScmuSwgzj8hxKJKhA7giJnpmQyiV9jxVvF9SW375nkjTtQk3itEZcotjY9AJJJkbGfZjiKbhlgNvg==", + "resolved": "14.0.0-beta001", + "contentHash": "DDNsvIMKH3w41Ml8ZFHNq3eGbx51jo9rrfpEfZ2Zak/qhtCdf8CotV4Ic13XHvxyx4reIseekEUuL1nVnXhbcg==", "dependencies": { "Examine": "3.2.0", "System.Security.Cryptography.Xml": "8.0.0", - "Umbraco.Cms.Infrastructure": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Infrastructure": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.Cms.Infrastructure": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "zB+wjCSoMCN8jKUva1yoc19oeDpAhMNhjMLo98L0Agb+lJsMJNHyxHf95i97v4E654UfibSPsda0xw26HeJcSQ==", + "resolved": "14.0.0-beta001", + "contentHash": "uO6pwXe49pTCJoPFvS0rB2WBKXDF7mdJzX2z2jeHbwqHd9oZiNOo+mveLjwa1O/uPiSX9M+EerUpzgXzXDJlHQ==", "dependencies": { "Examine.Core": "3.2.0", "HtmlAgilityPack": "1.11.57", @@ -2382,7 +2337,6 @@ "Microsoft.Extensions.Identity.Stores": "8.0.1", "MiniProfiler.Shared": "4.3.8", "NPoco": "5.7.1", - "Newtonsoft.Json": "13.0.3", "OpenIddict.Abstractions": "4.10.1", "Serilog": "3.1.1", "Serilog.Enrichers.Process": "2.0.2", @@ -2395,57 +2349,44 @@ "Serilog.Sinks.Async": "1.5.0", "Serilog.Sinks.File": "5.0.0", "Serilog.Sinks.Map": "1.0.2", - "Umbraco.Cms.Core": "[14.0.0--preview006, 15.0.0)", + "Umbraco.Cms.Core": "[14.0.0-beta001, 15.0.0)", "ncrontab": "3.3.3" } }, "Umbraco.Cms.PublishedCache.NuCache": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "nkK6gGPysMsokfgLeir497YoVgb51qEKPxdtoaT75ENvoXoppo3i+3+WEUQjg73FcuH4lsbB+S8ncqONbPZm2A==", + "resolved": "14.0.0-beta001", + "contentHash": "r7KDkEjVxeVmgNcLmS+D5MnHmxaEhHUUIdy3gIY9c/z/sGwmmiW2WRCquprVe+xLJh7yc7YsYuLSQQ0mO72RFw==", "dependencies": { "K4os.Compression.LZ4": "1.3.6", "MessagePack": "2.5.140", "Umbraco.CSharpTest.Net.Collections": "15.0.0", - "Umbraco.Cms.Infrastructure": "[14.0.0--preview006, 15.0.0)" - } - }, - "Umbraco.Cms.Web.BackOffice": { - "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "CNliwq77WLyh5D5B31qIf3TzEIkSXWGLEscTDw39SGIkzl1FMP9+Own9zrkhelzuG5qt/Z+iyRPppb/Tq7Tirw==", - "dependencies": { - "Newtonsoft.Json": "13.0.3", - "Serilog.AspNetCore": "8.0.1", - "Umbraco.Cms.Web.Common": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Infrastructure": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.Cms.Web.Common": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "hy15XuNvXKL9VkioLG8VWIwa7g2GT27oZ6uKZiXDPObSxBaqnjlKOQxTqWqIa6zEQ4wt3avWjlMK38sO6Dq0yg==", + "resolved": "14.0.0-beta001", + "contentHash": "Q1dP0q9dP1BmfWw/m69RkM0qyAhRW8ps48fPR68LJSs/wQIukOmvWlMxamyZ4lIq9fJXfzt+q88MI5aXIRCNhg==", "dependencies": { "Asp.Versioning.Mvc": "8.0.0", "Asp.Versioning.Mvc.ApiExplorer": "8.0.0", "Dazinator.Extensions.FileProviders": "2.0.0", - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "8.0.1", "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "8.0.1", "MiniProfiler.AspNetCore.Mvc": "4.3.8", "Serilog.AspNetCore": "8.0.1", - "Smidge.InMemory": "4.3.0", - "Smidge.Nuglify": "4.3.0", "System.Net.Http": "4.3.4", "System.Text.RegularExpressions": "4.3.1", - "Umbraco.Cms.Examine.Lucene": "[14.0.0--preview006, 15.0.0)", - "Umbraco.Cms.PublishedCache.NuCache": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Examine.Lucene": "[14.0.0-beta001, 15.0.0)", + "Umbraco.Cms.PublishedCache.NuCache": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.Cms.Web.Website": { "type": "Transitive", - "resolved": "14.0.0--preview006", - "contentHash": "5GRN6PFBrLDRJWUl3T6EvsWBZmkTNxiEkaMfYhFTgdLrsKCcwOvIy1GUAS+mnA4mUeawLj9iFTlkLRRTfneLHQ==", + "resolved": "14.0.0-beta001", + "contentHash": "tP4eueXTCDtdl0DqMNMaXFSx92IFsrndHPLdnxlJc91LVEnr52Q9bhb1gqzsQJx1Lx3Z28lXJZbfk94PBT9gSg==", "dependencies": { - "Umbraco.Cms.Web.Common": "[14.0.0--preview006, 15.0.0)" + "Umbraco.Cms.Web.Common": "[14.0.0-beta001, 15.0.0)" } }, "Umbraco.CSharpTest.Net.Collections": { @@ -2457,15 +2398,15 @@ "type": "Project", "dependencies": { "uSync.Community.Contrib": "[12.0.0, )", - "uSync.Core": "[13.0.0, )" + "uSync.Core": "[14.0.0, )" } }, "usync.backoffice.management.api": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Components.Web": "[8.0.0, )", - "Umbraco.Cms.Api.Management": "[14.0.0--preview006, )", - "uSync.BackOffice": "[13.0.0, )" + "Umbraco.Cms.Api.Management": "[14.0.0-beta001, )", + "uSync.BackOffice": "[14.0.0, )" } }, "usync.backoffice.management.client": { @@ -2477,14 +2418,13 @@ "usync.community.contrib": { "type": "Project", "dependencies": { - "uSync.Core": "[13.0.0, )" + "uSync.Core": "[14.0.0, )" } }, "usync.core": { "type": "Project", "dependencies": { - "Umbraco.Cms.Web.BackOffice": "[14.0.0--preview006, )", - "Umbraco.Cms.Web.Website": "[14.0.0--preview006, )" + "Umbraco.Cms.Web.Website": "[14.0.0-beta001, )" } } } diff --git a/uSync/uSync.cs b/uSync/uSync.cs index 7251670e..92519d2a 100644 --- a/uSync/uSync.cs +++ b/uSync/uSync.cs @@ -1,21 +1,20 @@ -namespace uSync +namespace uSync; + +/// +/// we only have this class, so there is a DLL in the root +/// uSync package. +/// +/// With a root DLL, the package can be stopped from installing +/// on .netframework sites. +/// +public static class uSync { - /// - /// we only have this class, so there is a DLL in the root - /// uSync package. - /// - /// With a root DLL, the package can be stopped from installing - /// on .netframework sites. - /// - public static class uSync - { - public static string PackageName = "uSync"; - // private static string Welcome = "uSync all the things"; - } + public static string PackageName = "uSync"; + // private static string Welcome = "uSync all the things"; +} - // from v10.1 the package.manifest is enough for a file to appear in - // the list of installed packages +// from v10.1 the package.manifest is enough for a file to appear in +// the list of installed packages - // from v10.2 the version also comes from the package.manifest - // so we can remove the package migration, as we don't need it now. -} +// from v10.2 the version also comes from the package.manifest +// so we can remove the package migration, as we don't need it now.