diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js index ea4cd2ae2..941b9a2ce 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputConnector.js +++ b/frontend/src/Components/Form/RootFolderSelectInputConnector.js @@ -9,14 +9,17 @@ const ADD_NEW_KEY = 'addNew'; function createMapStateToProps() { return createSelector( (state) => state.settings.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, name: rootFolder.name, - freeSpace: rootFolder.freeSpace + freeSpace: rootFolder.freeSpace, + isMissing: false }; }); @@ -25,7 +28,8 @@ function createMapStateToProps() { key: 'noChange', value: '', name: 'No Change', - isDisabled: true + isDisabled: true, + isMissing: false }); } @@ -39,6 +43,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: '', diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.css b/frontend/src/Components/Form/RootFolderSelectInputOption.css index f7a2759fd..0c62c6646 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.css +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.css @@ -18,3 +18,9 @@ color: var(--darkGray); font-size: $smallFontSize; } + +.isMissing { + margin-left: 15px; + color: var(--dangerColor); + font-size: $smallFontSize; +} diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.js b/frontend/src/Components/Form/RootFolderSelectInputOption.js index 929b9f342..00825f993 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.js +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.js @@ -10,6 +10,7 @@ function RootFolderSelectInputOption(props) { value, name, freeSpace, + isMissing, isMobile, ...otherProps } = props; @@ -29,11 +30,20 @@ function RootFolderSelectInputOption(props) {
{text}
{ - freeSpace != null && + freeSpace == null ? + null :
{formatBytes(freeSpace)} Free
} + + { + isMissing ? +
+ Missing +
: + null + } ); @@ -43,6 +53,7 @@ RootFolderSelectInputOption.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.string.isRequired, freeSpace: PropTypes.number, + isMissing: PropTypes.bool, isMobile: PropTypes.bool.isRequired }; diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js index 3f7657a48..19df8e0c6 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js @@ -222,6 +222,7 @@ function EditImportListModalContent(props) { name="rootFolderPath" helpText={translate('RootFolderPathHelpText')} {...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..2d15d6271 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Books.Events; +using NzbDrone.Core.ImportLists; +using NzbDrone.Core.Localization; +using NzbDrone.Core.MediaFiles.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(AuthorDeletedEvent))] + [CheckOn(typeof(AuthorMovedEvent))] + [CheckOn(typeof(BookImportedEvent), CheckOnCondition.FailedOnly)] + [CheckOn(typeof(TrackImportedEvent), CheckOnCondition.FailedOnly)] + [CheckOn(typeof(TrackImportFailedEvent), CheckOnCondition.SuccessfulOnly)] + public class ImportListRootFolderCheck : HealthCheckBase + { + private readonly IImportListFactory _importListFactory; + private readonly IDiskProvider _diskProvider; + + public ImportListRootFolderCheck(IImportListFactory importListFactory, IDiskProvider diskProvider, ILocalizationService localizationService) + : base(localizationService) + { + _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, string.Format(_localizationService.GetLocalizedString("ImportListMissingRoot"), FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)), "#import-list-missing-root-folder"); + } + + var message = string.Format(_localizationService.GetLocalizedString("ImportListMultipleMissingRoots"), 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))})"; + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index bcae2ad72..222b9b1f2 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -348,6 +348,8 @@ "ImportFailedInterp": "Import failed: {0}", "ImportFailures": "Import failures", "ImportListExclusions": "Import List Exclusions", + "ImportListMissingRoot": "Missing root folder for import list(s): {0}", + "ImportListMultipleMissingRoots": "Multiple root folders are missing for import lists: {0}", "ImportListSettings": "General Import List Settings", "ImportListSpecificSettings": "Import List Specific Settings", "ImportListStatusCheckAllClientMessage": "All lists are unavailable due to failures",