diff --git a/frontend/src/Components/Form/RootFolderSelectInputConnector.js b/frontend/src/Components/Form/RootFolderSelectInputConnector.js index d780d542b..62077d68a 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,6 +28,16 @@ function createMapStateToProps() { key: 'noChange', value: '', name: 'No Change', + isDisabled: true, + isMissing: false + }); + } + + if (includeMissingValue && !values.find((v) => v.key === value)) { + values.push({ + key: value, + value, + isMissing: true, isDisabled: true }); } diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.css b/frontend/src/Components/Form/RootFolderSelectInputOption.css index b2021254a..f63cba8d8 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.css +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.css @@ -27,3 +27,10 @@ color: var(--darkGray); font-size: $smallFontSize; } + +.isMissing { + margin-left: 15px; + color: var(--dangerColor); + font-size: $smallFontSize; +} + diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts b/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts index 2c31fda05..41b5a15ac 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.css.d.ts @@ -3,6 +3,7 @@ interface CssExports { 'artistFolder': string; 'freeSpace': string; + 'isMissing': string; 'isMobile': string; 'optionText': string; 'value': string; diff --git a/frontend/src/Components/Form/RootFolderSelectInputOption.js b/frontend/src/Components/Form/RootFolderSelectInputOption.js index 0e7bb572c..b339c84e8 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputOption.js +++ b/frontend/src/Components/Form/RootFolderSelectInputOption.js @@ -11,6 +11,7 @@ function RootFolderSelectInputOption(props) { value, name, freeSpace, + isMissing, artistFolder, isMobile, isWindows, @@ -19,9 +20,7 @@ function RootFolderSelectInputOption(props) { const slashCharacter = isWindows ? '\\' : '/'; - const text = value === '' ? name : `[${name}] ${value}`; - - console.debug(props); + const text = name === '' ? value : `[${name}] ${value}`; return ( { - freeSpace != null && + freeSpace == null ? + null :
{formatBytes(freeSpace)} Free
} + + { + isMissing ? +
+ Missing +
: + null + }
); @@ -63,9 +71,14 @@ RootFolderSelectInputOption.propTypes = { name: PropTypes.string.isRequired, value: PropTypes.string.isRequired, freeSpace: PropTypes.number, + isMissing: PropTypes.bool, artistFolder: PropTypes.string, isMobile: PropTypes.bool.isRequired, isWindows: PropTypes.bool }; +RootFolderSelectInputOption.defaultProps = { + name: '' +}; + export default RootFolderSelectInputOption; diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js index 3cd43b204..58e8efbd3 100644 --- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js +++ b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js @@ -17,7 +17,7 @@ function RootFolderSelectInputSelectedValue(props) { const slashCharacter = isWindows ? '\\' : '/'; - const text = value === '' ? name : `[${name}] ${value}`; + const text = name === '' ? value : `[${name}] ${value}`; return ( diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs new file mode 100644 index 000000000..e5367c28c --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportListRootFolderCheck.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.ImportLists; +using NzbDrone.Core.Localization; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Music.Events; +using NzbDrone.Core.RootFolders; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(ModelEvent))] + [CheckOn(typeof(ArtistsDeletedEvent))] + [CheckOn(typeof(ArtistMovedEvent))] + [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>(); + + Console.WriteLine(importLists.ToArray().ToJson()); + + 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("ImportListRootFolderMissingRootHealthCheckMessage"), FormatRootFolder(missingRootFolder.Key, missingRootFolder.Value)), + "#import-list-missing-root-folder"); + } + + return new HealthCheck(GetType(), + HealthCheckResult.Error, + string.Format(_localizationService.GetLocalizedString("ImportListRootFolderMultipleMissingRootsHealthCheckMessage"), string.Join(" | ", missingRootFolders.Select(m => FormatRootFolder(m.Key, m.Value)))), + "#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 aae88d762..c1de14894 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -437,6 +437,8 @@ "ImportFailedInterp": "Import failed: {0}", "ImportFailures": "Import failures", "ImportListExclusions": "Import List Exclusions", + "ImportListRootFolderMissingRootHealthCheckMessage": "Missing root folder for import list(s): {0}", + "ImportListRootFolderMultipleMissingRootsHealthCheckMessage": "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",