New: Health check for import lists with missing root folders

Closes #1998
This commit is contained in:
Qstick 2023-05-29 22:05:17 -05:00
parent d400685cd1
commit a7a5c7f700
7 changed files with 110 additions and 3 deletions

View File

@ -9,8 +9,10 @@ const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.rootFolders, (state) => state.settings.rootFolders,
(state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange, (state, { includeNoChange }) => includeNoChange,
(rootFolders, includeNoChange) => { (rootFolders, value, includeMissingValue, includeNoChange) => {
const values = rootFolders.items.map((rootFolder) => { const values = rootFolders.items.map((rootFolder) => {
return { return {
key: rootFolder.path, key: rootFolder.path,
@ -25,7 +27,8 @@ function createMapStateToProps() {
key: 'noChange', key: 'noChange',
value: '', value: '',
name: 'No Change', name: 'No Change',
isDisabled: true isDisabled: true,
isMissing: false
}); });
} }
@ -39,6 +42,15 @@ function createMapStateToProps() {
}); });
} }
if (includeMissingValue && !values.find((v) => v.key === value)) {
values.push({
key: value,
value,
isMissing: true,
isDisabled: true
});
}
values.push({ values.push({
key: ADD_NEW_KEY, key: ADD_NEW_KEY,
value: '', value: '',

View File

@ -18,3 +18,9 @@
color: var(--darkGray); color: var(--darkGray);
font-size: $smallFontSize; font-size: $smallFontSize;
} }
.isMissing {
margin-left: 15px;
color: var(--dangerColor);
font-size: $smallFontSize;
}

View File

@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'freeSpace': string; 'freeSpace': string;
'isMissing': string;
'isMobile': string; 'isMobile': string;
'optionText': string; 'optionText': string;
} }

View File

@ -10,6 +10,7 @@ function RootFolderSelectInputOption(props) {
value, value,
name, name,
freeSpace, freeSpace,
isMissing,
isMobile, isMobile,
...otherProps ...otherProps
} = props; } = props;
@ -29,11 +30,20 @@ function RootFolderSelectInputOption(props) {
<div>{text}</div> <div>{text}</div>
{ {
freeSpace != null && freeSpace == null ?
null :
<div className={styles.freeSpace}> <div className={styles.freeSpace}>
{formatBytes(freeSpace)} Free {formatBytes(freeSpace)} Free
</div> </div>
} }
{
isMissing ?
<div className={styles.isMissing}>
Missing
</div> :
null
}
</div> </div>
</EnhancedSelectInputOption> </EnhancedSelectInputOption>
); );
@ -43,6 +53,7 @@ RootFolderSelectInputOption.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
freeSpace: PropTypes.number, freeSpace: PropTypes.number,
isMissing: PropTypes.bool,
isMobile: PropTypes.bool.isRequired isMobile: PropTypes.bool.isRequired
}; };

View File

@ -4,6 +4,7 @@ interface CssExports {
'deleteButton': string; 'deleteButton': string;
'hideMetadataProfile': string; 'hideMetadataProfile': string;
'labelIcon': string; 'labelIcon': string;
'message': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View File

@ -222,6 +222,7 @@ function EditImportListModalContent(props) {
name="rootFolderPath" name="rootFolderPath"
helpText={translate('RootFolderPathHelpText')} helpText={translate('RootFolderPathHelpText')}
{...rootFolderPath} {...rootFolderPath}
includeMissingValue={true}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>

View File

@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Disk;
using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ArtistsDeletedEvent))]
[CheckOn(typeof(ArtistMovedEvent))]
[CheckOn(typeof(AlbumImportedEvent), CheckOnCondition.FailedOnly)]
[CheckOn(typeof(AlbumImportIncompleteEvent), 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<string, List<ImportListDefinition>>();
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<ImportListDefinition> { 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<ImportListDefinition> importLists)
{
return $"{rootFolderPath} ({string.Join(", ", importLists.Select(l => l.Name))})";
}
}
}