diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index f50ff1a7c..c76dc6339 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; +import titleCase from 'Utilities/String/titleCase'; import { inputTypes, sizes } from 'Helpers/Props'; import FieldSet from 'Components/FieldSet'; import FormGroup from 'Components/Form/FormGroup'; @@ -11,6 +12,7 @@ function UpdateSettings(props) { advancedSettings, settings, isWindows, + packageUpdateMechanism, onInputChange } = props; @@ -25,10 +27,20 @@ function UpdateSettings(props) { return null; } - const updateOptions = [ - { key: 'builtIn', value: 'Built-In' }, - { key: 'script', value: 'Script' } - ]; + const usingExternalUpdateMechanism = packageUpdateMechanism !== 'builtIn'; + + const updateOptions = []; + + if (usingExternalUpdateMechanism) { + updateOptions.push({ + key: packageUpdateMechanism, + value: titleCase(packageUpdateMechanism) + }); + } else { + updateOptions.push({ key: 'builtIn', value: 'Built-In' }); + } + + updateOptions.push({ key: 'script', value: 'Script' }); return (
@@ -41,10 +53,11 @@ function UpdateSettings(props) { @@ -111,6 +124,7 @@ UpdateSettings.propTypes = { advancedSettings: PropTypes.bool.isRequired, settings: PropTypes.object.isRequired, isWindows: PropTypes.bool.isRequired, + packageUpdateMechanism: PropTypes.string.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js index 7751068a9..d04e8b99e 100644 --- a/frontend/src/System/Status/About/About.js +++ b/frontend/src/System/Status/About/About.js @@ -15,6 +15,8 @@ class About extends Component { render() { const { version, + packageVersion, + packageAuthor, isNetCore, isMono, isDocker, @@ -36,6 +38,14 @@ class About extends Component { data={version} /> + { + packageVersion && + + } + { isMono && @@ -52,9 +59,9 @@ class Updates extends Component { { hasUpdateToInstall && -
+
{ - !isDocker && + (updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ? Install Latest - - } + : - { - isDocker && -
- An update is available. Please update your Docker image and re-create the container. -
+ + + +
+ {externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} +
+
} { diff --git a/src/NzbDrone.Core/Configuration/DeploymentInfoProvider.cs b/src/NzbDrone.Core/Configuration/DeploymentInfoProvider.cs new file mode 100644 index 000000000..560699b4b --- /dev/null +++ b/src/NzbDrone.Core/Configuration/DeploymentInfoProvider.cs @@ -0,0 +1,105 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Update; + +namespace NzbDrone.Core.Configuration +{ + public interface IDeploymentInfoProvider + { + string PackageVersion { get; } + string PackageAuthor { get; } + string PackageBranch { get; } + UpdateMechanism PackageUpdateMechanism { get; } + + string ReleaseVersion { get; } + string ReleaseBranch { get; } + + bool IsExternalUpdateMechanism { get; } + UpdateMechanism DefaultUpdateMechanism { get; } + string DefaultBranch { get; } + } + + public class DeploymentInfoProvider : IDeploymentInfoProvider + { + public DeploymentInfoProvider(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) + { + var bin = appFolderInfo.StartUpFolder; + var packageInfoPath = Path.Combine(bin, "..", "package_info"); + var releaseInfoPath = Path.Combine(bin, "release_info"); + + PackageUpdateMechanism = UpdateMechanism.BuiltIn; + DefaultBranch = "aphrodite"; + + if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath)) + { + var data = diskProvider.ReadAllText(packageInfoPath); + + PackageVersion = ReadValue(data, "PackageVersion"); + PackageAuthor = ReadValue(data, "PackageAuthor"); + PackageUpdateMechanism = ReadEnumValue(data, "UpdateMethod", UpdateMechanism.BuiltIn); + PackageBranch = ReadValue(data, "Branch"); + + ReleaseVersion = ReadValue(data, "ReleaseVersion"); + + if (PackageBranch.IsNotNullOrWhiteSpace()) + { + DefaultBranch = PackageBranch; + } + } + + if (diskProvider.FileExists(releaseInfoPath)) + { + var data = diskProvider.ReadAllText(releaseInfoPath); + + ReleaseVersion = ReadValue(data, "ReleaseVersion", ReleaseVersion); + ReleaseBranch = ReadValue(data, "Branch"); + + if (ReleaseBranch.IsNotNullOrWhiteSpace()) + { + DefaultBranch = ReleaseBranch; + } + } + + DefaultUpdateMechanism = PackageUpdateMechanism; + } + + private static string ReadValue(string fileData, string key, string defaultValue = null) + { + var match = Regex.Match(fileData, "^" + key + "=(.*)$", RegexOptions.Multiline); + if (match.Success) + { + return match.Groups[1].Value.Trim(); + } + + return defaultValue; + } + + private static T ReadEnumValue(string fileData, string key, T defaultValue) + where T : struct + { + var value = ReadValue(fileData, key); + if (value != null && Enum.TryParse(value, true, out var result)) + { + return result; + } + + return defaultValue; + } + + public string PackageVersion { get; private set; } + public string PackageAuthor { get; private set; } + public string PackageBranch { get; private set; } + public UpdateMechanism PackageUpdateMechanism { get; private set; } + + public string ReleaseVersion { get; private set; } + public string ReleaseBranch { get; set; } + + public bool IsExternalUpdateMechanism => PackageUpdateMechanism >= UpdateMechanism.External; + public UpdateMechanism DefaultUpdateMechanism { get; private set; } + public string DefaultBranch { get; private set; } + } +} diff --git a/src/NzbDrone.Core/Update/ConfigureUpdateMechanism.cs b/src/NzbDrone.Core/Update/ConfigureUpdateMechanism.cs new file mode 100644 index 000000000..1f001d280 --- /dev/null +++ b/src/NzbDrone.Core/Update/ConfigureUpdateMechanism.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Update +{ + public interface IUpdaterConfigProvider + { + } + + public class UpdaterConfigProvider : IUpdaterConfigProvider, IHandle + { + private Logger _logger; + private IConfigFileProvider _configFileProvider; + private IDeploymentInfoProvider _deploymentInfoProvider; + + public UpdaterConfigProvider(IDeploymentInfoProvider deploymentInfoProvider, IConfigFileProvider configFileProvider, Logger logger) + { + _deploymentInfoProvider = deploymentInfoProvider; + _configFileProvider = configFileProvider; + _logger = logger; + } + + public void Handle(ApplicationStartedEvent message) + { + var updateMechanism = _configFileProvider.UpdateMechanism; + var packageUpdateMechanism = _deploymentInfoProvider.PackageUpdateMechanism; + + var externalMechanisms = Enum.GetValues(typeof(UpdateMechanism)) + .Cast() + .Where(v => v >= UpdateMechanism.External) + .ToArray(); + + foreach (var externalMechanism in externalMechanisms) + { + if ((packageUpdateMechanism != externalMechanism && updateMechanism == externalMechanism) || + (packageUpdateMechanism == externalMechanism && updateMechanism == UpdateMechanism.BuiltIn)) + { + _logger.Info("Update mechanism {0} not supported in the current configuration, changing to {1}.", updateMechanism, packageUpdateMechanism); + ChangeUpdateMechanism(packageUpdateMechanism); + break; + } + } + + if (_deploymentInfoProvider.IsExternalUpdateMechanism) + { + var currentBranch = _configFileProvider.Branch; + var packageBranch = _deploymentInfoProvider.PackageBranch; + if (packageBranch.IsNotNullOrWhiteSpace() & packageBranch != currentBranch) + { + _logger.Info("External updater uses branch {0} instead of the currently selected {1}, changing to {0}.", packageBranch, currentBranch); + ChangeBranch(packageBranch); + } + } + } + + private void ChangeUpdateMechanism(UpdateMechanism updateMechanism) + { + var config = new Dictionary + { + [nameof(_configFileProvider.UpdateMechanism)] = updateMechanism + }; + _configFileProvider.SaveConfigDictionary(config); + } + + private void ChangeBranch(string branch) + { + var config = new Dictionary + { + [nameof(_configFileProvider.Branch)] = branch + }; + _configFileProvider.SaveConfigDictionary(config); + } + } +} diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index ea644e2c3..413bf718f 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using NLog; @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Update private readonly IProcessProvider _processProvider; private readonly IVerifyUpdates _updateVerifier; private readonly IStartupContext _startupContext; + private readonly IDeploymentInfoProvider _deploymentInfoProvider; private readonly IConfigFileProvider _configFileProvider; private readonly IRuntimeInfo _runtimeInfo; private readonly IBackupService _backupService; @@ -43,6 +44,7 @@ namespace NzbDrone.Core.Update IProcessProvider processProvider, IVerifyUpdates updateVerifier, IStartupContext startupContext, + IDeploymentInfoProvider deploymentInfoProvider, IConfigFileProvider configFileProvider, IRuntimeInfo runtimeInfo, IBackupService backupService, @@ -63,6 +65,7 @@ namespace NzbDrone.Core.Update _processProvider = processProvider; _updateVerifier = updateVerifier; _startupContext = startupContext; + _deploymentInfoProvider = deploymentInfoProvider; _configFileProvider = configFileProvider; _runtimeInfo = runtimeInfo; _backupService = backupService; @@ -230,6 +233,18 @@ namespace NzbDrone.Core.Update return; } + // Safety net, ConfigureUpdateMechanism should take care of invalid settings + if (_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn && _deploymentInfoProvider.IsExternalUpdateMechanism) + { + _logger.ProgressDebug("Built-In updater disabled, please use {0} to install", _deploymentInfoProvider.PackageUpdateMechanism); + return; + } + else if (_configFileProvider.UpdateMechanism != UpdateMechanism.Script && _deploymentInfoProvider.IsExternalUpdateMechanism) + { + _logger.ProgressDebug("Update available, please use {0} to install", _deploymentInfoProvider.PackageUpdateMechanism); + return; + } + try { InstallUpdate(latestAvailable); diff --git a/src/NzbDrone.Core/Update/UpdateMechanism.cs b/src/NzbDrone.Core/Update/UpdateMechanism.cs index 8b647a1e7..f87dca79c 100644 --- a/src/NzbDrone.Core/Update/UpdateMechanism.cs +++ b/src/NzbDrone.Core/Update/UpdateMechanism.cs @@ -1,8 +1,11 @@ -namespace NzbDrone.Core.Update +namespace NzbDrone.Core.Update { public enum UpdateMechanism { BuiltIn = 0, - Script = 1 + Script = 1, + External = 10, + Apt = 11, + Docker = 12 } } diff --git a/src/Radarr.Api.V3/System/SystemModule.cs b/src/Radarr.Api.V3/System/SystemModule.cs index 7a3b27bb2..d1c962268 100644 --- a/src/Radarr.Api.V3/System/SystemModule.cs +++ b/src/Radarr.Api.V3/System/SystemModule.cs @@ -18,6 +18,7 @@ namespace Radarr.Api.V3.System private readonly IConfigFileProvider _configFileProvider; private readonly IMainDatabase _database; private readonly ILifecycleService _lifecycleService; + private readonly IDeploymentInfoProvider _deploymentInfoProvider; public SystemModule(IAppFolderInfo appFolderInfo, IRuntimeInfo runtimeInfo, @@ -26,7 +27,8 @@ namespace Radarr.Api.V3.System IRouteCacheProvider routeCacheProvider, IConfigFileProvider configFileProvider, IMainDatabase database, - ILifecycleService lifecycleService) + ILifecycleService lifecycleService, + IDeploymentInfoProvider deploymentInfoProvider) : base("system") { _appFolderInfo = appFolderInfo; @@ -37,6 +39,7 @@ namespace Radarr.Api.V3.System _configFileProvider = configFileProvider; _database = database; _lifecycleService = lifecycleService; + _deploymentInfoProvider = deploymentInfoProvider; Get("/status", x => GetStatus()); Get("/routes", x => GetRoutes()); Post("/shutdown", x => Shutdown()); @@ -71,7 +74,10 @@ namespace Radarr.Api.V3.System UrlBase = _configFileProvider.UrlBase, RuntimeVersion = _platformInfo.Version, RuntimeName = PlatformInfo.Platform, - StartTime = _runtimeInfo.StartTime + StartTime = _runtimeInfo.StartTime, + PackageVersion = _deploymentInfoProvider.PackageVersion, + PackageAuthor = _deploymentInfoProvider.PackageAuthor, + PackageUpdateMechanism = _deploymentInfoProvider.PackageUpdateMechanism }; }