diff --git a/src/Umbraco.Core/IO/CleanFolderResult.cs b/src/Umbraco.Core/IO/CleanFolderResult.cs new file mode 100644 index 0000000000..b85705ea83 --- /dev/null +++ b/src/Umbraco.Core/IO/CleanFolderResult.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; + +namespace Umbraco.Core.IO +{ + public enum CleanFolderResultStatus + { + Success, + FailedAsDoesNotExist, + FailedWithException + } + + public class CleanFolderResult + { + private CleanFolderResult() + { + } + + public CleanFolderResultStatus Status { get; set; } + + public Exception Exception { get; set; } + + public FileInfo ErroringFile { get; set; } + + public static CleanFolderResult Success() + { + return new CleanFolderResult { Status = CleanFolderResultStatus.Success }; + } + + public static CleanFolderResult FailedAsDoesNotExist() + { + return new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist }; + } + + public static CleanFolderResult FailedWithException(Exception exception, FileInfo erroringFile) + { + return new CleanFolderResult + { + Status = CleanFolderResultStatus.FailedWithException, + Exception = exception, + ErroringFile = erroringFile, + }; + } + } +} diff --git a/src/Umbraco.Core/IO/IIOHelper.cs b/src/Umbraco.Core/IO/IIOHelper.cs index 42ca804f44..510e45890f 100644 --- a/src/Umbraco.Core/IO/IIOHelper.cs +++ b/src/Umbraco.Core/IO/IIOHelper.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.IO; +using Umbraco.Core.Hosting; namespace Umbraco.Core.IO { @@ -53,5 +56,19 @@ namespace Umbraco.Core.IO /// string GetRelativePath(string path); + /// + /// Retrieves array of temporary folders from the hosting environment. + /// + /// Array of instances. + DirectoryInfo[] GetTempFolders(); + + /// + /// Cleans contents of a folder. + /// + /// Folder to clean. + /// Age of files within folder to delete. + /// Result of operation + CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age); + } } diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index b3ca956733..260c6923ed 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -188,5 +188,60 @@ namespace Umbraco.Core.IO return PathUtility.EnsurePathIsApplicationRootPrefixed(path); } + /// + /// Retrieves array of temporary folders from the hosting environment. + /// + /// Array of instances. + public DirectoryInfo[] GetTempFolders() + { + var tempFolderPaths = new[] + { + _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads) + }; + + foreach (var tempFolderPath in tempFolderPaths) + { + // Ensure it exists + Directory.CreateDirectory(tempFolderPath); + } + + return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray(); + } + + /// + /// Cleans contents of a folder. + /// + /// Folder to clean. + /// Age of files within folder to delete. + /// Result of operation + public CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age) + { + folder.Refresh(); // In case it's changed during runtime. + + if (!folder.Exists) + { + return CleanFolderResult.FailedAsDoesNotExist(); + } + + var files = folder.GetFiles("*.*", SearchOption.AllDirectories); + foreach (var file in files) + { + if (DateTime.UtcNow - file.LastWriteTimeUtc > age) + { + try + { + file.IsReadOnly = false; + file.Delete(); + } + catch (Exception ex) + { + return CleanFolderResult.FailedWithException(ex, file); + } + } + } + + return CleanFolderResult.Success(); + } + } } diff --git a/src/Umbraco.Core/Scheduling/TempFileCleanup.cs b/src/Umbraco.Core/Scheduling/TempFileCleanup.cs deleted file mode 100644 index 1a8ece78d1..0000000000 --- a/src/Umbraco.Core/Scheduling/TempFileCleanup.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Microsoft.Extensions.Logging; - -namespace Umbraco.Web.Scheduling -{ - /// - /// Used to cleanup temporary file locations - /// - public class TempFileCleanup : RecurringTaskBase - { - private readonly DirectoryInfo[] _tempFolders; - private readonly TimeSpan _age; - private readonly IMainDom _mainDom; - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - - public TempFileCleanup(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - IEnumerable tempFolders, TimeSpan age, - IMainDom mainDom, IProfilingLogger profilingLogger, ILogger logger) - : base(runner, delayMilliseconds, periodMilliseconds) - { - //SystemDirectories.TempFileUploads - - _tempFolders = tempFolders.ToArray(); - _age = age; - _mainDom = mainDom; - _profilingLogger = profilingLogger; - _logger = logger; - } - - public override bool PerformRun() - { - // ensure we do not run if not main domain - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return false; // do NOT repeat, going down - } - - foreach (var dir in _tempFolders) - CleanupFolder(dir); - - return true; //repeat - } - - private void CleanupFolder(DirectoryInfo dir) - { - dir.Refresh(); //in case it's changed during runtime - if (!dir.Exists) - { - _logger.LogDebug("The cleanup folder doesn't exist {Folder}", dir.FullName); - return; - } - - var files = dir.GetFiles("*.*", SearchOption.AllDirectories); - foreach (var file in files) - { - if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) - { - try - { - file.IsReadOnly = false; - file.Delete(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName); - } - } - } - } - - public override bool IsAsync => false; - } -} diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs new file mode 100644 index 0000000000..5b5feca029 --- /dev/null +++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Core.IO; + +namespace Umbraco.Infrastructure.HostedServices +{ + /// + /// Used to cleanup temporary file locations. + /// + /// + /// Will run on all servers - even though file upload should only be handled on the master, this will + /// ensure that in the case it happes on replicas that they are cleaned up too. + /// + public class TempFileCleanup : RecurringHostedServiceBase + { + private readonly IIOHelper _ioHelper; + private readonly IMainDom _mainDom; + private readonly ILogger _logger; + + private readonly DirectoryInfo[] _tempFolders; + private readonly TimeSpan _age = TimeSpan.FromDays(1); + + public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger) + : base(TimeSpan.FromMinutes(60), DefaultDelay) + { + _ioHelper = ioHelper; + _mainDom = mainDom; + _logger = logger; + + _tempFolders = _ioHelper.GetTempFolders(); + } + + public override async void ExecuteAsync(object state) + { + // Ensure we do not run if not main domain + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return; + } + + foreach (var folder in _tempFolders) + { + CleanupFolder(folder); + } + } + + private void CleanupFolder(DirectoryInfo folder) + { + var result = _ioHelper.CleanFolder(folder, _age); + switch (result.Status) + { + case CleanFolderResultStatus.FailedAsDoesNotExist: + _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); + break; + case CleanFolderResultStatus.FailedWithException: + _logger.LogError(result.Exception, "Could not delete temp file {FileName}", result.ErroringFile.FullName); + break; + } + + folder.Refresh(); // In case it's changed during runtime + if (!folder.Exists) + { + _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); + return; + } + + var files = folder.GetFiles("*.*", SearchOption.AllDirectories); + foreach (var file in files) + { + if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) + { + try + { + file.IsReadOnly = false; + file.Delete(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName); + } + } + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs index af7ca33256..513b2abd89 100644 --- a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.Collections.Generic; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,8 +18,6 @@ namespace Umbraco.Web.Scheduling { private const int DefaultDelayMilliseconds = 180000; // 3 mins private const int OneMinuteMilliseconds = 60000; - private const int FiveMinuteMilliseconds = 300000; - private const int OneHourMilliseconds = 3600000; private readonly IRuntimeState _runtime; private readonly IMainDom _mainDom; @@ -43,7 +38,6 @@ namespace Umbraco.Web.Scheduling private BackgroundTaskRunner _publishingRunner; private BackgroundTaskRunner _scrubberRunner; - private BackgroundTaskRunner _fileCleanupRunner; private bool _started; private object _locker = new object(); @@ -82,7 +76,6 @@ namespace Umbraco.Web.Scheduling // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing", logger, _applicationShutdownRegistry); _scrubberRunner = new BackgroundTaskRunner("LogScrubber", logger, _applicationShutdownRegistry); - _fileCleanupRunner = new BackgroundTaskRunner("TempFileCleanup", logger, _applicationShutdownRegistry); // we will start the whole process when a successful request is made _requestAccessor.RouteAttempt += RegisterBackgroundTasksOnce; @@ -115,7 +108,6 @@ namespace Umbraco.Web.Scheduling tasks.Add(RegisterScheduledPublishing()); tasks.Add(RegisterLogScrubber(_loggingSettings)); - tasks.Add(RegisterTempFileCleanup()); return tasks.ToArray(); }); @@ -138,29 +130,5 @@ namespace Umbraco.Web.Scheduling _scrubberRunner.TryAdd(task); return task; } - - private IBackgroundTask RegisterTempFileCleanup() - { - - var tempFolderPaths = new[] - { - _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads) - }; - - foreach (var tempFolderPath in tempFolderPaths) - { - //ensure it exists - Directory.CreateDirectory(tempFolderPath); - } - - // temp file cleanup, will run on all servers - even though file upload should only be handled on the master, this will - // ensure that in the case it happes on replicas that they are cleaned up. - var task = new TempFileCleanup(_fileCleanupRunner, DefaultDelayMilliseconds, OneHourMilliseconds, - tempFolderPaths.Select(x=>new DirectoryInfo(x)), - TimeSpan.FromDays(1), //files that are over a day old - _mainDom, _profilingLogger, _loggerFactory.CreateLogger()); - _scrubberRunner.TryAdd(task); - return task; - } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/TempFileCleanupTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/TempFileCleanupTests.cs new file mode 100644 index 0000000000..bdebd3055c --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/TempFileCleanupTests.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Infrastructure.HostedServices; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices +{ + [TestFixture] + public class TempFileCleanupTests + { + private Mock _mockIOHelper; + private string _testPath = @"c:\test\temp\path"; + + [Test] + public void Does_Not_Execute_When_Not_Main_Dom() + { + var sut = CreateTempFileCleanup(isMainDom: false); + sut.ExecuteAsync(null); + VerifyFilesNotCleaned(); + } + + [Test] + public void Executes_And_Cleans_Files() + { + var sut = CreateTempFileCleanup(); + sut.ExecuteAsync(null); + VerifyFilesCleaned(); + } + + private TempFileCleanup CreateTempFileCleanup(bool isMainDom = true) + { + var mockMainDom = new Mock(); + mockMainDom.SetupGet(x => x.IsMainDom).Returns(isMainDom); + + _mockIOHelper = new Mock(); + _mockIOHelper.Setup(x => x.GetTempFolders()).Returns(new DirectoryInfo[] { new DirectoryInfo(_testPath) }); + + var mockLogger = new Mock>(); + var mockProfilingLogger = new Mock(); + + return new TempFileCleanup(_mockIOHelper.Object, mockMainDom.Object, mockLogger.Object); + } + + private void VerifyFilesNotCleaned() + { + VerifyFilesCleaned(Times.Never()); + } + + private void VerifyFilesCleaned() + { + VerifyFilesCleaned(Times.Once()); + } + + private void VerifyFilesCleaned(Times times) + { + _mockIOHelper.Verify(x => x.CleanFolder(It.Is(y => y.FullName == _testPath), It.IsAny()), times); + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 2f82f86ba4..ec5dbd2781 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -294,6 +294,7 @@ namespace Umbraco.Extensions { services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); return services; }