diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js index ea0ba535b..5b0a8d570 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputConnector.js +++ b/frontend/src/Components/Form/RootFolderSelectInputConnector.js @@ -10,13 +10,16 @@ const ADD_NEW_KEY = 'addNew'; function createMapStateToProps() { return createSelector( (state) => state.rootFolders, + (state, { value }) => value, + (state, { includeMissingValue }) => includeMissingValue, (state, { includeNoChange }) => includeNoChange, - (rootFolders, includeNoChange) => { + (rootFolders, value, includeMissingValue, includeNoChange) => { const values = rootFolders.items.map((rootFolder) => { return { key: rootFolder.path, value: rootFolder.path, - freeSpace: rootFolder.freeSpace + freeSpace: rootFolder.freeSpace, + isMissing: false }; }); @@ -24,7 +27,8 @@ function createMapStateToProps() { values.unshift({ key: 'noChange', value: 'No Change', - isDisabled: true + isDisabled: true, + isMissing: false }); } @@ -37,6 +41,15 @@ function createMapStateToProps() { }); } + if (includeMissingValue && !values.find((v) => v.key === value)) { + values.push({ + key: value, + value, + isMissing: true, + isDisabled: true + }); + } + values.push({ key: ADD_NEW_KEY, value: 'Add a new path' diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.css b/frontend/src/Components/Form/RootFolderSelectInputOption.css index 0bad54366..fe0dd914b 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.css +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.css @@ -27,3 +27,9 @@ color: $darkGray; font-size: $smallFontSize; } + +.isMissing { + margin-left: 15px; + color: $dangerColor; + font-size: $smallFontSize; +} diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.js b/frontend/src/Components/Form/RootFolderSelectInputOption.js index c3065c6f1..4af1779f1 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.js +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.js @@ -10,6 +10,7 @@ function RootFolderSelectInputOption(props) { id, value, freeSpace, + isMissing, seriesFolder, isMobile, isWindows, @@ -43,11 +44,20 @@ function RootFolderSelectInputOption(props) { { - freeSpace != null && + freeSpace == null ? + null :
{formatBytes(freeSpace)} Free
} + + { + isMissing ? +
+ Missing +
: + null + } ); @@ -57,6 +67,7 @@ RootFolderSelectInputOption.propTypes = { id: PropTypes.string.isRequired, value: PropTypes.string.isRequired, freeSpace: PropTypes.number, + isMissing: PropTypes.boolean, seriesFolder: PropTypes.string, isMobile: PropTypes.bool.isRequired, isWindows: PropTypes.bool diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index caf208431..fcfbfabe7 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -131,6 +131,7 @@ function EditImportListModalContent(props) { name="rootFolderPath" helpText={'Root Folder list items will be added to'} {...rootFolderPath} + includeMissingValue={true} onChange={onInputChange} /> diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs new file mode 100644 index 000000000..2f7e81663 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Disk; +using NzbDrone.Core.ImportLists; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(SeriesDeletedEvent))] + [CheckOn(typeof(SeriesMovedEvent))] + [CheckOn(typeof(EpisodeImportedEvent), CheckOnCondition.FailedOnly)] + [CheckOn(typeof(EpisodeImportFailedEvent), CheckOnCondition.SuccessfulOnly)] + public class ImportListRootFolderCheck : HealthCheckBase + { + private readonly IImportListFactory _importListFactory; + private readonly IDiskProvider _diskProvider; + + public ImportListRootFolderCheck(IImportListFactory importListFactory, IDiskProvider diskProvider) + { + _importListFactory = importListFactory; + _diskProvider = diskProvider; + } + + public override HealthCheck Check() + { + var importLists = _importListFactory.All(); + var missingRootFolders = new Dictionary>(); + + foreach (var importList in importLists) + { + var rootFolderPath = importList.RootFolderPath; + + if (missingRootFolders.ContainsKey(rootFolderPath)) + { + missingRootFolders[rootFolderPath].Add(importList); + + continue; + } + + if (!_diskProvider.FolderExists(rootFolderPath)) + { + missingRootFolders.Add(rootFolderPath, new List { importList }); + } + } + + if (missingRootFolders.Any()) + { + if (missingRootFolders.Count == 1) + { + var missingRootFolder = missingRootFolders.First(); + return new HealthCheck(GetType(), HealthCheckResult.Error, $"Missing root folder for import list(s): {FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)}", "#import_list_missing_root_folder"); + } + + var message = string.Format("Multiple root folders are missing for import lists: {0}", string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value)))); + return new HealthCheck(GetType(), HealthCheckResult.Error, message, "#import_list_missing_root_folder"); + } + + return new HealthCheck(GetType()); + } + + private string FormatRootFolder(string rootFolderPath, List importLists) + { + return $"{rootFolderPath} ({string.Join(", ", importLists.Select(l => l.Name))})"; + } + } +}