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;
}