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",