From 25fdb603c0f2263934d0e1e1b27ba1b67b889b6c Mon Sep 17 00:00:00 2001 From: Craig Agnew Date: Wed, 7 Aug 2024 00:14:00 +0100 Subject: [PATCH 01/10] Added idea of StorageProviderFactory to allow configurable maintenance status persistence. --- .../Composers/MaintenceModeComposer.cs | 7 ++ .../Configurations/MaintenanceModeSettings.cs | 1 + .../Configurations/StorageMode.cs | 13 +++ .../Factories/IStorageProviderFactory.cs | 14 ++++ .../Factories/StorageProviderFactory.cs | 33 ++++++++ .../Providers/FileSystemStorageProvider.cs | 79 +++++++++++++++++++ .../Providers/IStorageProvider.cs | 11 +++ .../Services/MaintenanceModeService.cs | 60 ++++---------- 8 files changed, 173 insertions(+), 45 deletions(-) create mode 100644 Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs create mode 100644 Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs create mode 100644 Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs create mode 100644 Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs create mode 100644 Our.Umbraco.MaintenanceMode/Providers/IStorageProvider.cs diff --git a/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs b/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs index f274c14..ecd0958 100644 --- a/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs +++ b/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs @@ -1,10 +1,13 @@ using Microsoft.Extensions.DependencyInjection; using Our.Umbraco.MaintenanceMode.Configurations; +using Our.Umbraco.MaintenanceMode.Factories; using Our.Umbraco.MaintenanceMode.Interfaces; +using Our.Umbraco.MaintenanceMode.NotificationHandlers.Application; using Our.Umbraco.MaintenanceMode.NotificationHandlers.Content; using Our.Umbraco.MaintenanceMode.NotificationHandlers.Media; using Our.Umbraco.MaintenanceMode.NotificationHandlers.ServerVariables; +using Our.Umbraco.MaintenanceMode.Providers; using Our.Umbraco.MaintenanceMode.Services; using System.Collections.Generic; using System.Linq; @@ -14,6 +17,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Our.Umbraco.MaintenanceMode.Composers @@ -40,6 +44,9 @@ public static IUmbracoBuilder AddMaintenceManager(this IUmbracoBuilder builder) builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton() + .AddSingleton(s => s.GetService()); builder.Services.AddUnique(); builder.AddNotificationHandlers(); diff --git a/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs b/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs index 5810e31..ae045ed 100644 --- a/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs +++ b/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs @@ -4,5 +4,6 @@ public class MaintenanceModeSettings { public bool IsInMaintenanceMode { get; set; } public bool IsContentFrozen { get; set; } + public StorageMode StorageMode { get; set; } } } diff --git a/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs b/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs new file mode 100644 index 0000000..ca7a624 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Our.Umbraco.MaintenanceMode.Configurations +{ + public enum StorageMode + { + FileSystem = 0 + } +} diff --git a/Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs b/Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs new file mode 100644 index 0000000..835cea1 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs @@ -0,0 +1,14 @@ +using Our.Umbraco.MaintenanceMode.Providers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Our.Umbraco.MaintenanceMode.Factories +{ + public interface IStorageProviderFactory + { + IStorageProvider GetProvider(); + } +} diff --git a/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs b/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs new file mode 100644 index 0000000..d81ab1f --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Our.Umbraco.MaintenanceMode.Configurations; +using Our.Umbraco.MaintenanceMode.Providers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Our.Umbraco.MaintenanceMode.Factories +{ + public class StorageProviderFactory : IStorageProviderFactory + { + private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; + + private readonly IServiceProvider _serviceProvider; + + public StorageProviderFactory(IOptions maintenanceModeSettings, + IServiceProvider serviceProvider) + { + _maintenanceModeSettings = maintenanceModeSettings.Value; + _serviceProvider = serviceProvider; + } + + public StorageMode StorageMode => this._maintenanceModeSettings?.StorageMode ?? StorageMode.FileSystem; + + public IStorageProvider GetProvider() => StorageMode switch + { + _ => (IStorageProvider)_serviceProvider.GetService(typeof(FileSystemStorageProvider)) + }; + } +} diff --git a/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs new file mode 100644 index 0000000..045949e --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Options; +using Our.Umbraco.MaintenanceMode.Models; +using Serilog; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Extensions; + +namespace Our.Umbraco.MaintenanceMode.Providers +{ + public class FileSystemStorageProvider : IStorageProvider + { + private readonly string _configFilePath; + + private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; + + private readonly ILogger _logger; + + public FileSystemStorageProvider( + ILogger logger, + IOptions maintenanceModeSettings, + IWebHostEnvironment webHostingEnvironment) + { + _maintenanceModeSettings = maintenanceModeSettings.Value; + + // put maintenanceMode config in the 'config folder' + var configFolder = new DirectoryInfo(webHostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config)); + _configFilePath = Path.Combine(configFolder.FullName, "maintenanceMode.json"); + } + + public async Task Read() + { + if (_configFilePath != null && File.Exists(_configFilePath)) + { + // load from config + try + { + var file = await File.ReadAllBytesAsync(_configFilePath); + var status = JsonSerializer.Deserialize(file); + if (status != null) + { + return status; + } + } + catch (Exception ex) + { + _logger.Warning(ex, string.Concat("Failed to load Status ", ex.Message)); + } + } + + return null; + } + + public async Task Save(MaintenanceModeStatus status) + { + try + { + if (File.Exists(_configFilePath)) + File.Delete(_configFilePath); + + Directory.CreateDirectory(Path.GetDirectoryName(_configFilePath)); + + string json = JsonSerializer.Serialize(status); + await File.WriteAllTextAsync(_configFilePath, json); + } + catch (Exception ex) + { + _logger.Debug(ex, string.Concat("Failed to save config ", ex.Message)); + } + } + } +} diff --git a/Our.Umbraco.MaintenanceMode/Providers/IStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/IStorageProvider.cs new file mode 100644 index 0000000..3d44b91 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Providers/IStorageProvider.cs @@ -0,0 +1,11 @@ +using Our.Umbraco.MaintenanceMode.Models; +using System.Threading.Tasks; + +namespace Our.Umbraco.MaintenanceMode.Providers +{ + public interface IStorageProvider + { + Task Save(MaintenanceModeStatus status); + Task Read(); + } +} diff --git a/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs b/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs index a03d535..ab964f8 100644 --- a/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs +++ b/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Options; - +using Our.Umbraco.MaintenanceMode.Factories; using Our.Umbraco.MaintenanceMode.Interfaces; using Our.Umbraco.MaintenanceMode.Models; - +using Our.Umbraco.MaintenanceMode.Providers; using Serilog; using System; @@ -20,20 +20,21 @@ namespace Our.Umbraco.MaintenanceMode.Services public class MaintenanceModeService : IMaintenanceModeService { private readonly ILogger _logger; + private readonly IStorageProvider _storageProvider; + private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; private readonly string _configFilePath; public MaintenanceModeStatus Status { get; private set; } public MaintenanceModeService(ILogger logger, - IOptions maintenanceModeSettings, IWebHostEnvironment webHostingEnvironment) + IOptions maintenanceModeSettings, + IWebHostEnvironment webHostingEnvironment, + IStorageProviderFactory storageProviderFactory) { _logger = logger; + _storageProvider = storageProviderFactory.GetProvider(); _maintenanceModeSettings = maintenanceModeSettings.Value; - // put maintenanceMode config in the 'config folder' - var configFolder = new DirectoryInfo(webHostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config)); - _configFilePath = Path.Combine(configFolder.FullName, "maintenanceMode.json"); - Status = LoadStatus().Result; } @@ -49,7 +50,7 @@ public async Task ToggleMaintenanceMode(bool maintenanceMode) return; // already in this state Status.IsInMaintenanceMode = maintenanceMode; - await SaveToDisk(); + await _storageProvider.Save(Status); } public async Task ToggleContentFreeze(bool isContentFrozen) @@ -58,13 +59,13 @@ public async Task ToggleContentFreeze(bool isContentFrozen) return; // already in this state Status.IsContentFrozen = isContentFrozen; - await SaveToDisk(); + await _storageProvider.Save(Status); } public async Task SaveSettings(MaintenanceModeSettings settings) { Status.Settings = settings; - await SaveToDisk(); + await _storageProvider.Save(Status); } private async Task LoadStatus() @@ -79,45 +80,11 @@ private async Task LoadStatus() } }; - if (_configFilePath != null && File.Exists(_configFilePath)) - { - // load from config - try - { - var file = await File.ReadAllBytesAsync(_configFilePath); - var status = JsonSerializer.Deserialize(file); - if (status != null) - { - maintenanceModeStatus = status; - } - } - catch (Exception ex) - { - _logger.Warning(ex, string.Concat("Failed to load Status ", ex.Message)); - } - } + maintenanceModeStatus = await CheckStorage(maintenanceModeStatus); return CheckAppSettings(maintenanceModeStatus); } - private async Task SaveToDisk() - { - try - { - if (File.Exists(_configFilePath)) - File.Delete(_configFilePath); - - Directory.CreateDirectory(Path.GetDirectoryName(_configFilePath)); - - string json = JsonSerializer.Serialize(Status); - await File.WriteAllTextAsync(_configFilePath, json); - } - catch (Exception ex) - { - _logger.Debug(ex, string.Concat("Failed to save config ", ex.Message)); - } - } - private MaintenanceModeStatus CheckAppSettings(MaintenanceModeStatus status) { if (_maintenanceModeSettings is null or { IsInMaintenanceMode: false }) @@ -129,5 +96,8 @@ private MaintenanceModeStatus CheckAppSettings(MaintenanceModeStatus status) return status; } + + private async Task CheckStorage(MaintenanceModeStatus status) + => await _storageProvider.Read() ?? status; } } From 211401fe33bd1d7479015e2195e3f9cef59b905f Mon Sep 17 00:00:00 2001 From: Craig Agnew Date: Thu, 8 Aug 2024 09:04:10 +0100 Subject: [PATCH 02/10] Added MaintenanceModeSavedNotification to notify when status has changed for opportunity to extend. --- .../MaintenanceModeSavedNotification.cs | 17 +++++++++++++++++ .../Providers/FileSystemStorageProvider.cs | 7 +++++++ 2 files changed, 24 insertions(+) create mode 100644 Our.Umbraco.MaintenanceMode/Notifications/MaintenanceModeSavedNotification.cs diff --git a/Our.Umbraco.MaintenanceMode/Notifications/MaintenanceModeSavedNotification.cs b/Our.Umbraco.MaintenanceMode/Notifications/MaintenanceModeSavedNotification.cs new file mode 100644 index 0000000..42b92bb --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Notifications/MaintenanceModeSavedNotification.cs @@ -0,0 +1,17 @@ +using Our.Umbraco.MaintenanceMode.Models; +using Umbraco.Cms.Core.Notifications; + +namespace Our.Umbraco.MaintenanceMode.Notifications +{ + public sealed class MaintenanceModeSavedNotification : INotification + { + public bool IsInMaintenanceMode { get; } + public bool IsContentFrozen { get; } + + public MaintenanceModeSavedNotification(MaintenanceModeStatus status) + { + IsInMaintenanceMode = status.IsInMaintenanceMode; + IsContentFrozen = status.IsContentFrozen; + } + } +} diff --git a/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs index 045949e..dcc04b9 100644 --- a/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs +++ b/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Options; using Our.Umbraco.MaintenanceMode.Models; +using Our.Umbraco.MaintenanceMode.Notifications; using Serilog; using System; using System.Collections.Generic; @@ -11,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Extensions; namespace Our.Umbraco.MaintenanceMode.Providers @@ -22,13 +24,17 @@ public class FileSystemStorageProvider : IStorageProvider private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; private readonly ILogger _logger; + private readonly IEventAggregator _eventAggregator; public FileSystemStorageProvider( ILogger logger, IOptions maintenanceModeSettings, + IEventAggregator eventAggregator, IWebHostEnvironment webHostingEnvironment) { _maintenanceModeSettings = maintenanceModeSettings.Value; + _logger = logger; + _eventAggregator = eventAggregator; // put maintenanceMode config in the 'config folder' var configFolder = new DirectoryInfo(webHostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config)); @@ -69,6 +75,7 @@ public async Task Save(MaintenanceModeStatus status) string json = JsonSerializer.Serialize(status); await File.WriteAllTextAsync(_configFilePath, json); + _eventAggregator.Publish(new MaintenanceModeSavedNotification(status)); } catch (Exception ex) { From d96489832826ff38b13c638cd781fe62cbcff21a Mon Sep 17 00:00:00 2001 From: Craig Agnew Date: Mon, 12 Aug 2024 16:44:25 +0100 Subject: [PATCH 03/10] Added DatabaseStorageProvider to enable support for distributed environments. --- .../Composers/MaintenceModeComposer.cs | 6 +- .../Configurations/StorageMode.cs | 3 +- .../Controllers/MaintenanceModeController.cs | 2 +- .../Factories/IStorageProviderFactory.cs | 4 +- .../Factories/StorageProviderFactory.cs | 41 ++++++-- .../Interfaces/IMaintenanceModeService.cs | 3 - .../MaintenanceMode.cs | 7 ++ .../MaintenanceRedirectMiddleware.cs | 6 +- .../Migrations/InitialMigration.cs | 29 ++++++ .../Models/Schema/MaintenanceModeSchema.cs | 23 +++++ .../UmbracoApplicationStartingHandler.cs | 45 +++++++++ .../FreezeContentCopyingNotification.cs | 4 +- .../FreezeContentDeletingNotification.cs | 4 +- .../FreezeContentMovingNotification.cs | 4 +- ...zeContentMovingToRecycleBinNotification.cs | 4 +- .../FreezeContentPublishingNotification.cs | 4 +- .../FreezeContentSavingNotification.cs | 4 +- .../FreezeContentUnpublishingNotification.cs | 4 +- .../Media/FreezeMediaDeletingNotification.cs | 4 +- .../Media/FreezeMediaMovingNotification.cs | 4 +- ...eezeMediaMovingToRecycleBinNotification.cs | 4 +- .../Media/FreezeMediaSavingNotification.cs | 4 +- .../Providers/DatabaseStorageProvider.cs | 98 +++++++++++++++++++ .../Services/MaintenanceModeService.cs | 72 ++++++++------ 24 files changed, 309 insertions(+), 74 deletions(-) create mode 100644 Our.Umbraco.MaintenanceMode/Migrations/InitialMigration.cs create mode 100644 Our.Umbraco.MaintenanceMode/Models/Schema/MaintenanceModeSchema.cs create mode 100644 Our.Umbraco.MaintenanceMode/NotificationHandlers/Application/UmbracoApplicationStartingHandler.cs create mode 100644 Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs diff --git a/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs b/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs index ecd0958..9040301 100644 --- a/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs +++ b/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs @@ -17,7 +17,6 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Our.Umbraco.MaintenanceMode.Composers @@ -42,11 +41,12 @@ public static IUmbracoBuilder AddMaintenceManager(this IUmbracoBuilder builder) builder.Services.Configure (builder.Config.GetSection("MaintenanceMode")); - builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddSingleton() .AddSingleton(s => s.GetService()); + builder.Services.AddSingleton() + .AddSingleton(s => s.GetService()); builder.Services.AddUnique(); builder.AddNotificationHandlers(); @@ -73,6 +73,8 @@ private static void AddNotificationHandlers(this IUmbracoBuilder builder) .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler(); + + builder.AddNotificationHandler(); } } diff --git a/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs b/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs index ca7a624..3f781c2 100644 --- a/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs +++ b/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs @@ -8,6 +8,7 @@ namespace Our.Umbraco.MaintenanceMode.Configurations { public enum StorageMode { - FileSystem = 0 + FileSystem = 0, + Database = 1 } } diff --git a/Our.Umbraco.MaintenanceMode/Controllers/MaintenanceModeController.cs b/Our.Umbraco.MaintenanceMode/Controllers/MaintenanceModeController.cs index 3717452..b62a35f 100644 --- a/Our.Umbraco.MaintenanceMode/Controllers/MaintenanceModeController.cs +++ b/Our.Umbraco.MaintenanceMode/Controllers/MaintenanceModeController.cs @@ -19,7 +19,7 @@ public IActionResult Index() { Response.StatusCode = 503; - return View($"/views/{_maintenanceModeService.Settings.TemplateName}.cshtml", _maintenanceModeService.Settings.ViewModel); + return View($"/views/{_maintenanceModeService.Status.Settings.TemplateName}.cshtml", _maintenanceModeService.Status.Settings.ViewModel); } public MaintenanceModeController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider, IMaintenanceModeService maintenanceModeService) diff --git a/Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs b/Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs index 835cea1..681347c 100644 --- a/Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs +++ b/Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs @@ -1,4 +1,5 @@ -using Our.Umbraco.MaintenanceMode.Providers; +using Our.Umbraco.MaintenanceMode.Configurations; +using Our.Umbraco.MaintenanceMode.Providers; using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +10,7 @@ namespace Our.Umbraco.MaintenanceMode.Factories { public interface IStorageProviderFactory { + StorageMode StorageMode { get; } IStorageProvider GetProvider(); } } diff --git a/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs b/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs index d81ab1f..786e06a 100644 --- a/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs +++ b/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs @@ -1,32 +1,53 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Our.Umbraco.MaintenanceMode.Configurations; using Our.Umbraco.MaintenanceMode.Providers; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Umbraco.Cms.Core.Configuration.Models; namespace Our.Umbraco.MaintenanceMode.Factories { public class StorageProviderFactory : IStorageProviderFactory { private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; - + private readonly IOptions _globalSettings; private readonly IServiceProvider _serviceProvider; - public StorageProviderFactory(IOptions maintenanceModeSettings, + public StorageProviderFactory( + IOptions maintenanceModeSettings, + IOptions globalSettings, IServiceProvider serviceProvider) { _maintenanceModeSettings = maintenanceModeSettings.Value; - _serviceProvider = serviceProvider; + _serviceProvider = serviceProvider; + _globalSettings = globalSettings; } - public StorageMode StorageMode => this._maintenanceModeSettings?.StorageMode ?? StorageMode.FileSystem; + //public StorageMode StorageMode => this._maintenanceModeSettings?.StorageMode ?? StorageMode.FileSystem; + public StorageMode StorageMode + { + get + { + if (_maintenanceModeSettings?.StorageMode is not null) + { + // if it's been explicitly set in config + // this is particularly useful for testing (!) + return _maintenanceModeSettings.StorageMode; + } + else if (_globalSettings.Value.MainDomLock is not "MainDomSemaphoreLock") + { + // otherwise detect whether Umbraco is running in a distributed environment, if so we can't rely on file system/application scope + // by default Umbraco runs under the semaphore lock + // see https://docs.umbraco.com/umbraco-cms/fundamentals/setup/server-setup/load-balancing/azure-web-apps#host-synchronization + return StorageMode.Database; + } + + return StorageMode.FileSystem; + } + } public IStorageProvider GetProvider() => StorageMode switch { + StorageMode.Database => (IStorageProvider)_serviceProvider.GetService(typeof(DatabaseStorageProvider)), _ => (IStorageProvider)_serviceProvider.GetService(typeof(FileSystemStorageProvider)) }; } diff --git a/Our.Umbraco.MaintenanceMode/Interfaces/IMaintenanceModeService.cs b/Our.Umbraco.MaintenanceMode/Interfaces/IMaintenanceModeService.cs index 94db9e7..c1e2d78 100644 --- a/Our.Umbraco.MaintenanceMode/Interfaces/IMaintenanceModeService.cs +++ b/Our.Umbraco.MaintenanceMode/Interfaces/IMaintenanceModeService.cs @@ -7,9 +7,6 @@ public interface IMaintenanceModeService { Task ToggleMaintenanceMode(bool maintenanceMode); Task ToggleContentFreeze(bool isContentFrozen); - bool IsInMaintenanceMode { get; } - bool IsContentFrozen { get; } - MaintenanceModeSettings Settings { get; } MaintenanceModeStatus Status { get; } Task SaveSettings(MaintenanceModeSettings settings); } diff --git a/Our.Umbraco.MaintenanceMode/MaintenanceMode.cs b/Our.Umbraco.MaintenanceMode/MaintenanceMode.cs index a67445c..7584745 100644 --- a/Our.Umbraco.MaintenanceMode/MaintenanceMode.cs +++ b/Our.Umbraco.MaintenanceMode/MaintenanceMode.cs @@ -12,5 +12,12 @@ internal static class MaintenanceMode /// route use for maintenance requests (random name so as not to clash) /// public const string MaintenanceRoot = "e7f7581c6bcd4113954e163ff18cbaba"; + + public const string PackageAlias = "Our.Umbraco.MaintenanceMode"; + + /// + /// Identifier for accessing record in the DB, currently only setting this at global level (-1) is supported + /// + public const int MaintenanceConfigRootId = -1; } } diff --git a/Our.Umbraco.MaintenanceMode/Middleware/MaintenanceRedirectMiddleware.cs b/Our.Umbraco.MaintenanceMode/Middleware/MaintenanceRedirectMiddleware.cs index ead8ff9..c52a4d2 100644 --- a/Our.Umbraco.MaintenanceMode/Middleware/MaintenanceRedirectMiddleware.cs +++ b/Our.Umbraco.MaintenanceMode/Middleware/MaintenanceRedirectMiddleware.cs @@ -38,10 +38,10 @@ public async Task InvokeAsync(HttpContext context, IBackofficeUserAccessor backo if (backofficeUserAccessor != null) { if (_runtimeState.Level == RuntimeLevel.Run && - _maintenanceModeService.IsInMaintenanceMode) + _maintenanceModeService.Status.IsInMaintenanceMode) { // todo figure out how to do this check here - if (!_maintenanceModeService.Settings.AllowBackOfficeUsersThrough + if (!_maintenanceModeService.Status.Settings.AllowBackOfficeUsersThrough && IsAuthenticated) { context = HandleRequest(context); @@ -54,8 +54,6 @@ public async Task InvokeAsync(HttpContext context, IBackofficeUserAccessor backo } await _next(context); - - } private bool IsBackOfficeAuthenticated(IBackofficeUserAccessor backofficeUserAccessor) { diff --git a/Our.Umbraco.MaintenanceMode/Migrations/InitialMigration.cs b/Our.Umbraco.MaintenanceMode/Migrations/InitialMigration.cs new file mode 100644 index 0000000..2d33ae6 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Migrations/InitialMigration.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Options; +using Our.Umbraco.MaintenanceMode.Configurations; +using Our.Umbraco.MaintenanceMode.Models.Schema; +using Umbraco.Cms.Infrastructure.Migrations; + +namespace Our.Umbraco.MaintenanceMode.Migrations +{ + public sealed class InitialMigration : MigrationBase + { + public const string Key = "maintenance-mode-init"; + + private readonly MaintenanceModeSettings _maintenanceModeSettings; + + public InitialMigration( + IMigrationContext context, + IOptions maintenanceModeSettings + ) : base(context) + { + } + + protected override void Migrate() + { + if (!TableExists(nameof(MaintenanceModeSchema))) + { + Create.Table().Do(); + } + } + } +} diff --git a/Our.Umbraco.MaintenanceMode/Models/Schema/MaintenanceModeSchema.cs b/Our.Umbraco.MaintenanceMode/Models/Schema/MaintenanceModeSchema.cs new file mode 100644 index 0000000..2903b64 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Models/Schema/MaintenanceModeSchema.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel.DataAnnotations; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Our.Umbraco.MaintenanceMode.Models.Schema +{ + [TableName(TableName)] + [PrimaryKey(nameof(Id), AutoIncrement = false)] + [ExplicitColumns] + public class MaintenanceModeSchema + { + public const string TableName = "MaintenanceModeConfig"; + + [Column(nameof(Id))] + [PrimaryKeyColumn(AutoIncrement = false)] + public int Id { get; set; } + + [Column(nameof(Value))] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string Value { get; set; } + } +} diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Application/UmbracoApplicationStartingHandler.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Application/UmbracoApplicationStartingHandler.cs new file mode 100644 index 0000000..b5ee9f2 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Application/UmbracoApplicationStartingHandler.cs @@ -0,0 +1,45 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; +using Umbraco.Cms.Infrastructure.Migrations; +using Our.Umbraco.MaintenanceMode.Migrations; +using Umbraco.Cms.Infrastructure.Scoping; + +namespace Our.Umbraco.MaintenanceMode.NotificationHandlers.Application +{ + public class UmbracoApplicationStartingHandler : INotificationHandler + { + private readonly IScopeProvider _scopeProvider; + private readonly IMigrationPlanExecutor _migrationPlanExecutor; + private readonly IKeyValueService _keyValueService; + private readonly IRuntimeState _runtimeState; + + public UmbracoApplicationStartingHandler(IScopeProvider scopeProvider, + IMigrationPlanExecutor migrationPlanExecutor, + IKeyValueService keyValueService, + IRuntimeState runtimeState) + { + _scopeProvider = scopeProvider; + _migrationPlanExecutor = migrationPlanExecutor; + _keyValueService = keyValueService; + _runtimeState = runtimeState; + } + + public void Handle(UmbracoApplicationStartingNotification notification) + { + if (_runtimeState.Level < RuntimeLevel.Run) return; + + var plan = new MigrationPlan(MaintenanceMode.PackageAlias); + + plan.From(string.Empty) + .To(InitialMigration.Key); + + var upgrader = new Upgrader(plan); + + upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); + } + } +} diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentCopyingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentCopyingNotification.cs index 89d2580..4c867ff 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentCopyingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentCopyingNotification.cs @@ -17,11 +17,11 @@ public FreezeContentCopyingNotification(IMaintenanceModeService maintenanceModeS public void Handle(ContentCopyingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentDeletingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentDeletingNotification.cs index 14ca925..de3ee93 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentDeletingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentDeletingNotification.cs @@ -17,11 +17,11 @@ public FreezeContentDeletingNotification(IMaintenanceModeService maintenanceMode public void Handle(ContentDeletingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingNotification.cs index c8e1bb0..cb5816f 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingNotification.cs @@ -17,11 +17,11 @@ public FreezeContentMovingNotification(IMaintenanceModeService maintenanceModeSe public void Handle(ContentMovingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsInMaintenanceMode) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs index 8981d67..09c0a8c 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs @@ -17,11 +17,11 @@ public FreezeContentMovingToRecycleBinNotification(IMaintenanceModeService maint public void Handle(ContentMovingToRecycleBinNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsInMaintenanceMode) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentPublishingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentPublishingNotification.cs index 1d8d5a6..6662b6c 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentPublishingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentPublishingNotification.cs @@ -17,11 +17,11 @@ public FreezeContentPublishingNotification(IMaintenanceModeService maintenanceMo public void Handle(ContentPublishingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentSavingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentSavingNotification.cs index 82abbc8..c4cc36c 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentSavingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentSavingNotification.cs @@ -17,11 +17,11 @@ public FreezeContentSavingNotification(IMaintenanceModeService maintenanceModeSe public void Handle(ContentSavingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentUnpublishingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentUnpublishingNotification.cs index e3fa4f8..23c2d1f 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentUnpublishingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentUnpublishingNotification.cs @@ -17,11 +17,11 @@ public FreezeContentUnpublishingNotification(IMaintenanceModeService maintenance public void Handle(ContentUnpublishingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaDeletingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaDeletingNotification.cs index ae9ae78..1936772 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaDeletingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaDeletingNotification.cs @@ -17,11 +17,11 @@ public FreezeMediaDeletingNotification(IMaintenanceModeService maintenanceModeSe public void Handle(MediaDeletingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaMovingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaMovingNotification.cs index 42329ce..bc423af 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaMovingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaMovingNotification.cs @@ -17,11 +17,11 @@ public FreezeMediaMovingNotification(IMaintenanceModeService maintenanceModeServ public void Handle(MediaMovingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaMovingToRecycleBinNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaMovingToRecycleBinNotification.cs index fe69d93..88edc98 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaMovingToRecycleBinNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaMovingToRecycleBinNotification.cs @@ -17,11 +17,11 @@ public FreezeMediaMovingToRecycleBinNotification(IMaintenanceModeService mainten public void Handle(MediaMovingToRecycleBinNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaSavingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaSavingNotification.cs index 0a9081e..a97afff 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaSavingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Media/FreezeMediaSavingNotification.cs @@ -17,11 +17,11 @@ public FreezeMediaSavingNotification(IMaintenanceModeService maintenanceModeServ public void Handle(MediaSavingNotification notification) { - if (_maintenanceModeService.IsContentFrozen) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; - if (_maintenanceModeService.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; + if (_maintenanceModeService.Status.Settings.UnfrozenUsers.Contains(_backofficeUserAccessor.BackofficeUser.GetId().ToString())) return; notification.CancelOperation(new EventMessage("Warning", "This site is currently frozen during updates", EventMessageType.Error)); diff --git a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs new file mode 100644 index 0000000..1aa5098 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Options; +using Our.Umbraco.MaintenanceMode.Models; +using Our.Umbraco.MaintenanceMode.Models.Schema; +using Serilog; +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.Scoping; + +namespace Our.Umbraco.MaintenanceMode.Providers +{ + public class DatabaseStorageProvider : IStorageProvider + { + private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; + + private readonly ILogger _logger; + private readonly IEventAggregator _eventAggregator; + private readonly IScopeProvider _scopeProvider; + + public DatabaseStorageProvider( + ILogger logger, + IOptions maintenanceModeSettings, + IEventAggregator eventAggregator, + IScopeProvider scope) + { + _maintenanceModeSettings = maintenanceModeSettings.Value; + _logger = logger; + _eventAggregator = eventAggregator; + _scopeProvider = scope; + } + + public async Task Read() + { + try + { + // read from the database + using var scope = _scopeProvider.CreateScope(); + + var db = scope.Database; + var dbStatus = await db.QueryAsync() + .Where(x => x.Id == MaintenanceMode.MaintenanceConfigRootId) + .FirstOrDefault(); + + if (dbStatus is null) + { + _logger.Warning("Maintenance mode status table was queried but is empty."); + return null; + } + + var status = JsonSerializer.Deserialize(dbStatus.Value); + + scope.Complete(); + + return status; + + } + catch (Exception ex) + { + _logger.Warning(ex, string.Concat("Failed to load Status ", ex.Message)); + } + + return null; + } + + public async Task Save(MaintenanceModeStatus status) + { + try + { + string json = JsonSerializer.Serialize(status); + + using var scope = _scopeProvider.CreateScope(); + + var db = scope.Database; + + var dbStatus = await db.QueryAsync() + .Where(x => x.Id == MaintenanceMode.MaintenanceConfigRootId) + .FirstOrDefault(); + + if (dbStatus is null) + { + await db.InsertAsync(new MaintenanceModeSchema() { Id = MaintenanceMode.MaintenanceConfigRootId, Value = json }); + } + else + { + dbStatus.Value = json; + await db.UpdateAsync(dbStatus); + } + + scope.Complete(); + } + catch (Exception ex) + { + _logger.Debug(ex, string.Concat("Failed to save config ", ex.Message)); + } + } + } +} diff --git a/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs b/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs index ab964f8..eb446bf 100644 --- a/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs +++ b/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs @@ -1,87 +1,99 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; + +using Our.Umbraco.MaintenanceMode.Configurations; using Our.Umbraco.MaintenanceMode.Factories; using Our.Umbraco.MaintenanceMode.Interfaces; using Our.Umbraco.MaintenanceMode.Models; using Our.Umbraco.MaintenanceMode.Providers; + using Serilog; -using System; -using System.IO; -using System.Text.Json; using System.Threading.Tasks; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Extensions; -using Umbraco.Cms.Core.Hosting; - namespace Our.Umbraco.MaintenanceMode.Services { public class MaintenanceModeService : IMaintenanceModeService { private readonly ILogger _logger; - private readonly IStorageProvider _storageProvider; + private readonly IStorageProviderFactory _storageProviderFactory; private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; private readonly string _configFilePath; - public MaintenanceModeStatus Status { get; private set; } + private MaintenanceModeStatus TrackedStatus { get; set; } + + public MaintenanceModeStatus Status + { + get + { + // when in 'Database' storage mode we want to be fetching every time as this + // typically will mean Umbraco is deployed in a distributed environment therefore + // status can't be tracked in scope, it needs to be read from storage each time + return _storageProviderFactory.StorageMode switch + { + StorageMode.Database => StorageProvider.Read().Result, + _ => TrackedStatus + }; + } + } public MaintenanceModeService(ILogger logger, IOptions maintenanceModeSettings, - IWebHostEnvironment webHostingEnvironment, IStorageProviderFactory storageProviderFactory) { _logger = logger; - _storageProvider = storageProviderFactory.GetProvider(); + _storageProviderFactory = storageProviderFactory; _maintenanceModeSettings = maintenanceModeSettings.Value; - Status = LoadStatus().Result; + TrackedStatus = LoadStatus().Result; } - public bool IsInMaintenanceMode => Status.IsInMaintenanceMode; - - public bool IsContentFrozen => Status.IsContentFrozen; - - public MaintenanceModeSettings Settings => Status.Settings; + public IStorageProvider StorageProvider => _storageProviderFactory.GetProvider(); public async Task ToggleMaintenanceMode(bool maintenanceMode) { - if (maintenanceMode == Status.IsInMaintenanceMode) + // checking against TrackedStatus is fine even in distributed environments + // the toggle will have been executed on the SchedulingPublisher app + if (maintenanceMode == TrackedStatus.IsInMaintenanceMode) return; // already in this state - Status.IsInMaintenanceMode = maintenanceMode; - await _storageProvider.Save(Status); + TrackedStatus.IsInMaintenanceMode = maintenanceMode; + await StorageProvider.Save(TrackedStatus); } public async Task ToggleContentFreeze(bool isContentFrozen) { - if (isContentFrozen == Status.IsContentFrozen) + // checking against TrackedStatus is fine even in distributed environments + // the toggle will have been executed on the SchedulingPublisher app + if (isContentFrozen == TrackedStatus.IsContentFrozen) return; // already in this state - Status.IsContentFrozen = isContentFrozen; - await _storageProvider.Save(Status); + TrackedStatus.IsContentFrozen = isContentFrozen; + await StorageProvider.Save(TrackedStatus); } - public async Task SaveSettings(MaintenanceModeSettings settings) + public async Task SaveSettings(Models.MaintenanceModeSettings settings) { - Status.Settings = settings; - await _storageProvider.Save(Status); + TrackedStatus.Settings = settings; + await StorageProvider.Save(TrackedStatus); } private async Task LoadStatus() { + // fallback - defaults set in the service code var maintenanceModeStatus = new MaintenanceModeStatus { IsInMaintenanceMode = false, UsingWebConfig = false, - Settings = new MaintenanceModeSettings + Settings = new Models.MaintenanceModeSettings { ViewModel = new Models.MaintenanceMode() } }; + // read from the storage location, if available maintenanceModeStatus = await CheckStorage(maintenanceModeStatus); + // override from appsettings, if applicable return CheckAppSettings(maintenanceModeStatus); } @@ -98,6 +110,6 @@ private MaintenanceModeStatus CheckAppSettings(MaintenanceModeStatus status) } private async Task CheckStorage(MaintenanceModeStatus status) - => await _storageProvider.Read() ?? status; + => await StorageProvider.Read() ?? status; } } From 3a9eb4dd4fb438f425fe2d52a03e444d95e47aa0 Mon Sep 17 00:00:00 2001 From: Craig Agnew Date: Mon, 12 Aug 2024 16:50:51 +0100 Subject: [PATCH 04/10] Updated README for load balanced environments. --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 2d4b6ba..1d10baf 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,14 @@ Put Umbraco Into Maintenance Mode - While you do things Simple dashboard to allow you to flick your Umbraco site in and out of Maintenance Mode. +## Load balanced environments +`Our.Umbraco.MaintenanceMode` supports single web instances immediately after installing. However, when deploying in a load balanced environment additional configuration is required. +```json + "MaintenanceMode": { + "StorageMode": "Database" + } +``` + +This will persist the maintenance mode configuration settings to your Umbraco database and read from there in your content delivery instances. \ No newline at end of file From 864128674f6052ca062aac1b74db1523dfdaf479 Mon Sep 17 00:00:00 2001 From: Craig Agnew Date: Mon, 12 Aug 2024 20:43:36 +0100 Subject: [PATCH 05/10] Updated DatabaseStorageProvider to raise notification on save. --- .../Providers/DatabaseStorageProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs index 1aa5098..05219b5 100644 --- a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs +++ b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using Our.Umbraco.MaintenanceMode.Models; using Our.Umbraco.MaintenanceMode.Models.Schema; +using Our.Umbraco.MaintenanceMode.Notifications; using Serilog; using System; using System.Text.Json; @@ -88,6 +89,8 @@ public async Task Save(MaintenanceModeStatus status) } scope.Complete(); + + _eventAggregator.Publish(new MaintenanceModeSavedNotification(status)); } catch (Exception ex) { From f9c55dfb6eebdd7f350de515e4a03115c015dbae Mon Sep 17 00:00:00 2001 From: henryjump <140804846+henryjump@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:50:45 +0100 Subject: [PATCH 06/10] use role to work out load balancing --- .../Factories/StorageProviderFactory.cs | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs b/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs index 786e06a..f0564ae 100644 --- a/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs +++ b/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs @@ -1,25 +1,25 @@ -using Microsoft.Extensions.Options; +using System; +using Microsoft.Extensions.Options; using Our.Umbraco.MaintenanceMode.Configurations; using Our.Umbraco.MaintenanceMode.Providers; -using System; -using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Sync; namespace Our.Umbraco.MaintenanceMode.Factories { public class StorageProviderFactory : IStorageProviderFactory { private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; - private readonly IOptions _globalSettings; private readonly IServiceProvider _serviceProvider; + private readonly IServerRoleAccessor _serverRoleAccessor; public StorageProviderFactory( IOptions maintenanceModeSettings, - IOptions globalSettings, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + IServerRoleAccessor serverRoleAccessor) { _maintenanceModeSettings = maintenanceModeSettings.Value; - _serviceProvider = serviceProvider; - _globalSettings = globalSettings; + _serviceProvider = serviceProvider; + _serverRoleAccessor = serverRoleAccessor; } //public StorageMode StorageMode => this._maintenanceModeSettings?.StorageMode ?? StorageMode.FileSystem; @@ -27,21 +27,12 @@ public StorageMode StorageMode { get { - if (_maintenanceModeSettings?.StorageMode is not null) + return _maintenanceModeSettings?.StorageMode ?? _serverRoleAccessor.CurrentServerRole switch { - // if it's been explicitly set in config - // this is particularly useful for testing (!) - return _maintenanceModeSettings.StorageMode; - } - else if (_globalSettings.Value.MainDomLock is not "MainDomSemaphoreLock") - { - // otherwise detect whether Umbraco is running in a distributed environment, if so we can't rely on file system/application scope - // by default Umbraco runs under the semaphore lock - // see https://docs.umbraco.com/umbraco-cms/fundamentals/setup/server-setup/load-balancing/azure-web-apps#host-synchronization - return StorageMode.Database; - } - - return StorageMode.FileSystem; + // check server role to see if umbraco thinks it's load balanced + ServerRole.Subscriber or ServerRole.SchedulingPublisher => StorageMode.Database, + _ => StorageMode.FileSystem, + }; } } From 906e916a58a15e83a0c3d09405c2ef9c99509afb Mon Sep 17 00:00:00 2001 From: henryjump <140804846+henryjump@users.noreply.github.com> Date: Thu, 15 Aug 2024 14:58:11 +0100 Subject: [PATCH 07/10] Fixed notifications fixed moving and recycling to check for IsContentFrozen instead of IsInMaintenanceMode --- .../Content/FreezeContentMovingNotification.cs | 2 +- .../Content/FreezeContentMovingToRecycleBinNotification.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingNotification.cs index cb5816f..981e0fc 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingNotification.cs @@ -17,7 +17,7 @@ public FreezeContentMovingNotification(IMaintenanceModeService maintenanceModeSe public void Handle(ContentMovingNotification notification) { - if (_maintenanceModeService.Status.IsInMaintenanceMode) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; diff --git a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs index 09c0a8c..e7f0d56 100644 --- a/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs +++ b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs @@ -17,7 +17,7 @@ public FreezeContentMovingToRecycleBinNotification(IMaintenanceModeService maint public void Handle(ContentMovingToRecycleBinNotification notification) { - if (_maintenanceModeService.Status.IsInMaintenanceMode) + if (_maintenanceModeService.Status.IsContentFrozen) { if (_backofficeUserAccessor.BackofficeUser == null) return; From ca7b3cd64f3c9fb9bd13fa4fcc180a360fb9fdb8 Mon Sep 17 00:00:00 2001 From: henryjump <140804846+henryjump@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:02:23 +0100 Subject: [PATCH 08/10] handle no table at startup makes sure code works on first boot before migration is ran to create table --- .../Providers/DatabaseStorageProvider.cs | 7 +++++ .../Services/MaintenanceModeService.cs | 27 +++++++++---------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs index 05219b5..babc69e 100644 --- a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs +++ b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs @@ -39,6 +39,13 @@ public async Task Read() using var scope = _scopeProvider.CreateScope(); var db = scope.Database; + + //check for table reduces log warnings + if (!scope.SqlContext.SqlSyntax.DoesTableExist(db, MaintenanceModeSchema.TableName)) + { + return null; + } + var dbStatus = await db.QueryAsync() .Where(x => x.Id == MaintenanceMode.MaintenanceConfigRootId) .FirstOrDefault(); diff --git a/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs b/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs index eb446bf..5476a93 100644 --- a/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs +++ b/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs @@ -30,7 +30,7 @@ public MaintenanceModeStatus Status // status can't be tracked in scope, it needs to be read from storage each time return _storageProviderFactory.StorageMode switch { - StorageMode.Database => StorageProvider.Read().Result, + StorageMode.Database => GetFromStorageOrDefault().Result, _ => TrackedStatus }; } @@ -77,21 +77,20 @@ public async Task SaveSettings(Models.MaintenanceModeSettings settings) await StorageProvider.Save(TrackedStatus); } - private async Task LoadStatus() + private static MaintenanceModeStatus _defaultStatus = new MaintenanceModeStatus { - // fallback - defaults set in the service code - var maintenanceModeStatus = new MaintenanceModeStatus + IsInMaintenanceMode = false, + UsingWebConfig = false, + Settings = new Models.MaintenanceModeSettings { - IsInMaintenanceMode = false, - UsingWebConfig = false, - Settings = new Models.MaintenanceModeSettings - { - ViewModel = new Models.MaintenanceMode() - } - }; + ViewModel = new Models.MaintenanceMode() + } + }; + private async Task LoadStatus() + { // read from the storage location, if available - maintenanceModeStatus = await CheckStorage(maintenanceModeStatus); + var maintenanceModeStatus = await GetFromStorageOrDefault(); // override from appsettings, if applicable return CheckAppSettings(maintenanceModeStatus); @@ -109,7 +108,7 @@ private MaintenanceModeStatus CheckAppSettings(MaintenanceModeStatus status) return status; } - private async Task CheckStorage(MaintenanceModeStatus status) - => await StorageProvider.Read() ?? status; + private async Task GetFromStorageOrDefault() + => await StorageProvider.Read() ?? _defaultStatus; } } From b117e6ee382405e1994cef4be8cc81fee2b02be6 Mon Sep 17 00:00:00 2001 From: henryjump <140804846+henryjump@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:13:07 +0100 Subject: [PATCH 09/10] adds cache on database lookups --- .../Configurations/MaintenanceModeSettings.cs | 1 + .../Providers/DatabaseStorageProvider.cs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs b/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs index ae045ed..2179ef7 100644 --- a/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs +++ b/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs @@ -5,5 +5,6 @@ public class MaintenanceModeSettings public bool IsInMaintenanceMode { get; set; } public bool IsContentFrozen { get; set; } public StorageMode StorageMode { get; set; } + public int CacheSeconds { get; set; } = 30; } } diff --git a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs index babc69e..b7cced7 100644 --- a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs +++ b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs @@ -18,6 +18,9 @@ public class DatabaseStorageProvider : IStorageProvider private readonly ILogger _logger; private readonly IEventAggregator _eventAggregator; private readonly IScopeProvider _scopeProvider; + private static DateTime _lastChecked; + private static MaintenanceModeStatus _lastKnownStatus; + private TimeSpan _timeBetweenChecks; public DatabaseStorageProvider( ILogger logger, @@ -29,12 +32,20 @@ public DatabaseStorageProvider( _logger = logger; _eventAggregator = eventAggregator; _scopeProvider = scope; + _timeBetweenChecks = TimeSpan.FromSeconds(_maintenanceModeSettings.CacheSeconds); } public async Task Read() { try { + if (DateTime.UtcNow - _lastChecked <= _timeBetweenChecks) + { + return _lastKnownStatus; + } + + _lastChecked = DateTime.UtcNow; + // read from the database using var scope = _scopeProvider.CreateScope(); @@ -56,11 +67,11 @@ public async Task Read() return null; } - var status = JsonSerializer.Deserialize(dbStatus.Value); + _lastKnownStatus = JsonSerializer.Deserialize(dbStatus.Value); scope.Complete(); - return status; + return _lastKnownStatus; } catch (Exception ex) @@ -97,6 +108,8 @@ public async Task Save(MaintenanceModeStatus status) scope.Complete(); + _lastChecked = DateTime.MinValue; + _eventAggregator.Publish(new MaintenanceModeSavedNotification(status)); } catch (Exception ex) From 3c2de81f8b9ab14f0550068d85ce6d274c7c3ffe Mon Sep 17 00:00:00 2001 From: henryjump <140804846+henryjump@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:23:01 +0100 Subject: [PATCH 10/10] Renamed CacheSeconds to WaitTimeBetweenDatabaseCalls --- .../Configurations/MaintenanceModeSettings.cs | 2 +- .../Providers/DatabaseStorageProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs b/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs index 2179ef7..4e93eb2 100644 --- a/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs +++ b/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs @@ -5,6 +5,6 @@ public class MaintenanceModeSettings public bool IsInMaintenanceMode { get; set; } public bool IsContentFrozen { get; set; } public StorageMode StorageMode { get; set; } - public int CacheSeconds { get; set; } = 30; + public int WaitTimeBetweenDatabaseCalls { get; set; } = 30; } } diff --git a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs index b7cced7..c8a1680 100644 --- a/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs +++ b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs @@ -32,7 +32,7 @@ public DatabaseStorageProvider( _logger = logger; _eventAggregator = eventAggregator; _scopeProvider = scope; - _timeBetweenChecks = TimeSpan.FromSeconds(_maintenanceModeSettings.CacheSeconds); + _timeBetweenChecks = TimeSpan.FromSeconds(_maintenanceModeSettings.WaitTimeBetweenDatabaseCalls); } public async Task Read()