diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js
index e39ed837d..e903079b5 100644
--- a/frontend/src/Settings/MediaManagement/MediaManagement.js
+++ b/frontend/src/Settings/MediaManagement/MediaManagement.js
@@ -210,25 +210,21 @@ class MediaManagement extends Component {
/>
- {
- isWindows ?
- null :
-
- {translate('SkipFreeSpaceCheck')}
+
+ {translate('SkipFreeSpaceCheck')}
-
-
- }
+
+
+ {
+ private RemoteEpisode _remoteEpisode;
+
+ [SetUp]
+ public void Setup()
+ {
+ _remoteEpisode = new RemoteEpisode() { Release = new ReleaseInfo(), Series = new Series { Path = @"C:\Test\TV\Series".AsOsAgnostic() } };
+ }
+
+ private void WithMinimumFreeSpace(int size)
+ {
+ Mocker.GetMock().SetupGet(c => c.MinimumFreeSpaceWhenImporting).Returns(size);
+ }
+
+ private void WithAvailableSpace(int size)
+ {
+ Mocker.GetMock().Setup(s => s.GetAvailableSpace(It.IsAny())).Returns(size.Megabytes());
+ }
+
+ private void WithSize(int size)
+ {
+ _remoteEpisode.Release.Size = size.Megabytes();
+ }
+
+ [Test]
+ public void should_return_true_when_available_space_is_more_than_size()
+ {
+ WithMinimumFreeSpace(0);
+ WithAvailableSpace(200);
+ WithSize(100);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_when_available_space_minus_size_is_more_than_minimum_free_space()
+ {
+ WithMinimumFreeSpace(50);
+ WithAvailableSpace(200);
+ WithSize(100);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_false_available_space_is_less_than_size()
+ {
+ WithMinimumFreeSpace(0);
+ WithAvailableSpace(200);
+ WithSize(1000);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_return_false_when_available_space_minus_size_is_less_than_minimum_free_space()
+ {
+ WithMinimumFreeSpace(150);
+ WithAvailableSpace(200);
+ WithSize(100);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_return_true_if_skip_free_space_check_is_true()
+ {
+ Mocker.GetMock()
+ .Setup(s => s.SkipFreeSpaceCheckWhenImporting)
+ .Returns(true);
+
+ WithMinimumFreeSpace(150);
+ WithAvailableSpace(200);
+ WithSize(100);
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs
index 65eb7a5be..f52480e19 100644
--- a/src/NzbDrone.Core/Configuration/ConfigService.cs
+++ b/src/NzbDrone.Core/Configuration/ConfigService.cs
@@ -186,6 +186,7 @@ namespace NzbDrone.Core.Configuration
set { SetValue("DownloadClientHistoryLimit", value); }
}
+ // TODO: Rename to 'Skip Free Space Check'
public bool SkipFreeSpaceCheckWhenImporting
{
get { return GetValueBoolean("SkipFreeSpaceCheckWhenImporting", false); }
diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs
new file mode 100644
index 000000000..c01c94601
--- /dev/null
+++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FreeSpaceSpecification.cs
@@ -0,0 +1,66 @@
+using NLog;
+using NzbDrone.Common.Disk;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Core.Configuration;
+using NzbDrone.Core.IndexerSearch.Definitions;
+using NzbDrone.Core.Parser.Model;
+
+namespace NzbDrone.Core.DecisionEngine.Specifications
+{
+ public class FreeSpaceSpecification : IDecisionEngineSpecification
+ {
+ private readonly IConfigService _configService;
+ private readonly IDiskProvider _diskProvider;
+ private readonly Logger _logger;
+
+ public FreeSpaceSpecification(IConfigService configService, IDiskProvider diskProvider, Logger logger)
+ {
+ _configService = configService;
+ _diskProvider = diskProvider;
+ _logger = logger;
+ }
+
+ public SpecificationPriority Priority => SpecificationPriority.Default;
+ public RejectionType Type => RejectionType.Permanent;
+
+ public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
+ {
+ if (_configService.SkipFreeSpaceCheckWhenImporting)
+ {
+ _logger.Debug("Skipping free space check");
+ return Decision.Accept();
+ }
+
+ var size = subject.Release.Size;
+ var freeSpace = _diskProvider.GetAvailableSpace(subject.Series.Path);
+
+ if (!freeSpace.HasValue)
+ {
+ _logger.Debug("Unable to get available space for {0}. Skipping", subject.Series.Path);
+
+ return Decision.Accept();
+ }
+
+ var minimumSpace = _configService.MinimumFreeSpaceWhenImporting.Megabytes();
+ var remainingSpace = freeSpace.Value - size;
+
+ if (remainingSpace <= 0)
+ {
+ var message = "Importing after download will exceed available disk space";
+
+ _logger.Debug(message);
+ return Decision.Reject(message);
+ }
+
+ if (remainingSpace < minimumSpace)
+ {
+ var message = $"Not enough free space ({minimumSpace.SizeSuffix()}) to import after download: {remainingSpace.SizeSuffix()}. (Settings: Media Management: Minimum Free Space)";
+
+ _logger.Debug(message);
+ return Decision.Reject(message);
+ }
+
+ return Decision.Accept();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 8d58e8c9b..9aecc80ef 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -1903,7 +1903,7 @@
"SizeLimit": "Size Limit",
"SizeOnDisk": "Size on disk",
"SkipFreeSpaceCheck": "Skip Free Space Check",
- "SkipFreeSpaceCheckWhenImportingHelpText": "Use when {appName} is unable to detect free space of your root folder during file import",
+ "SkipFreeSpaceCheckHelpText": "Use when {appName} is unable to detect free space of your root folder",
"SkipRedownload": "Skip Redownload",
"SkipRedownloadHelpText": "Prevents {appName} from trying to download an alternative release for this item",
"Small": "Small",