From 8aad1ac55497579c1afdc2d918f7ebe490fb4013 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 19 Oct 2024 09:01:23 +0300 Subject: [PATCH] New: Allow major version updates to be installed (#2260) * New: Allow major version updates to be installed (cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab) * fixup! New: Allow major version updates to be installed --------- Co-authored-by: Mark McDowall --- frontend/src/App/AppRoutes.tsx | 4 +- frontend/src/App/State/SettingsAppState.ts | 8 +- frontend/src/System/Updates/UpdateChanges.js | 52 --- frontend/src/System/Updates/UpdateChanges.tsx | 43 +++ frontend/src/System/Updates/Updates.js | 252 --------------- frontend/src/System/Updates/Updates.tsx | 303 ++++++++++++++++++ .../src/System/Updates/UpdatesConnector.js | 101 ------ frontend/src/typings/Settings/General.ts | 45 +++ .../src/typings/{ => Settings}/UiSettings.ts | 2 +- frontend/src/typings/SystemStatus.ts | 1 + .../UpdateTests/UpdateServiceFixture.cs | 8 +- src/NzbDrone.Core/Localization/Core/en.json | 5 + .../Commands/ApplicationUpdateCheckCommand.cs | 2 + .../Commands/ApplicationUpdateCommand.cs | 1 + .../Update/InstallUpdateService.cs | 14 +- .../Update/UpdatePackageProvider.cs | 1 + 16 files changed, 425 insertions(+), 417 deletions(-) delete mode 100644 frontend/src/System/Updates/UpdateChanges.js create mode 100644 frontend/src/System/Updates/UpdateChanges.tsx delete mode 100644 frontend/src/System/Updates/Updates.js create mode 100644 frontend/src/System/Updates/Updates.tsx delete mode 100644 frontend/src/System/Updates/UpdatesConnector.js create mode 100644 frontend/src/typings/Settings/General.ts rename frontend/src/typings/{ => Settings}/UiSettings.ts (79%) diff --git a/frontend/src/App/AppRoutes.tsx b/frontend/src/App/AppRoutes.tsx index 8b16e19a3..d451a12fb 100644 --- a/frontend/src/App/AppRoutes.tsx +++ b/frontend/src/App/AppRoutes.tsx @@ -20,7 +20,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector'; import Logs from 'System/Logs/Logs'; import Status from 'System/Status/Status'; import Tasks from 'System/Tasks/Tasks'; -import UpdatesConnector from 'System/Updates/UpdatesConnector'; +import Updates from 'System/Updates/Updates'; import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; function RedirectWithUrlBase() { @@ -99,7 +99,7 @@ function AppRoutes() { - + diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index 1404e5d80..33c6c936d 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -7,7 +7,8 @@ import { IndexerCategory } from 'Indexer/Indexer'; import Application from 'typings/Application'; import DownloadClient from 'typings/DownloadClient'; import Notification from 'typings/Notification'; -import { UiSettings } from 'typings/UiSettings'; +import General from 'typings/Settings/General'; +import UiSettings from 'typings/Settings/UiSettings'; export interface AppProfileAppState extends AppSectionState, @@ -28,6 +29,10 @@ export interface DownloadClientAppState isTestingAll: boolean; } +export interface GeneralAppState + extends AppSectionItemState, + AppSectionSaveState {} + export interface IndexerCategoryAppState extends AppSectionState, AppSectionDeleteState, @@ -43,6 +48,7 @@ interface SettingsAppState { appProfiles: AppProfileAppState; applications: ApplicationAppState; downloadClients: DownloadClientAppState; + general: GeneralAppState; indexerCategories: IndexerCategoryAppState; notifications: NotificationAppState; ui: UiSettingsAppState; diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js deleted file mode 100644 index 75ee74c10..000000000 --- a/frontend/src/System/Updates/UpdateChanges.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import styles from './UpdateChanges.css'; - -class UpdateChanges extends Component { - - // - // Render - - render() { - const { - title, - changes - } = this.props; - - if (changes.length === 0) { - return null; - } - - const uniqueChanges = [...new Set(changes)]; - - return ( -
-
{title}
-
    - { - uniqueChanges.map((change, index) => { - const checkChange = change.replace(/#\d{3,5}\b/g, (match, contents) => { - return `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring(1)})`; - }); - - return ( -
  • - -
  • - ); - }) - } -
-
- ); - } - -} - -UpdateChanges.propTypes = { - title: PropTypes.string.isRequired, - changes: PropTypes.arrayOf(PropTypes.string) -}; - -export default UpdateChanges; diff --git a/frontend/src/System/Updates/UpdateChanges.tsx b/frontend/src/System/Updates/UpdateChanges.tsx new file mode 100644 index 000000000..460814cbe --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import styles from './UpdateChanges.css'; + +interface UpdateChangesProps { + title: string; + changes: string[]; +} + +function UpdateChanges(props: UpdateChangesProps) { + const { title, changes } = props; + + if (changes.length === 0) { + return null; + } + + const uniqueChanges = [...new Set(changes)]; + + return ( +
+
{title}
+
    + {uniqueChanges.map((change, index) => { + const checkChange = change.replace( + /#\d{3,5}\b/g, + (match) => + `[${match}](https://github.com/Prowlarr/Prowlarr/issues/${match.substring( + 1 + )})` + ); + + return ( +
  • + +
  • + ); + })} +
+
+ ); +} + +export default UpdateChanges; diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js deleted file mode 100644 index 6232d816a..000000000 --- a/frontend/src/System/Updates/Updates.js +++ /dev/null @@ -1,252 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; -import Alert from 'Components/Alert'; -import Icon from 'Components/Icon'; -import Label from 'Components/Label'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import PageContent from 'Components/Page/PageContent'; -import PageContentBody from 'Components/Page/PageContentBody'; -import { icons, kinds } from 'Helpers/Props'; -import formatDate from 'Utilities/Date/formatDate'; -import formatDateTime from 'Utilities/Date/formatDateTime'; -import translate from 'Utilities/String/translate'; -import UpdateChanges from './UpdateChanges'; -import styles from './Updates.css'; - -class Updates extends Component { - - // - // Render - - render() { - const { - currentVersion, - isFetching, - isPopulated, - updatesError, - generalSettingsError, - items, - isInstallingUpdate, - updateMechanism, - isDocker, - updateMechanismMessage, - shortDateFormat, - longDateFormat, - timeFormat, - onInstallLatestPress - } = this.props; - - const hasError = !!(updatesError || generalSettingsError); - const hasUpdates = isPopulated && !hasError && items.length > 0; - const noUpdates = isPopulated && !hasError && !items.length; - const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); - const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; - - const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError'); - const externalUpdaterMessages = { - external: translate('ExternalUpdater'), - apt: translate('AptUpdater'), - docker: translate('DockerUpdater') - }; - - return ( - - - { - !isPopulated && !hasError && - - } - - { - noUpdates && - - {translate('NoUpdatesAreAvailable')} - - } - - { - hasUpdateToInstall && -
- { - (updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ? - - {translate('InstallLatest')} - : - - - - -
- {externalUpdaterPrefix} -
-
- } - - { - isFetching && - - } -
- } - - { - noUpdateToInstall && -
- - -
- {translate('OnLatestVersion')} -
- - { - isFetching && - - } -
- } - - { - hasUpdates && -
- { - items.map((update) => { - const hasChanges = !!update.changes; - - return ( -
-
-
{update.version}
-
-
- {formatDate(update.releaseDate, shortDateFormat)} -
- - { - update.branch === 'master' ? - null: - - } - - { - update.version === currentVersion ? - : - null - } - - { - update.version !== currentVersion && update.installedOn ? - : - null - } -
- - { - !hasChanges && -
- {translate('MaintenanceRelease')} -
- } - - { - hasChanges && -
- - - -
- } -
- ); - }) - } -
- } - - { - !!updatesError && - - {translate('FailedToFetchUpdates')} - - } - - { - !!generalSettingsError && - - {translate('FailedToUpdateSettings')} - - } -
-
- ); - } - -} - -Updates.propTypes = { - currentVersion: PropTypes.string.isRequired, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - updatesError: PropTypes.object, - generalSettingsError: PropTypes.object, - items: PropTypes.array.isRequired, - isInstallingUpdate: PropTypes.bool.isRequired, - isDocker: PropTypes.bool.isRequired, - updateMechanism: PropTypes.string, - updateMechanismMessage: PropTypes.string, - shortDateFormat: PropTypes.string.isRequired, - longDateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - onInstallLatestPress: PropTypes.func.isRequired -}; - -export default Updates; diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx new file mode 100644 index 000000000..f490a51ed --- /dev/null +++ b/frontend/src/System/Updates/Updates.tsx @@ -0,0 +1,303 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBody from 'Components/Page/PageContentBody'; +import { icons, kinds } from 'Helpers/Props'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { UpdateMechanism } from 'typings/Settings/General'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import translate from 'Utilities/String/translate'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i; + +function createUpdatesSelector() { + return createSelector( + (state: AppState) => state.system.updates, + (state: AppState) => state.settings.general, + (updates, generalSettings) => { + const { error: updatesError, items } = updates; + + const isFetching = updates.isFetching || generalSettings.isFetching; + const isPopulated = updates.isPopulated && generalSettings.isPopulated; + + return { + isFetching, + isPopulated, + updatesError, + generalSettingsError: generalSettings.error, + items, + updateMechanism: generalSettings.item.updateMechanism, + }; + } + ); +} + +function Updates() { + const currentVersion = useSelector((state: AppState) => state.app.version); + const { packageUpdateMechanismMessage } = useSelector( + createSystemStatusSelector() + ); + const { shortDateFormat, longDateFormat, timeFormat } = useSelector( + createUISettingsSelector() + ); + const isInstallingUpdate = useSelector( + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE) + ); + + const { + isFetching, + isPopulated, + updatesError, + generalSettingsError, + items, + updateMechanism, + } = useSelector(createUpdatesSelector()); + + const dispatch = useDispatch(); + const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false); + const hasError = !!(updatesError || generalSettingsError); + const hasUpdates = isPopulated && !hasError && items.length > 0; + const noUpdates = isPopulated && !hasError && !items.length; + + const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError'); + const externalUpdaterMessages: Partial> = { + external: translate('ExternalUpdater'), + apt: translate('AptUpdater'), + docker: translate('DockerUpdater'), + }; + + const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => { + const majorVersion = parseInt( + currentVersion.match(VERSION_REGEX)?.[0] ?? '0' + ); + + const latestVersion = items[0]?.version; + const latestMajorVersion = parseInt( + latestVersion?.match(VERSION_REGEX)?.[0] ?? '0' + ); + + return { + isMajorUpdate: latestMajorVersion > majorVersion, + hasUpdateToInstall: items.some( + (update) => update.installable && update.latest + ), + }; + }, [currentVersion, items]); + + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + const handleInstallLatestPress = useCallback(() => { + if (isMajorUpdate) { + setIsMajorUpdateModalOpen(true); + } else { + dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE })); + } + }, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]); + + const handleInstallLatestMajorVersionPress = useCallback(() => { + setIsMajorUpdateModalOpen(false); + + dispatch( + executeCommand({ + name: commandNames.APPLICATION_UPDATE, + installMajorUpdate: true, + }) + ); + }, [setIsMajorUpdateModalOpen, dispatch]); + + const handleCancelMajorVersionPress = useCallback(() => { + setIsMajorUpdateModalOpen(false); + }, [setIsMajorUpdateModalOpen]); + + useEffect(() => { + dispatch(fetchUpdates()); + dispatch(fetchGeneralSettings()); + }, [dispatch]); + + return ( + + + {isPopulated || hasError ? null : } + + {noUpdates ? ( + {translate('NoUpdatesAreAvailable')} + ) : null} + + {hasUpdateToInstall ? ( +
+ {updateMechanism === 'builtIn' || updateMechanism === 'script' ? ( + + {translate('InstallLatest')} + + ) : ( + <> + + +
+ {externalUpdaterPrefix}{' '} + +
+ + )} + + {isFetching ? ( + + ) : null} +
+ ) : null} + + {noUpdateToInstall && ( +
+ +
{translate('OnLatestVersion')}
+ + {isFetching && ( + + )} +
+ )} + + {hasUpdates && ( +
+ {items.map((update) => { + return ( +
+
+
{update.version}
+
+
+ {formatDate(update.releaseDate, shortDateFormat)} +
+ + {update.branch === 'main' ? null : ( + + )} + + {update.version === currentVersion ? ( + + ) : null} + + {update.version !== currentVersion && update.installedOn ? ( + + ) : null} +
+ + {update.changes ? ( +
+ + + +
+ ) : ( +
{translate('MaintenanceRelease')}
+ )} +
+ ); + })} +
+ )} + + {updatesError ? ( + + {translate('FailedToFetchUpdates')} + + ) : null} + + {generalSettingsError ? ( + + {translate('FailedToUpdateSettings')} + + ) : null} + + +
{translate('InstallMajorVersionUpdateMessage')}
+
+ +
+ + } + confirmLabel={translate('Install')} + onConfirm={handleInstallLatestMajorVersionPress} + onCancel={handleCancelMajorVersionPress} + /> +
+
+ ); +} + +export default Updates; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js deleted file mode 100644 index 38873a990..000000000 --- a/frontend/src/System/Updates/UpdatesConnector.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; -import { fetchUpdates } from 'Store/Actions/systemActions'; -import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; -import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import Updates from './Updates'; - -function createMapStateToProps() { - return createSelector( - (state) => state.app.version, - createSystemStatusSelector(), - (state) => state.system.updates, - (state) => state.settings.general, - createUISettingsSelector(), - createSystemStatusSelector(), - createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), - ( - currentVersion, - status, - updates, - generalSettings, - uiSettings, - systemStatus, - isInstallingUpdate - ) => { - const { - error: updatesError, - items - } = updates; - - const isFetching = updates.isFetching || generalSettings.isFetching; - const isPopulated = updates.isPopulated && generalSettings.isPopulated; - - return { - currentVersion, - isFetching, - isPopulated, - updatesError, - generalSettingsError: generalSettings.error, - items, - isInstallingUpdate, - isDocker: systemStatus.isDocker, - updateMechanism: generalSettings.item.updateMechanism, - updateMechanismMessage: status.packageUpdateMechanismMessage, - shortDateFormat: uiSettings.shortDateFormat, - longDateFormat: uiSettings.longDateFormat, - timeFormat: uiSettings.timeFormat - }; - } - ); -} - -const mapDispatchToProps = { - dispatchFetchUpdates: fetchUpdates, - dispatchFetchGeneralSettings: fetchGeneralSettings, - dispatchExecuteCommand: executeCommand -}; - -class UpdatesConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - this.props.dispatchFetchUpdates(); - this.props.dispatchFetchGeneralSettings(); - } - - // - // Listeners - - onInstallLatestPress = () => { - this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE }); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -UpdatesConnector.propTypes = { - dispatchFetchUpdates: PropTypes.func.isRequired, - dispatchFetchGeneralSettings: PropTypes.func.isRequired, - dispatchExecuteCommand: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector); diff --git a/frontend/src/typings/Settings/General.ts b/frontend/src/typings/Settings/General.ts new file mode 100644 index 000000000..c867bed74 --- /dev/null +++ b/frontend/src/typings/Settings/General.ts @@ -0,0 +1,45 @@ +export type UpdateMechanism = + | 'builtIn' + | 'script' + | 'external' + | 'apt' + | 'docker'; + +export default interface General { + bindAddress: string; + port: number; + sslPort: number; + enableSsl: boolean; + launchBrowser: boolean; + authenticationMethod: string; + authenticationRequired: string; + analyticsEnabled: boolean; + username: string; + password: string; + passwordConfirmation: string; + logLevel: string; + consoleLogLevel: string; + branch: string; + apiKey: string; + sslCertPath: string; + sslCertPassword: string; + urlBase: string; + instanceName: string; + applicationUrl: string; + updateAutomatically: boolean; + updateMechanism: UpdateMechanism; + updateScriptPath: string; + proxyEnabled: boolean; + proxyType: string; + proxyHostname: string; + proxyPort: number; + proxyUsername: string; + proxyPassword: string; + proxyBypassFilter: string; + proxyBypassLocalAddresses: boolean; + certificateValidation: string; + backupFolder: string; + backupInterval: number; + backupRetention: number; + id: number; +} diff --git a/frontend/src/typings/UiSettings.ts b/frontend/src/typings/Settings/UiSettings.ts similarity index 79% rename from frontend/src/typings/UiSettings.ts rename to frontend/src/typings/Settings/UiSettings.ts index 3c23a0356..656c4518b 100644 --- a/frontend/src/typings/UiSettings.ts +++ b/frontend/src/typings/Settings/UiSettings.ts @@ -1,4 +1,4 @@ -export interface UiSettings { +export default interface UiSettings { theme: 'auto' | 'dark' | 'light'; showRelativeDates: boolean; shortDateFormat: string; diff --git a/frontend/src/typings/SystemStatus.ts b/frontend/src/typings/SystemStatus.ts index 668efbfbf..d5eab3ca3 100644 --- a/frontend/src/typings/SystemStatus.ts +++ b/frontend/src/typings/SystemStatus.ts @@ -22,6 +22,7 @@ interface SystemStatus { osVersion: string; packageAuthor: string; packageUpdateMechanism: string; + packageUpdateMechanismMessage: string; packageVersion: string; runtimeName: string; runtimeVersion: string; diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs index a1f68609d..5f2aa2662 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdateServiceFixture.cs @@ -35,18 +35,18 @@ namespace NzbDrone.Core.Test.UpdateTests { _updatePackage = new UpdatePackage { - FileName = "NzbDrone.develop.2.0.0.0.tar.gz", + FileName = "NzbDrone.develop.1.0.0.0.tar.gz", Url = "http://download.sonarr.tv/v2/develop/mono/NzbDrone.develop.tar.gz", - Version = new Version("2.0.0.0") + Version = new Version("1.0.0.0") }; } else { _updatePackage = new UpdatePackage { - FileName = "NzbDrone.develop.2.0.0.0.zip", + FileName = "NzbDrone.develop.1.0.0.0.zip", Url = "http://download.sonarr.tv/v2/develop/windows/NzbDrone.develop.zip", - Version = new Version("2.0.0.0") + Version = new Version("1.0.0.0") }; } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 1ec6070f2..9243b7905 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -445,6 +445,11 @@ "Info": "Info", "InfoUrl": "Info URL", "InitialFailure": "Initial Failure", + "Install": "Install", + "InstallLatest": "Install Latest", + "InstallMajorVersionUpdate": "Install Update", + "InstallMajorVersionUpdateMessage": "This update will install a new major version and may not be compatible with your system. Are you sure you want to install this update?", + "InstallMajorVersionUpdateMessageLink": "Please check [{domain}]({url}) for more information.", "InstanceName": "Instance Name", "InstanceNameHelpText": "Instance name in tab and for Syslog app name", "InteractiveSearch": "Interactive Search", diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs index ece18a111..fa2cfbf41 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCheckCommand.cs @@ -7,5 +7,7 @@ namespace NzbDrone.Core.Update.Commands public override bool SendUpdatesToClient => true; public override string CompletionMessage => null; + + public bool InstallMajorUpdate { get; set; } } } diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index 59a827a0b..6980af708 100644 --- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -4,6 +4,7 @@ namespace NzbDrone.Core.Update.Commands { public class ApplicationUpdateCommand : Command { + public bool InstallMajorUpdate { get; set; } public override bool SendUpdatesToClient => true; public override bool IsExclusive => true; } diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index d02cd3803..793e1a8ac 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -231,7 +231,7 @@ namespace NzbDrone.Core.Update } } - private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger) + private UpdatePackage GetUpdatePackage(CommandTrigger updateTrigger, bool installMajorUpdate) { _logger.ProgressDebug("Checking for updates"); @@ -243,7 +243,13 @@ namespace NzbDrone.Core.Update return null; } - if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && updateTrigger != CommandTrigger.Manual) + if (latestAvailable.Version.Major > BuildInfo.Version.Major && !installMajorUpdate) + { + _logger.ProgressInfo("Unable to install major update, please update update manually from System: Updates"); + return null; + } + + if (!_configFileProvider.UpdateAutomatically && updateTrigger != CommandTrigger.Manual) { _logger.ProgressDebug("Auto-update not enabled, not installing available update."); return null; @@ -272,7 +278,7 @@ namespace NzbDrone.Core.Update public void Execute(ApplicationUpdateCheckCommand message) { - if (GetUpdatePackage(message.Trigger) != null) + if (GetUpdatePackage(message.Trigger, true) != null) { _commandQueueManager.Push(new ApplicationUpdateCommand(), trigger: message.Trigger); } @@ -280,7 +286,7 @@ namespace NzbDrone.Core.Update public void Execute(ApplicationUpdateCommand message) { - var latestAvailable = GetUpdatePackage(message.Trigger); + var latestAvailable = GetUpdatePackage(message.Trigger, message.InstallMajorUpdate); if (latestAvailable != null) { diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index 17743c8b0..58d9872cc 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -47,6 +47,7 @@ namespace NzbDrone.Core.Update .AddQueryParam("runtime", "netcore") .AddQueryParam("runtimeVer", _platformInfo.Version) .AddQueryParam("dbType", _mainDatabase.DatabaseType) + .AddQueryParam("includeMajorVersion", true) .SetSegment("branch", branch); if (_analyticsService.IsEnabled)