diff --git a/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs b/Our.Umbraco.MaintenanceMode/Composers/MaintenceModeComposer.cs index f274c14..9040301 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; @@ -38,8 +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(); @@ -66,6 +73,8 @@ private static void AddNotificationHandlers(this IUmbracoBuilder builder) .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler(); + + builder.AddNotificationHandler(); } } diff --git a/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs b/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs index 5810e31..4e93eb2 100644 --- a/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs +++ b/Our.Umbraco.MaintenanceMode/Configurations/MaintenanceModeSettings.cs @@ -4,5 +4,7 @@ public class MaintenanceModeSettings { public bool IsInMaintenanceMode { get; set; } public bool IsContentFrozen { get; set; } + public StorageMode StorageMode { get; set; } + public int WaitTimeBetweenDatabaseCalls { get; set; } = 30; } } diff --git a/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs b/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs new file mode 100644 index 0000000..3f781c2 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Configurations/StorageMode.cs @@ -0,0 +1,14 @@ +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, + 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 new file mode 100644 index 0000000..681347c --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Factories/IStorageProviderFactory.cs @@ -0,0 +1,16 @@ +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 interface IStorageProviderFactory + { + StorageMode StorageMode { get; } + IStorageProvider GetProvider(); + } +} diff --git a/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs b/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs new file mode 100644 index 0000000..f0564ae --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Factories/StorageProviderFactory.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.Extensions.Options; +using Our.Umbraco.MaintenanceMode.Configurations; +using Our.Umbraco.MaintenanceMode.Providers; +using Umbraco.Cms.Core.Sync; + +namespace Our.Umbraco.MaintenanceMode.Factories +{ + public class StorageProviderFactory : IStorageProviderFactory + { + private readonly Configurations.MaintenanceModeSettings _maintenanceModeSettings; + private readonly IServiceProvider _serviceProvider; + private readonly IServerRoleAccessor _serverRoleAccessor; + + public StorageProviderFactory( + IOptions maintenanceModeSettings, + IServiceProvider serviceProvider, + IServerRoleAccessor serverRoleAccessor) + { + _maintenanceModeSettings = maintenanceModeSettings.Value; + _serviceProvider = serviceProvider; + _serverRoleAccessor = serverRoleAccessor; + } + + //public StorageMode StorageMode => this._maintenanceModeSettings?.StorageMode ?? StorageMode.FileSystem; + public StorageMode StorageMode + { + get + { + return _maintenanceModeSettings?.StorageMode ?? _serverRoleAccessor.CurrentServerRole switch + { + // check server role to see if umbraco thinks it's load balanced + ServerRole.Subscriber or ServerRole.SchedulingPublisher => StorageMode.Database, + _ => 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..981e0fc 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.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/FreezeContentMovingToRecycleBinNotification.cs b/Our.Umbraco.MaintenanceMode/NotificationHandlers/Content/FreezeContentMovingToRecycleBinNotification.cs index 8981d67..e7f0d56 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.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/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/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/DatabaseStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs new file mode 100644 index 0000000..c8a1680 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Providers/DatabaseStorageProvider.cs @@ -0,0 +1,121 @@ +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; +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; + private static DateTime _lastChecked; + private static MaintenanceModeStatus _lastKnownStatus; + private TimeSpan _timeBetweenChecks; + + public DatabaseStorageProvider( + ILogger logger, + IOptions maintenanceModeSettings, + IEventAggregator eventAggregator, + IScopeProvider scope) + { + _maintenanceModeSettings = maintenanceModeSettings.Value; + _logger = logger; + _eventAggregator = eventAggregator; + _scopeProvider = scope; + _timeBetweenChecks = TimeSpan.FromSeconds(_maintenanceModeSettings.WaitTimeBetweenDatabaseCalls); + } + + public async Task Read() + { + try + { + if (DateTime.UtcNow - _lastChecked <= _timeBetweenChecks) + { + return _lastKnownStatus; + } + + _lastChecked = DateTime.UtcNow; + + // read from the database + 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(); + + if (dbStatus is null) + { + _logger.Warning("Maintenance mode status table was queried but is empty."); + return null; + } + + _lastKnownStatus = JsonSerializer.Deserialize(dbStatus.Value); + + scope.Complete(); + + return _lastKnownStatus; + + } + 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(); + + _lastChecked = DateTime.MinValue; + + _eventAggregator.Publish(new MaintenanceModeSavedNotification(status)); + } + catch (Exception ex) + { + _logger.Debug(ex, string.Concat("Failed to save config ", ex.Message)); + } + } + } +} diff --git a/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs b/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs new file mode 100644 index 0000000..dcc04b9 --- /dev/null +++ b/Our.Umbraco.MaintenanceMode/Providers/FileSystemStorageProvider.cs @@ -0,0 +1,86 @@ +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; +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.Events; +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; + 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)); + _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); + _eventAggregator.Publish(new MaintenanceModeSavedNotification(status)); + } + 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..5476a93 100644 --- a/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs +++ b/Our.Umbraco.MaintenanceMode/Services/MaintenanceModeService.cs @@ -1,121 +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 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 => GetFromStorageOrDefault().Result, + _ => TrackedStatus + }; + } + } public MaintenanceModeService(ILogger logger, - IOptions maintenanceModeSettings, IWebHostEnvironment webHostingEnvironment) + IOptions maintenanceModeSettings, + IStorageProviderFactory storageProviderFactory) { _logger = logger; + _storageProviderFactory = storageProviderFactory; _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; + 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 SaveToDisk(); + 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 SaveToDisk(); + TrackedStatus.IsContentFrozen = isContentFrozen; + await StorageProvider.Save(TrackedStatus); } - public async Task SaveSettings(MaintenanceModeSettings settings) + public async Task SaveSettings(Models.MaintenanceModeSettings settings) { - Status.Settings = settings; - await SaveToDisk(); + TrackedStatus.Settings = settings; + await StorageProvider.Save(TrackedStatus); } - private async Task LoadStatus() + private static MaintenanceModeStatus _defaultStatus = new MaintenanceModeStatus { - var maintenanceModeStatus = new MaintenanceModeStatus - { - IsInMaintenanceMode = false, - UsingWebConfig = false, - Settings = new MaintenanceModeSettings - { - ViewModel = new Models.MaintenanceMode() - } - }; - - if (_configFilePath != null && File.Exists(_configFilePath)) + IsInMaintenanceMode = false, + UsingWebConfig = false, + Settings = new Models.MaintenanceModeSettings { - // 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)); - } + ViewModel = new Models.MaintenanceMode() } + }; - return CheckAppSettings(maintenanceModeStatus); - } - - private async Task SaveToDisk() + private async Task LoadStatus() { - try - { - if (File.Exists(_configFilePath)) - File.Delete(_configFilePath); + // read from the storage location, if available + var maintenanceModeStatus = await GetFromStorageOrDefault(); - 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)); - } + // override from appsettings, if applicable + return CheckAppSettings(maintenanceModeStatus); } private MaintenanceModeStatus CheckAppSettings(MaintenanceModeStatus status) @@ -129,5 +107,8 @@ private MaintenanceModeStatus CheckAppSettings(MaintenanceModeStatus status) return status; } + + private async Task GetFromStorageOrDefault() + => await StorageProvider.Read() ?? _defaultStatus; } } 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