From b371296e78e7083b1cd9433e3ef35f20c77e6269 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 15 Dec 2017 16:14:28 -0800 Subject: [PATCH] Validate before deleting series folders Closes #264 --- .../RootFolders/RootFolderModule.cs | 9 +- src/NzbDrone.Api/Series/SeriesModule.cs | 4 +- .../NzbDrone.Core.Test.csproj | 1 + .../SystemFolderValidatorFixture.cs | 74 +++++++++++++++ .../MediaFiles/MediaFileDeletionService.cs | 23 +++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Paths/SeriesAncestorValidator.cs | 6 +- .../Validation/Paths/SystemFolderValidator.cs | 92 +++++++++++++++++++ 8 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 src/NzbDrone.Core.Test/ValidationTests/SystemFolderValidatorFixture.cs create mode 100644 src/NzbDrone.Core/Validation/Paths/SystemFolderValidator.cs diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs index e87e581de..30303ab73 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation.Paths; @@ -17,7 +17,9 @@ namespace NzbDrone.Api.RootFolders DroneFactoryValidator droneFactoryValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator, StartupFolderValidator startupFolderValidator, - FolderWritableValidator folderWritableValidator) + SystemFolderValidator systemFolderValidator, + FolderWritableValidator folderWritableValidator + ) : base(signalRBroadcaster) { _rootFolderService = rootFolderService; @@ -35,6 +37,7 @@ namespace NzbDrone.Api.RootFolders .SetValidator(mappedNetworkDriveValidator) .SetValidator(startupFolderValidator) .SetValidator(pathExistsValidator) + .SetValidator(systemFolderValidator) .SetValidator(folderWritableValidator); } @@ -60,4 +63,4 @@ namespace NzbDrone.Api.RootFolders _rootFolderService.Remove(id); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index 0b33b9ee3..20c5d8ecb 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation; @@ -45,6 +45,7 @@ namespace NzbDrone.Api.Series SeriesExistsValidator seriesExistsValidator, DroneFactoryValidator droneFactoryValidator, SeriesAncestorValidator seriesAncestorValidator, + SystemFolderValidator systemFolderValidator, ProfileExistsValidator profileExistsValidator ) : base(signalRBroadcaster) @@ -71,6 +72,7 @@ namespace NzbDrone.Api.Series .SetValidator(seriesPathValidator) .SetValidator(droneFactoryValidator) .SetValidator(seriesAncestorValidator) + .SetValidator(systemFolderValidator) .When(s => !s.Path.IsNullOrWhiteSpace()); SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 3f2465279..a88ef4945 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -402,6 +402,7 @@ + diff --git a/src/NzbDrone.Core.Test/ValidationTests/SystemFolderValidatorFixture.cs b/src/NzbDrone.Core.Test/ValidationTests/SystemFolderValidatorFixture.cs new file mode 100644 index 000000000..39a7c68e4 --- /dev/null +++ b/src/NzbDrone.Core.Test/ValidationTests/SystemFolderValidatorFixture.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ValidationTests +{ + public class SystemFolderValidatorFixture : CoreTest + { + private TestValidator _validator; + + [SetUp] + public void Setup() + { + _validator = new TestValidator + { + v => v.RuleFor(s => s.Path).SetValidator(Subject) + }; + } + + [Test] + public void should_not_be_valid_if_set_to_windows_folder() + { + WindowsOnly(); + + var series = Builder.CreateNew() + .With(s => s.Path = Environment.GetFolderPath(Environment.SpecialFolder.Windows)) + .Build(); + + _validator.Validate(series).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_child_of_windows_folder() + { + WindowsOnly(); + + var series = Builder.CreateNew() + .With(s => s.Path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Test")) + .Build(); + + _validator.Validate(series).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_set_to_bin_folder() + { + MonoOnly(); + + var series = Builder.CreateNew() + .With(s => s.Path = "/bin") + .Build(); + + _validator.Validate(series).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_child_of_bin_folder() + { + MonoOnly(); + + var series = Builder.CreateNew() + .With(s => s.Path = "/bin/test") + .Build(); + + _validator.Validate(series).IsValid.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs index 29066f08f..70c9a5a75 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileDeletionService.cs @@ -21,16 +21,19 @@ namespace NzbDrone.Core.MediaFiles private readonly IDiskProvider _diskProvider; private readonly IRecycleBinProvider _recycleBinProvider; private readonly IMediaFileService _mediaFileService; + private readonly ISeriesService _seriesService; private readonly Logger _logger; public MediaFileDeletionService(IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, IMediaFileService mediaFileService, + ISeriesService seriesService, Logger logger) { _diskProvider = diskProvider; _recycleBinProvider = recycleBinProvider; _mediaFileService = mediaFileService; + _seriesService = seriesService; _logger = logger; } @@ -76,6 +79,26 @@ namespace NzbDrone.Core.MediaFiles { if (message.DeleteFiles) { + var series = message.Series; + var allSeries = _seriesService.GetAllSeries(); + + foreach (var s in allSeries) + { + if (s.Id == series.Id) continue; + + if (series.Path.IsParentPath(s.Path)) + { + _logger.Error("Series path: '{0}' is a parent of another series, not deleting files.", series.Path); + return; + } + + if (series.Path.PathEquals(s.Path)) + { + _logger.Error("Series path: '{0}' is the same as another series, not deleting files.", series.Path); + return; + } + } + if (_diskProvider.FolderExists(message.Series.Path)) { _recycleBinProvider.DeleteFolder(message.Series.Path); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 8b45694d3..0aa6fef7c 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1182,6 +1182,7 @@ + diff --git a/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs b/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs index c91560873..850118a0d 100644 --- a/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/SeriesAncestorValidator.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using FluentValidation.Validators; using NzbDrone.Common.Extensions; using NzbDrone.Core.Tv; @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Validation.Paths private readonly ISeriesService _seriesService; public SeriesAncestorValidator(ISeriesService seriesService) - : base("Path is an ancestor of an existing path") + : base("Path is an ancestor of an existing series") { _seriesService = seriesService; } @@ -22,4 +22,4 @@ namespace NzbDrone.Core.Validation.Paths return !_seriesService.GetAllSeries().Any(s => context.PropertyValue.ToString().IsParentPath(s.Path)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Validation/Paths/SystemFolderValidator.cs b/src/NzbDrone.Core/Validation/Paths/SystemFolderValidator.cs new file mode 100644 index 000000000..a7af763cb --- /dev/null +++ b/src/NzbDrone.Core/Validation/Paths/SystemFolderValidator.cs @@ -0,0 +1,92 @@ +using System; +using FluentValidation.Validators; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Validation.Paths +{ + public class SystemFolderValidator : PropertyValidator + { + public SystemFolderValidator() + : base("Is {relationship} system folder {systemFolder}") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var folder = context.PropertyValue.ToString(); + + if (OsInfo.IsWindows) + { + var windowsFolder = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + context.MessageFormatter.AppendArgument("systemFolder", windowsFolder); + + if (windowsFolder.PathEquals(folder)) + { + context.MessageFormatter.AppendArgument("relationship", "set to"); + + return false; + } + + if (windowsFolder.IsParentPath(folder)) + { + context.MessageFormatter.AppendArgument("relationship", "child of"); + + return false; + } + } + else if (OsInfo.IsOsx) + { + var systemFolder = "/System"; + context.MessageFormatter.AppendArgument("systemFolder", systemFolder); + + if (systemFolder.PathEquals(folder)) + { + context.MessageFormatter.AppendArgument("relationship", "child of"); + + return false; + } + + if (systemFolder.IsParentPath(folder)) + { + context.MessageFormatter.AppendArgument("relationship", "child of"); + + return false; + } + } + else + { + var folders = new[] + { + "/bin", + "/boot", + "/lib", + "/sbin", + "/srv", + "/proc" + }; + + foreach (var f in folders) + { + context.MessageFormatter.AppendArgument("systemFolder", f); + + if (f.PathEquals(folder)) + { + context.MessageFormatter.AppendArgument("relationship", "child of"); + + return false; + } + + if (f.IsParentPath(folder)) + { + context.MessageFormatter.AppendArgument("relationship", "child of"); + + return false; + } + } + } + + return true; + } + } +}