From 9a8d3a2d9d8a03018709d6c275cc8dc6b24017d3 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Thu, 28 May 2026 11:53:10 +0100 Subject: [PATCH 1/5] V17/image upload mapper (#959) * update packages * Split image upload and Image cropper mappers (to handle uploads in media slightly diffrently) * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * add missing xml comments * add debug , so we can see which mapper we hit. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Directory.Packages.props | 68 ++++---- .../Extensions/uSyncActionExtensions.cs | 4 +- .../Services/ISyncVersionFileService.cs | 12 ++ .../Services/SyncVersionFileService.cs | 14 ++ uSync.BackOffice/uSyncBackOffice.cs | 3 + .../usync-assets/package-lock.json | 4 +- .../usync-assets/package.json | 2 +- uSync.Core/Mapping/Mappers/ImagePathMapper.cs | 150 +--------------- .../Mapping/Mappers/ImagePathMapperBase.cs | 163 ++++++++++++++++++ .../Mapping/Mappers/ImageUploadMapper.cs | 51 ++++++ uSync.Core/Mapping/SyncBlockMapperBase.cs | 3 +- .../history-client/package-lock.json | 4 +- uSync.History/history-client/package.json | 2 +- uSync.Tests/packages.lock.json | 2 +- 14 files changed, 297 insertions(+), 185 deletions(-) create mode 100644 uSync.Core/Mapping/Mappers/ImagePathMapperBase.cs create mode 100644 uSync.Core/Mapping/Mappers/ImageUploadMapper.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 2fc28662..dd3d247e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,36 +1,36 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/uSync.BackOffice/Extensions/uSyncActionExtensions.cs b/uSync.BackOffice/Extensions/uSyncActionExtensions.cs index 923eafb1..e9cf8146 100644 --- a/uSync.BackOffice/Extensions/uSyncActionExtensions.cs +++ b/uSync.BackOffice/Extensions/uSyncActionExtensions.cs @@ -137,7 +137,9 @@ public static void UpdateActions(this List actions, Guid k public static bool RequiresSave(this SyncAttempt attempt) => attempt.Success && attempt.Change > Core.ChangeType.NoChange && !attempt.Saved && attempt.Item != null; - + /// + /// return the uSyncAction as an ActionView (used in the controllers) + /// public static uSyncActionView AsActionView(this uSyncAction action) { var msg = string.IsNullOrWhiteSpace(action.Message) is false diff --git a/uSync.BackOffice/Services/ISyncVersionFileService.cs b/uSync.BackOffice/Services/ISyncVersionFileService.cs index b3355932..90152666 100644 --- a/uSync.BackOffice/Services/ISyncVersionFileService.cs +++ b/uSync.BackOffice/Services/ISyncVersionFileService.cs @@ -2,8 +2,20 @@ namespace uSync.BackOffice.Services; +/// +/// Controls the version file we write to disk on syncs (used to warn if sync is old) +/// public interface ISyncVersionFileService { + /// + /// get the Sync file version information for a folder. + /// Task GetSyncFileInfo(string folder); + + /// + /// write the version information to disk. + /// + /// + /// Task WriteVersionFileAsync(string folder); } \ No newline at end of file diff --git a/uSync.BackOffice/Services/SyncVersionFileService.cs b/uSync.BackOffice/Services/SyncVersionFileService.cs index af146b8e..fee5c640 100644 --- a/uSync.BackOffice/Services/SyncVersionFileService.cs +++ b/uSync.BackOffice/Services/SyncVersionFileService.cs @@ -119,9 +119,23 @@ private bool HmacValuesMatch(XElement node) } } +/// +/// results of a check of the version file +/// public class SyncFileVersionCheckResult { + /// + /// the sync on disk is current to the current format we are writing. + /// public bool IsCurrent { get; set; } + + /// + /// the version we are writing to disk + /// public string? FormatVersion { get; set; } + + /// + /// the hmac value for the folders matches. (reserved) + /// public bool HmacMatch { get; set; } } diff --git a/uSync.BackOffice/uSyncBackOffice.cs b/uSync.BackOffice/uSyncBackOffice.cs index 2ac79b1f..8f9f519e 100644 --- a/uSync.BackOffice/uSyncBackOffice.cs +++ b/uSync.BackOffice/uSyncBackOffice.cs @@ -7,6 +7,9 @@ namespace uSync.BackOffice; /// public class uSync { + /// + /// assembly version for uSync + /// public static Version Version => typeof(uSync).Assembly.GetName().Version ?? new Version(15, 0, 0); /// diff --git a/uSync.Backoffice.Management.Client/usync-assets/package-lock.json b/uSync.Backoffice.Management.Client/usync-assets/package-lock.json index 6f9c935c..a0c082e7 100644 --- a/uSync.Backoffice.Management.Client/usync-assets/package-lock.json +++ b/uSync.Backoffice.Management.Client/usync-assets/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jumoo/usync", - "version": "17.3.2", + "version": "17.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jumoo/usync", - "version": "17.3.2", + "version": "17.3.3", "license": "MPL-2.0", "devDependencies": { "@hey-api/openapi-ts": "^0.95.0", diff --git a/uSync.Backoffice.Management.Client/usync-assets/package.json b/uSync.Backoffice.Management.Client/usync-assets/package.json index 9ac73a50..82b5cbd2 100644 --- a/uSync.Backoffice.Management.Client/usync-assets/package.json +++ b/uSync.Backoffice.Management.Client/usync-assets/package.json @@ -8,7 +8,7 @@ "homepage": "https://jumoo.co.uk/uSync", "license": "MPL-2.0", "type": "module", - "version": "17.3.2", + "version": "17.3.3", "main": "./dist/usync.js", "types": "./dist/index.d.ts", "module": "./dist/usync.js", diff --git a/uSync.Core/Mapping/Mappers/ImagePathMapper.cs b/uSync.Core/Mapping/Mappers/ImagePathMapper.cs index 807fd9c7..a8730d28 100644 --- a/uSync.Core/Mapping/Mappers/ImagePathMapper.cs +++ b/uSync.Core/Mapping/Mappers/ImagePathMapper.cs @@ -2,8 +2,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using System.Text.RegularExpressions; - using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Media; @@ -11,7 +9,6 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -using uSync.Core.Dependency; using uSync.Core.Extensions; using uSync.Core.Serialization; @@ -28,49 +25,33 @@ namespace uSync.Core.Mapping; /// becomes /// {"src":"/media/2cud1lzo/15656993711_ccd199b83e_k.jpg","crops":null} /// -public class ImagePathMapper : SyncValueMapperBase, ISyncMapper +public class ImagePathMapper : ImagePathMapperBase, ISyncMapper { - private const string _genericMediaPath = "/media"; - - private readonly string _siteRoot; - private string? _mediaFolder; - private readonly ILogger _logger; - private readonly IConfiguration _configuration; private readonly IImageUrlGenerator _imageUrlGenerator; public ImagePathMapper( - IConfiguration configuration, - IOptionsMonitor _globalOptions, IEntityService entityService, ILogger logger, - IImageUrlGenerator imageUrlGenerator) : base(entityService) + IConfiguration configuration, + IOptionsMonitor globalOptions, + IImageUrlGenerator imageUrlGenerator) : base(entityService, logger, configuration, globalOptions) { - _logger = logger; - _configuration = configuration; - - // todo: site root might need us to include extra NuGet. - _siteRoot = ""; - - _mediaFolder = GetMediaFolderSetting(_globalOptions.CurrentValue.UmbracoMediaPath.TrimStart('~')); - _globalOptions.OnChange(x => _mediaFolder = GetMediaFolderSetting(x.UmbracoMediaPath.TrimStart('~'))); - - if (logger.IsEnabled(LogLevel.Debug)) - logger.LogDebug("Media Folders: [{media}]", _mediaFolder ?? "(Blank)"); - _imageUrlGenerator = imageUrlGenerator; } public override string Name => "ImageCropper Mapper"; public override string[] Editors => [ - Constants.PropertyEditors.Aliases.ImageCropper, - Constants.PropertyEditors.Aliases.UploadField + Constants.PropertyEditors.Aliases.ImageCropper ]; public override Task GetExportValueAsync(object value, string editorAlias) { return uSyncTaskHelper.FromResultOf(() => { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Getting export value for ImageCropper with value {Value}", value); + var stringValue = value?.ToString(); if (string.IsNullOrWhiteSpace(stringValue)) return stringValue; @@ -107,76 +88,6 @@ public ImagePathMapper( }); } - private string StripSitePath(string filePath) - { - var path = filePath; - if (_siteRoot.Length > 0 && !string.IsNullOrWhiteSpace(filePath) && filePath.InvariantStartsWith(_siteRoot)) - path = filePath.Substring(_siteRoot.Length); - - return ReplacePath(path, _mediaFolder, _genericMediaPath); - } - - private string PrePendSitePath(string filePath) - { - var path = filePath; - if (_siteRoot.Length > 0 && !string.IsNullOrEmpty(filePath)) - path = $"{_siteRoot}{filePath}"; - - return ReplacePath(path, _genericMediaPath, _mediaFolder); - } - - - /// - /// makes a specific media path generic. - /// - /// - /// sometimes paths may be defined by umbraco settings, (especially blob settings) - /// that mean they are not stored as /media - /// - /// for the sake of generic importing we want the folder stored to be /media. - /// so we re-write the setting on import and export - /// - /// assumes you have a app setting in the web.config - /// - /// /someFolder - /// - /// - /// - private static string ReplacePath(string filePath, string? currentPath, string? targetPath) - { - if (!string.IsNullOrWhiteSpace(targetPath) - && !string.IsNullOrWhiteSpace(currentPath) - && !currentPath.Equals(targetPath)) - { - return Regex.Replace(filePath, $"^{currentPath}", targetPath, RegexOptions.IgnoreCase); - } - - return filePath; - } - - /// - /// Get the media rewrite folder - /// - /// - /// looks in appSettings for uSync:mediaFolder - /// - /// - /// - /// or in uSync8.config for media setting - /// - /// - /// - /// /someFolder - /// - /// - /// - private string GetMediaFolderSetting(string umbracoMediaPath) - { - var folder = this._configuration.GetValue("uSync:MediaFolder", string.Empty); - if (!string.IsNullOrEmpty(folder)) return folder; - - return umbracoMediaPath; - } public override Task GetImportValueAsync(string value, string editorAlias, SyncSerializerOptions options) { @@ -202,49 +113,4 @@ private string GetMediaFolderSetting(string umbracoMediaPath) return json.SerializeJsonNode(true); }); } - - /// - /// Get the actual media file as a dependency. - /// - public override Task> GetDependenciesAsync(object value, string editorAlias, DependencyFlags flags) - { - return uSyncTaskHelper.FromResultOf>(() => - { - - var stringValue = value?.ToString(); - if (string.IsNullOrWhiteSpace(stringValue)) - return []; - - var stringPath = GetImagePath(stringValue).TrimStart('/').ToLower(); - - if (!string.IsNullOrWhiteSpace(stringPath)) - { - return [new uSyncDependency() - { - Name = $"File: {Path.GetFileName(stringPath)}", - Udi = Udi.Create(Constants.UdiEntityType.MediaFile, stringPath), - Flags = flags, - Order = DependencyOrders.OrderFromEntityType(Constants.UdiEntityType.MediaFile), - Level = 0 - }]; - } - - return []; - }); - } - - private string GetImagePath(string stringValue) - { - if (stringValue.TryParseToJsonObject(out var json) is false || json is null) - return StripSitePath(stringValue); - - - if (json.TryGetPropertyValue("src", out var srcNode) is true) - { - var source = srcNode?.GetValue() ?? string.Empty; - if (string.IsNullOrWhiteSpace(source) is false) return source; - } - - return string.Empty; - } } diff --git a/uSync.Core/Mapping/Mappers/ImagePathMapperBase.cs b/uSync.Core/Mapping/Mappers/ImagePathMapperBase.cs new file mode 100644 index 00000000..b60b5579 --- /dev/null +++ b/uSync.Core/Mapping/Mappers/ImagePathMapperBase.cs @@ -0,0 +1,163 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using System.Text.RegularExpressions; + +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +using uSync.Core.Dependency; +using uSync.Core.Extensions; + +namespace uSync.Core.Mapping; + +public abstract class ImagePathMapperBase : SyncValueMapperBase +{ + private readonly IConfiguration _configuration; + protected readonly ILogger _logger; + + private const string _genericMediaPath = "/media"; + private readonly string _siteRoot; + private string? _mediaFolder; + + public ImagePathMapperBase( + IEntityService entityService, + ILogger logger, + IConfiguration configuration, + IOptionsMonitor globalOptions + ) : base(entityService) + { + _configuration = configuration; + _logger = logger; + + // todo: site root might need us to include extra NuGet. + _siteRoot = ""; + + _mediaFolder = GetMediaFolderSetting(globalOptions.CurrentValue.UmbracoMediaPath.TrimStart('~')); + globalOptions.OnChange(x => _mediaFolder = GetMediaFolderSetting(x.UmbracoMediaPath.TrimStart('~'))); + + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug("Media Folders: [{media}]", _mediaFolder ?? "(Blank)"); + + } + + + protected string StripSitePath(string filePath) + { + var path = filePath; + if (_siteRoot.Length > 0 && !string.IsNullOrWhiteSpace(filePath) && filePath.InvariantStartsWith(_siteRoot)) + path = filePath.Substring(_siteRoot.Length); + + return ReplacePath(path, _mediaFolder, _genericMediaPath); + } + + protected string PrePendSitePath(string filePath) + { + var path = filePath; + if (_siteRoot.Length > 0 && !string.IsNullOrEmpty(filePath)) + path = $"{_siteRoot}{filePath}"; + + return ReplacePath(path, _genericMediaPath, _mediaFolder); + } + + + /// + /// makes a specific media path generic. + /// + /// + /// sometimes paths may be defined by umbraco settings, (especially blob settings) + /// that mean they are not stored as /media + /// + /// for the sake of generic importing we want the folder stored to be /media. + /// so we re-write the setting on import and export + /// + /// assumes you have a app setting in the web.config + /// + /// /someFolder + /// + /// + /// + private static string ReplacePath(string filePath, string? currentPath, string? targetPath) + { + if (!string.IsNullOrWhiteSpace(targetPath) + && !string.IsNullOrWhiteSpace(currentPath) + && !currentPath.Equals(targetPath)) + { + return Regex.Replace(filePath, $"^{currentPath}", targetPath, RegexOptions.IgnoreCase); + } + + return filePath; + } + + /// + /// Get the media rewrite folder + /// + /// + /// looks in appSettings for uSync:mediaFolder + /// + /// + /// + /// or in uSync8.config for media setting + /// + /// + /// + /// /someFolder + /// + /// + /// + private string GetMediaFolderSetting(string umbracoMediaPath) + { + var folder = this._configuration.GetValue("uSync:MediaFolder", string.Empty); + if (!string.IsNullOrEmpty(folder)) return folder; + + return umbracoMediaPath; + } + + /// + /// Get the actual media file as a dependency. + /// + public override Task> GetDependenciesAsync(object value, string editorAlias, DependencyFlags flags) + { + return uSyncTaskHelper.FromResultOf>(() => + { + + var stringValue = value?.ToString(); + if (string.IsNullOrWhiteSpace(stringValue)) + return []; + + var stringPath = GetImagePath(stringValue).TrimStart('/').ToLower(); + + if (!string.IsNullOrWhiteSpace(stringPath)) + { + return [new uSyncDependency() + { + Name = $"File: {Path.GetFileName(stringPath)}", + Udi = Udi.Create(Constants.UdiEntityType.MediaFile, stringPath), + Flags = flags, + Order = DependencyOrders.OrderFromEntityType(Constants.UdiEntityType.MediaFile), + Level = 0 + }]; + } + + return []; + }); + } + + private string GetImagePath(string stringValue) + { + if (stringValue.TryParseToJsonObject(out var json) is false || json is null) + return StripSitePath(stringValue); + + + if (json.TryGetPropertyValue("src", out var srcNode) is true) + { + var source = srcNode?.GetValue() ?? string.Empty; + if (string.IsNullOrWhiteSpace(source) is false) return source; + } + + return string.Empty; + } +} diff --git a/uSync.Core/Mapping/Mappers/ImageUploadMapper.cs b/uSync.Core/Mapping/Mappers/ImageUploadMapper.cs new file mode 100644 index 00000000..bd47dbf4 --- /dev/null +++ b/uSync.Core/Mapping/Mappers/ImageUploadMapper.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; + +using uSync.Core.Extensions; +using uSync.Core.Serialization; + +namespace uSync.Core.Mapping; + +/// +/// image uploads don't store any of the json, stuff, so they are similar to image croppers, +/// but a bit simpler. +/// +public class ImageUploadMapper : ImagePathMapperBase, ISyncMapper +{ + public ImageUploadMapper( + IEntityService entityService, + ILogger logger, + IConfiguration configuration, + IOptionsMonitor globalOptions) : base(entityService, logger, configuration, globalOptions) + { } + + public override string Name => "Image Upload Mapper"; + public override string[] Editors => [Constants.PropertyEditors.Aliases.UploadField]; + public override Task GetExportValueAsync(object value, string editorAlias) + { + return uSyncTaskHelper.FromResultOf(() => + { + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Getting export value for ImageUpload with value {Value}", value); + + var stringValue = value?.ToString(); + if (string.IsNullOrWhiteSpace(stringValue)) return stringValue; + return StripSitePath(stringValue); + }); + } + + public override Task GetImportValueAsync(string value, string editorAlias, SyncSerializerOptions options) + { + return uSyncTaskHelper.FromResultOf(() => + { + var stringValue = value?.ToString(); + if (string.IsNullOrWhiteSpace(stringValue)) return stringValue; + return PrePendSitePath(stringValue); + }); + } +} diff --git a/uSync.Core/Mapping/SyncBlockMapperBase.cs b/uSync.Core/Mapping/SyncBlockMapperBase.cs index b9d24030..267d288e 100644 --- a/uSync.Core/Mapping/SyncBlockMapperBase.cs +++ b/uSync.Core/Mapping/SyncBlockMapperBase.cs @@ -69,7 +69,8 @@ public SyncBlockMapperBase( // and prevent double-encoding when the block value is re-serialized. if (result is string stringResult && value.IsNonStringJsonValue()) { - return stringResult.ConvertToJsonNode() ?? result; + return stringResult.ConvertStringToExpandedJson() ?? result; + // return stringResult.ConvertToJsonNode() ?? result; } return result; diff --git a/uSync.History/history-client/package-lock.json b/uSync.History/history-client/package-lock.json index c9bd6758..426fb256 100644 --- a/uSync.History/history-client/package-lock.json +++ b/uSync.History/history-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "usync-history-client", - "version": "17.3.2", + "version": "17.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "usync-history-client", - "version": "17.3.2", + "version": "17.3.3", "devDependencies": { "@hey-api/openapi-ts": "^0.95.0", "@jumoo/translate": "^17.2.3", diff --git a/uSync.History/history-client/package.json b/uSync.History/history-client/package.json index bca2bcfc..9696c20e 100644 --- a/uSync.History/history-client/package.json +++ b/uSync.History/history-client/package.json @@ -1,6 +1,6 @@ { "name": "usync-history-client", - "version": "17.3.2", + "version": "17.3.3", "licence": "Custom", "description": "uSync history function", "type": "module", diff --git a/uSync.Tests/packages.lock.json b/uSync.Tests/packages.lock.json index 2e15ba6c..8c9df6ff 100644 --- a/uSync.Tests/packages.lock.json +++ b/uSync.Tests/packages.lock.json @@ -2616,7 +2616,7 @@ }, "Umbraco.Cms": { "type": "CentralTransitive", - "requested": "[17.3.0, )", + "requested": "[17.4.2, )", "resolved": "17.3.0", "contentHash": "yaG/li5/5RT7354r6rloErE96HojsDEHyYE+iVxcb+ySCLn2PF424qVSXEYq5ZXAbN4Zbx2/6oytTA4HX5w2rg==", "dependencies": { From b7b2dd9dd4165f45a354af33fec3aeaf57e47d22 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Mon, 8 Jun 2026 14:47:45 +0100 Subject: [PATCH 2/5] check on loading if the backoffice is there, if it's not we don't register anything for uSync. (only works on backoffice servers) (#962) --- uSync.AutoTemplates/AutoTemplateComposer.cs | 8 ++++++-- .../HealthChecks/SyncFolderIntegrityChecks.cs | 19 +++++++++++++++++-- uSync.BackOffice/uSyncBackOfficeComposer.cs | 18 +++++++++++++----- .../ApiComposer.cs | 4 ++++ .../uSyncManifestReader.cs | 9 +++++++-- .../Extensions/SyncBuilderExtensions.cs | 9 +++++++++ uSync.History/uSyncHistoryComposer.cs | 5 +++++ 7 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 uSync.Core/Extensions/SyncBuilderExtensions.cs diff --git a/uSync.AutoTemplates/AutoTemplateComposer.cs b/uSync.AutoTemplates/AutoTemplateComposer.cs index c056436f..1b36d052 100644 --- a/uSync.AutoTemplates/AutoTemplateComposer.cs +++ b/uSync.AutoTemplates/AutoTemplateComposer.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core.Composing; +using System.Linq; + +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; namespace uSync.AutoTemplates; @@ -7,6 +9,8 @@ public class AutoTemplateComposer : IComposer { public void Compose(IUmbracoBuilder builder) { - builder.AdduSyncAutoTemplates(); + // only load when the backoffice is enabled. + if (builder.Services.Any(s => s.ServiceType == typeof(IBackOfficeEnabledMarker))) + builder.AdduSyncAutoTemplates(); } } diff --git a/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs b/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs index 926db7bd..c440942f 100644 --- a/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs +++ b/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs @@ -20,8 +20,11 @@ namespace uSync.BackOffice.HealthChecks; Group = "uSync")] public class SyncFolderIntegrityChecks : HealthCheck { - private readonly ISyncConfigService _configService; - private readonly ISyncFileService _fileService; + private readonly ISyncConfigService? _configService; + private readonly ISyncFileService? _fileService; + + + public SyncFolderIntegrityChecks() { } /// /// Constructor @@ -41,6 +44,18 @@ public override HealthCheckStatus ExecuteAction(HealthCheckAction action) /// public override Task> GetStatusAsync() { + if (_configService is null || _fileService is null) + { + return Task.FromResult((IEnumerable)new List + { + new HealthCheckStatus("uSync services not available") + { + Description = "The uSync services are not available, this likely means the site has no backoffice loaded.", + ResultType = StatusResultType.Info + } + }); + } + var items = new List { CheckuSyncFolder(), diff --git a/uSync.BackOffice/uSyncBackOfficeComposer.cs b/uSync.BackOffice/uSyncBackOfficeComposer.cs index f661170d..71512ef9 100644 --- a/uSync.BackOffice/uSyncBackOfficeComposer.cs +++ b/uSync.BackOffice/uSyncBackOfficeComposer.cs @@ -1,7 +1,13 @@  +using Microsoft.Extensions.Logging; + +using System.Linq; + using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; +using uSync.Core.Extensions; + namespace uSync.BackOffice; /// @@ -12,10 +18,12 @@ public class uSyncBackOfficeComposer : IComposer /// 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(); + if (builder.IsUmbracoBackOfficeEnabled() is true) { + // 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.Management.Api/ApiComposer.cs b/uSync.Backoffice.Management.Api/ApiComposer.cs index ca7da952..f5d3a686 100644 --- a/uSync.Backoffice.Management.Api/ApiComposer.cs +++ b/uSync.Backoffice.Management.Api/ApiComposer.cs @@ -7,6 +7,7 @@ using uSync.Backoffice.Management.Api.Configuration; using uSync.Backoffice.Management.Api.Services; using uSync.BackOffice; +using uSync.Core.Extensions; namespace uSync.Backoffice.Management.Api; @@ -15,6 +16,9 @@ public class ApiComposer : IComposer { public void Compose(IUmbracoBuilder builder) { + if (builder.IsUmbracoBackOfficeEnabled() is false) + return; + builder.Services.AddSingleton(); builder.Services.ConfigureOptions(); diff --git a/uSync.Backoffice.Management.Client/uSyncManifestReader.cs b/uSync.Backoffice.Management.Client/uSyncManifestReader.cs index 2a664bb6..b72b385c 100644 --- a/uSync.Backoffice.Management.Client/uSyncManifestReader.cs +++ b/uSync.Backoffice.Management.Client/uSyncManifestReader.cs @@ -10,6 +10,7 @@ using uSync.BackOffice.Configuration; using uSync.BackOffice.Extensions; +using uSync.Core.Extensions; namespace uSync.Backoffice.Management.Client; @@ -18,8 +19,12 @@ public class uSyncManifestComposer : IComposer { public void Compose(IUmbracoBuilder builder) { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + if (builder.IsUmbracoBackOfficeEnabled()) + { + // only load this when the backoffice is enabled. + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + } } } diff --git a/uSync.Core/Extensions/SyncBuilderExtensions.cs b/uSync.Core/Extensions/SyncBuilderExtensions.cs new file mode 100644 index 00000000..4ac0d055 --- /dev/null +++ b/uSync.Core/Extensions/SyncBuilderExtensions.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.DependencyInjection; + +namespace uSync.Core.Extensions; + +public static class SyncBuilderExtensions +{ + public static bool IsUmbracoBackOfficeEnabled(this IUmbracoBuilder builder) + => builder.Services.Any(s => s.ServiceType == typeof(IBackOfficeEnabledMarker)); +} diff --git a/uSync.History/uSyncHistoryComposer.cs b/uSync.History/uSyncHistoryComposer.cs index eea736e4..43783ac3 100644 --- a/uSync.History/uSyncHistoryComposer.cs +++ b/uSync.History/uSyncHistoryComposer.cs @@ -18,6 +18,7 @@ using uSync.BackOffice; using uSync.BackOffice.Extensions; +using uSync.Core.Extensions; using uSync.History.Service; namespace uSync.History @@ -26,6 +27,10 @@ public class uSyncHistoryComposer : IComposer { public void Compose(IUmbracoBuilder builder) { + // don't load if the backoffice is not loaded as part of the project. + if (builder.IsUmbracoBackOfficeEnabled() is false) + return; + builder.Services.AddSingleton(); builder.AddNotificationAsyncHandler(); From 2ed6b808db5ec85ae30017cd92b601b1bfbbad2f Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Tue, 9 Jun 2026 10:43:35 +0100 Subject: [PATCH 3/5] V17/block encoding (#963) * revert the encoding of blocks from #955 - because that breaks rendering as per #958 * Add Null checks to health check (cause it can load without the services). * v17.3.4 - package files. --- Directory.Packages.props | 1 + .../HealthChecks/SyncFolderIntegrityChecks.cs | 23 +++++++++---------- .../usync-assets/package-lock.json | 4 ++-- .../usync-assets/package.json | 2 +- uSync.Core/Mapping/SyncBlockMapperBase.cs | 15 ++++-------- .../history-client/package-lock.json | 4 ++-- uSync.History/history-client/package.json | 2 +- 7 files changed, 22 insertions(+), 29 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dd3d247e..2eb384ad 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,7 @@ + diff --git a/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs b/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs index c440942f..866e292b 100644 --- a/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs +++ b/uSync.BackOffice/HealthChecks/SyncFolderIntegrityChecks.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; @@ -23,7 +24,6 @@ public class SyncFolderIntegrityChecks : HealthCheck private readonly ISyncConfigService? _configService; private readonly ISyncFileService? _fileService; - public SyncFolderIntegrityChecks() { } /// @@ -44,17 +44,8 @@ public override HealthCheckStatus ExecuteAction(HealthCheckAction action) /// public override Task> GetStatusAsync() { - if (_configService is null || _fileService is null) - { - return Task.FromResult((IEnumerable)new List - { - new HealthCheckStatus("uSync services not available") - { - Description = "The uSync services are not available, this likely means the site has no backoffice loaded.", - ResultType = StatusResultType.Info - } - }); - } + if (_configService is null || _fileService is null) + return Task.FromResult(Enumerable.Empty()); var items = new List { @@ -67,6 +58,9 @@ public override Task> GetStatusAsync() private HealthCheckStatus CheckuSyncFolder() { + if (_configService is null || _fileService is null) + return new HealthCheckStatus("Unable to check uSync folder integrity"); + var root = _fileService.GetAbsPath(_configService.GetWorkingFolder()); if (_fileService.DirectoryExists(root) is false) @@ -100,6 +94,8 @@ private HealthCheckStatus CheckuSyncFolder() private List CheckFolder(string folder) { + if (_fileService is null) return []; + var _keys = new Dictionary(); var clashes = new List(); @@ -143,6 +139,9 @@ private List CheckFolder(string folder) private HealthCheckStatus CheckConfigFolderValidity() { + if (_configService is null || _fileService is null) + return new HealthCheckStatus("Unable to check uSync folder integrity"); + var root = _fileService.GetAbsPath(_configService.GetWorkingFolder()); if (_fileService.DirectoryExists(root) is false) diff --git a/uSync.Backoffice.Management.Client/usync-assets/package-lock.json b/uSync.Backoffice.Management.Client/usync-assets/package-lock.json index a0c082e7..fafc88bb 100644 --- a/uSync.Backoffice.Management.Client/usync-assets/package-lock.json +++ b/uSync.Backoffice.Management.Client/usync-assets/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jumoo/usync", - "version": "17.3.3", + "version": "17.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jumoo/usync", - "version": "17.3.3", + "version": "17.3.4", "license": "MPL-2.0", "devDependencies": { "@hey-api/openapi-ts": "^0.95.0", diff --git a/uSync.Backoffice.Management.Client/usync-assets/package.json b/uSync.Backoffice.Management.Client/usync-assets/package.json index 82b5cbd2..a09d7e7e 100644 --- a/uSync.Backoffice.Management.Client/usync-assets/package.json +++ b/uSync.Backoffice.Management.Client/usync-assets/package.json @@ -8,7 +8,7 @@ "homepage": "https://jumoo.co.uk/uSync", "license": "MPL-2.0", "type": "module", - "version": "17.3.3", + "version": "17.3.4", "main": "./dist/usync.js", "types": "./dist/index.d.ts", "module": "./dist/usync.js", diff --git a/uSync.Core/Mapping/SyncBlockMapperBase.cs b/uSync.Core/Mapping/SyncBlockMapperBase.cs index 267d288e..92218a53 100644 --- a/uSync.Core/Mapping/SyncBlockMapperBase.cs +++ b/uSync.Core/Mapping/SyncBlockMapperBase.cs @@ -62,18 +62,11 @@ public SyncBlockMapperBase( _logger.LogDebug("Importing block value for {PropertyEditorAlias} {valueType}", propertyType.PropertyEditorAlias, value?.GetType().Name ?? "blank"); var importString = SyncBlockMapperBase.GetStringValue(value) ?? string.Empty; - var result = await _mapperCollection.Value.GetImportValueAsync(importString, propertyType, options); - // When the original value was a non-string JSON type (array, object, number, etc.), - // convert string results back to JsonNode to preserve the correct JSON type - // and prevent double-encoding when the block value is re-serialized. - if (result is string stringResult && value.IsNonStringJsonValue()) - { - return stringResult.ConvertStringToExpandedJson() ?? result; - // return stringResult.ConvertToJsonNode() ?? result; - } - - return result; + // revert this back to the old way - we don't expand the json we get back because umbraco is very + // sensitve to what the exact format of the blocks is, and if we expand them, then calls during render + // can return null. + return await _mapperCollection.Value.GetImportValueAsync(importString, propertyType, options); } private async Task GetExportProperty(object? value, IPropertyType? propertyType, SyncSerializerOptions options) diff --git a/uSync.History/history-client/package-lock.json b/uSync.History/history-client/package-lock.json index 426fb256..c0b6f51a 100644 --- a/uSync.History/history-client/package-lock.json +++ b/uSync.History/history-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "usync-history-client", - "version": "17.3.3", + "version": "17.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "usync-history-client", - "version": "17.3.3", + "version": "17.3.4", "devDependencies": { "@hey-api/openapi-ts": "^0.95.0", "@jumoo/translate": "^17.2.3", diff --git a/uSync.History/history-client/package.json b/uSync.History/history-client/package.json index 9696c20e..7d491ab4 100644 --- a/uSync.History/history-client/package.json +++ b/uSync.History/history-client/package.json @@ -1,6 +1,6 @@ { "name": "usync-history-client", - "version": "17.3.3", + "version": "17.3.4", "licence": "Custom", "description": "uSync history function", "type": "module", From dc61b53fff563949dd6cf9afd663a38762aad825 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Tue, 9 Jun 2026 14:51:03 +0100 Subject: [PATCH 4/5] Fix https://github.com/Jumoo/uSync.Complete.Issues/issues/297 - write clean files even when folder has no children (so last items get deleted) (#964) --- uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs | 5 ++--- .../usync-assets/package-lock.json | 4 ++-- uSync.Backoffice.Management.Client/usync-assets/package.json | 2 +- uSync.History/history-client/package-lock.json | 4 ++-- uSync.History/history-client/package.json | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs index 222b996d..61f36530 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs @@ -983,10 +983,9 @@ protected virtual async Task> Export_DoExportAsync(TObject await syncFileService.SaveXElementAsync(attempt.Item, filename); } - if (config.CreateClean && await HasChildrenAsync(item)) - { + if (config.CreateClean) await CreateCleanFileAsync(GetItemKey(item), filename); - } + } else { diff --git a/uSync.Backoffice.Management.Client/usync-assets/package-lock.json b/uSync.Backoffice.Management.Client/usync-assets/package-lock.json index fafc88bb..29d67483 100644 --- a/uSync.Backoffice.Management.Client/usync-assets/package-lock.json +++ b/uSync.Backoffice.Management.Client/usync-assets/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jumoo/usync", - "version": "17.3.4", + "version": "17.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jumoo/usync", - "version": "17.3.4", + "version": "17.3.5", "license": "MPL-2.0", "devDependencies": { "@hey-api/openapi-ts": "^0.95.0", diff --git a/uSync.Backoffice.Management.Client/usync-assets/package.json b/uSync.Backoffice.Management.Client/usync-assets/package.json index a09d7e7e..12b591a8 100644 --- a/uSync.Backoffice.Management.Client/usync-assets/package.json +++ b/uSync.Backoffice.Management.Client/usync-assets/package.json @@ -8,7 +8,7 @@ "homepage": "https://jumoo.co.uk/uSync", "license": "MPL-2.0", "type": "module", - "version": "17.3.4", + "version": "17.3.5", "main": "./dist/usync.js", "types": "./dist/index.d.ts", "module": "./dist/usync.js", diff --git a/uSync.History/history-client/package-lock.json b/uSync.History/history-client/package-lock.json index c0b6f51a..0c58e4b2 100644 --- a/uSync.History/history-client/package-lock.json +++ b/uSync.History/history-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "usync-history-client", - "version": "17.3.4", + "version": "17.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "usync-history-client", - "version": "17.3.4", + "version": "17.3.5", "devDependencies": { "@hey-api/openapi-ts": "^0.95.0", "@jumoo/translate": "^17.2.3", diff --git a/uSync.History/history-client/package.json b/uSync.History/history-client/package.json index 7d491ab4..51ade899 100644 --- a/uSync.History/history-client/package.json +++ b/uSync.History/history-client/package.json @@ -1,6 +1,6 @@ { "name": "usync-history-client", - "version": "17.3.4", + "version": "17.3.5", "licence": "Custom", "description": "uSync history function", "type": "module", From 0cdf2f996e4ad2e24d188368ca49d1f047e34e3e Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Thu, 11 Jun 2026 22:12:37 +0100 Subject: [PATCH 5/5] update obsolete call to pagedChildren on content handler. (#965) --- .../SyncHandlers/Handlers/ContentHandler.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs index 19e05d33..c5bc5d95 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/ContentHandler.cs @@ -97,7 +97,15 @@ protected override Task> GetChildItemsAsync(IEntity? parent var total = long.MaxValue; while (page * pageSize < total) { - items.AddRange(_contentService.GetPagedChildren(parent.Id, page++, pageSize, out total)); + items.AddRange(_contentService.GetPagedChildren( + id: parent.Id, + pageIndex: page++, + pageSize: pageSize, + totalRecords: out total, + propertyAliases: null, + filter: null, + ordering: null, + loadTemplates: true)); } return items;