From 86b8dd48564c1b5790e852d2e8af04f5f41b6db9 Mon Sep 17 00:00:00 2001 From: Qstick Date: Wed, 15 Jan 2020 21:52:45 -0500 Subject: [PATCH] Fixed: Not deleting movie files during upgrade when root folder is missing Fixes #4066 Co-Authored-By: Mark McDowall --- .../UpgradeMediaFileServiceFixture.cs | 54 +++++++++++++------ .../HealthCheck/Checks/RootFolderCheck.cs | 3 ++ .../Events/MovieImportFailedEvent.cs | 29 ++++++++++ .../MediaFiles/MovieFileMovingService.cs | 3 +- .../MovieImport/ImportApprovedMovie.cs | 12 +++++ .../RootFolderNotFoundException.cs | 28 ++++++++++ .../MediaFiles/UpgradeMediaFileService.cs | 9 ++++ 7 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 src/NzbDrone.Core/MediaFiles/Events/MovieImportFailedEvent.cs create mode 100644 src/NzbDrone.Core/MediaFiles/MovieImport/RootFolderNotFoundException.cs diff --git a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs index f990dc488..b6e2e49e4 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/UpgradeMediaFileServiceFixture.cs @@ -1,9 +1,11 @@ +using System.IO; using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MovieImport; using NzbDrone.Core.Movies; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; @@ -22,33 +24,41 @@ namespace NzbDrone.Core.Test.MediaFiles _localMovie = new LocalMovie(); _localMovie.Movie = new Movie { - Path = @"C:\Test\TV\Series".AsOsAgnostic() + Path = @"C:\Test\Movies\Movie".AsOsAgnostic() }; _movieFile = Builder - .CreateNew() - .Build(); + .CreateNew() + .Build(); Mocker.GetMock() - .Setup(c => c.FileExists(It.IsAny())) - .Returns(true); + .Setup(c => c.FolderExists(Directory.GetParent(_localMovie.Movie.Path).FullName)) + .Returns(true); + + Mocker.GetMock() + .Setup(c => c.FileExists(It.IsAny())) + .Returns(true); + + Mocker.GetMock() + .Setup(c => c.GetParentFolder(It.IsAny())) + .Returns(c => Path.GetDirectoryName(c)); } - private void GivenSingleEpisodeWithSingleEpisodeFile() + private void GivenSingleMovieWithSingleMovieFile() { _localMovie.Movie.MovieFileId = 1; _localMovie.Movie.MovieFile = new MovieFile { Id = 1, - RelativePath = @"Season 01\30.rock.s01e01.avi", + RelativePath = @"A.Movie.2019.avi", }; } [Test] - public void should_delete_single_episode_file_once() + public void should_delete_single_movie_file_once() { - GivenSingleEpisodeWithSingleEpisodeFile(); + GivenSingleMovieWithSingleMovieFile(); Subject.UpgradeMovieFile(_movieFile, _localMovie); @@ -56,9 +66,9 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_delete_episode_file_from_database() + public void should_delete_movie_file_from_database() { - GivenSingleEpisodeWithSingleEpisodeFile(); + GivenSingleMovieWithSingleMovieFile(); Subject.UpgradeMovieFile(_movieFile, _localMovie); @@ -68,7 +78,7 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_delete_existing_file_fromdb_if_file_doesnt_exist() { - GivenSingleEpisodeWithSingleEpisodeFile(); + GivenSingleMovieWithSingleMovieFile(); Mocker.GetMock() .Setup(c => c.FileExists(It.IsAny())) @@ -82,7 +92,7 @@ namespace NzbDrone.Core.Test.MediaFiles [Test] public void should_not_try_to_recyclebin_existing_file_if_file_doesnt_exist() { - GivenSingleEpisodeWithSingleEpisodeFile(); + GivenSingleMovieWithSingleMovieFile(); Mocker.GetMock() .Setup(c => c.FileExists(It.IsAny())) @@ -94,11 +104,25 @@ namespace NzbDrone.Core.Test.MediaFiles } [Test] - public void should_return_old_episode_file_in_oldFiles() + public void should_return_old_movie_file_in_oldFiles() { - GivenSingleEpisodeWithSingleEpisodeFile(); + GivenSingleMovieWithSingleMovieFile(); Subject.UpgradeMovieFile(_movieFile, _localMovie).OldFiles.Count.Should().Be(1); } + + [Test] + public void should_throw_if_there_are_existing_movie_files_and_the_root_folder_is_missing() + { + GivenSingleMovieWithSingleMovieFile(); + + Mocker.GetMock() + .Setup(c => c.FolderExists(Directory.GetParent(_localMovie.Movie.Path).FullName)) + .Returns(false); + + Assert.Throws(() => Subject.UpgradeMovieFile(_movieFile, _localMovie)); + + Mocker.GetMock().Verify(v => v.Delete(_localMovie.Movie.MovieFile, DeleteMediaFileReason.Upgrade), Times.Never()); + } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs index 076ba64bc..c1e9b4d8e 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/RootFolderCheck.cs @@ -1,5 +1,6 @@ using System.Linq; using NzbDrone.Common.Disk; +using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Movies; using NzbDrone.Core.Movies.Events; @@ -7,6 +8,8 @@ namespace NzbDrone.Core.HealthCheck.Checks { [CheckOn(typeof(MovieDeletedEvent))] [CheckOn(typeof(MovieMovedEvent))] + [CheckOn(typeof(MoviesImportedEvent), CheckOnCondition.FailedOnly)] + [CheckOn(typeof(MovieImportFailedEvent), CheckOnCondition.SuccessfulOnly)] public class RootFolderCheck : HealthCheckBase { private readonly IMovieService _movieService; diff --git a/src/NzbDrone.Core/MediaFiles/Events/MovieImportFailedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/MovieImportFailedEvent.cs new file mode 100644 index 000000000..f4bce4389 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/Events/MovieImportFailedEvent.cs @@ -0,0 +1,29 @@ +using System; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.MediaFiles.Events +{ + public class MovieImportFailedEvent : IEvent + { + public Exception Exception { get; set; } + public LocalMovie MovieInfo { get; } + public bool NewDownload { get; } + public string DownloadClient { get; } + public string DownloadId { get; } + + public MovieImportFailedEvent(Exception exception, LocalMovie movieInfo, bool newDownload, DownloadClientItem downloadClientItem) + { + Exception = exception; + MovieInfo = movieInfo; + NewDownload = newDownload; + + if (downloadClientItem != null) + { + DownloadClient = downloadClientItem.DownloadClient; + DownloadId = downloadClientItem.DownloadId; + } + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs index 48c1ac056..3de52ad07 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieFileMovingService.cs @@ -7,6 +7,7 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.MediaFiles.MovieImport; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Movies; using NzbDrone.Core.Organizer; @@ -188,7 +189,7 @@ namespace NzbDrone.Core.MediaFiles if (!_diskProvider.FolderExists(rootFolder)) { - throw new DirectoryNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); + throw new RootFolderNotFoundException(string.Format("Root folder '{0}' was not found.", rootFolder)); } var changed = false; diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs index 2618e69c1..a75423ae6 100644 --- a/src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/ImportApprovedMovie.cs @@ -134,6 +134,18 @@ namespace NzbDrone.Core.MediaFiles.MovieImport _eventAggregator.PublishEvent(new MovieDownloadedEvent(localMovie, movieFile, oldFiles, downloadClientItem)); } } + catch (RootFolderNotFoundException e) + { + _logger.Warn(e, "Couldn't import movie " + localMovie); + _eventAggregator.PublishEvent(new MovieImportFailedEvent(e, localMovie, newDownload, downloadClientItem)); + + importResults.Add(new ImportResult(importDecision, "Failed to import movie, Root folder missing.")); + } + catch (DestinationAlreadyExistsException e) + { + _logger.Warn(e, "Couldn't import movie " + localMovie); + importResults.Add(new ImportResult(importDecision, "Failed to import movie, Destination already exists.")); + } catch (Exception e) { _logger.Warn(e, "Couldn't import movie " + localMovie); diff --git a/src/NzbDrone.Core/MediaFiles/MovieImport/RootFolderNotFoundException.cs b/src/NzbDrone.Core/MediaFiles/MovieImport/RootFolderNotFoundException.cs new file mode 100644 index 000000000..1d47aac4b --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/MovieImport/RootFolderNotFoundException.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; +using System.Runtime.Serialization; + +namespace NzbDrone.Core.MediaFiles.MovieImport +{ + public class RootFolderNotFoundException : DirectoryNotFoundException + { + public RootFolderNotFoundException() + { + } + + public RootFolderNotFoundException(string message) + : base(message) + { + } + + public RootFolderNotFoundException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected RootFolderNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs index 883bee45f..e6a18ccec 100644 --- a/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/UpgradeMediaFileService.cs @@ -1,6 +1,7 @@ using System.IO; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Core.MediaFiles.MovieImport; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.MediaFiles @@ -41,6 +42,14 @@ namespace NzbDrone.Core.MediaFiles var existingFile = localMovie.Movie.MovieFile; + var rootFolder = _diskProvider.GetParentFolder(localMovie.Movie.Path); + + // If there are existing movie files and the root folder is missing, throw, so the old file isn't left behind during the import process. + if (existingFile != null && !_diskProvider.FolderExists(rootFolder)) + { + throw new RootFolderNotFoundException($"Root folder '{rootFolder}' was not found."); + } + if (existingFile != null) { var movieFilePath = Path.Combine(localMovie.Movie.Path, existingFile.RelativePath);