From 971d4b7e494c2b7bc5520b7de1564bd876dfcd53 Mon Sep 17 00:00:00 2001 From: Ben Olden-Cooligan Date: Sun, 16 Sep 2018 13:53:45 -0400 Subject: [PATCH] Auto update WIP --- .../Automation/ConsoleOperationProgress.cs | 4 +- NAPS2.Core/Config/UserConfig.cs | 2 + .../Lang/Resources/MiscResources.Designer.cs | 45 ++++++++ NAPS2.Core/Lang/Resources/MiscResources.resx | 15 +++ NAPS2.Core/NAPS2.Core.csproj | 4 + NAPS2.Core/Operation/OperationProgressType.cs | 12 ++ NAPS2.Core/Operation/OperationStatus.cs | 2 + NAPS2.Core/Update/UpdateChecker.cs | 85 ++++++++++++++ NAPS2.Core/Update/UpdateInfo.cs | 17 +++ NAPS2.Core/Update/UpdateOperation.cs | 74 ++++++++++++ NAPS2.Core/WinForms/FAbout.Designer.cs | 27 +++++ NAPS2.Core/WinForms/FAbout.cs | 92 ++++++++++++++- NAPS2.Core/WinForms/FAbout.resx | 108 +++++++++++++++--- NAPS2.Core/WinForms/FProgress.cs | 4 +- 14 files changed, 473 insertions(+), 18 deletions(-) create mode 100644 NAPS2.Core/Operation/OperationProgressType.cs create mode 100644 NAPS2.Core/Update/UpdateChecker.cs create mode 100644 NAPS2.Core/Update/UpdateInfo.cs create mode 100644 NAPS2.Core/Update/UpdateOperation.cs diff --git a/NAPS2.Core/Automation/ConsoleOperationProgress.cs b/NAPS2.Core/Automation/ConsoleOperationProgress.cs index 0b87bcaa6..99bfd9592 100644 --- a/NAPS2.Core/Automation/ConsoleOperationProgress.cs +++ b/NAPS2.Core/Automation/ConsoleOperationProgress.cs @@ -16,7 +16,9 @@ namespace NAPS2.Automation op.Wait(); } - public void ShowModalProgress(IOperation op) => throw new InvalidOperationException(); + public void ShowModalProgress(IOperation op) + { + } public void ShowBackgroundProgress(IOperation op) { } diff --git a/NAPS2.Core/Config/UserConfig.cs b/NAPS2.Core/Config/UserConfig.cs index b3c60448f..625a85af1 100644 --- a/NAPS2.Core/Config/UserConfig.cs +++ b/NAPS2.Core/Config/UserConfig.cs @@ -24,6 +24,8 @@ namespace NAPS2.Config public HashSet BackgroundOperations { get; set; } = new HashSet(); + public bool CheckForUpdates { get; set; } + public DateTime? LastUpdateCheckDate { get; set; } public DateTime? FirstRunDate { get; set; } diff --git a/NAPS2.Core/Lang/Resources/MiscResources.Designer.cs b/NAPS2.Core/Lang/Resources/MiscResources.Designer.cs index 581a49f24..9e279dfd7 100644 --- a/NAPS2.Core/Lang/Resources/MiscResources.Designer.cs +++ b/NAPS2.Core/Lang/Resources/MiscResources.Designer.cs @@ -213,6 +213,15 @@ namespace NAPS2.Lang.Resources { } } + /// + /// Looks up a localized string similar to Checking.... + /// + internal static string CheckingForUpdates { + get { + return ResourceManager.GetString("CheckingForUpdates", resourceCulture); + } + } + /// /// Looks up a localized string similar to Choose Profile. /// @@ -726,6 +735,15 @@ namespace NAPS2.Lang.Resources { } } + /// + /// Looks up a localized string similar to Install {0}. + /// + internal static string Install { + get { + return ResourceManager.GetString("Install", resourceCulture); + } + } + /// /// Looks up a localized string similar to Installation Complete. /// @@ -843,6 +861,15 @@ namespace NAPS2.Lang.Resources { } } + /// + /// Looks up a localized string similar to No updates available.. + /// + internal static string NoUpdates { + get { + return ResourceManager.GetString("NoUpdates", resourceCulture); + } + } + /// /// Looks up a localized string similar to OCR Progress. /// @@ -1086,6 +1113,24 @@ namespace NAPS2.Lang.Resources { } } + /// + /// Looks up a localized string similar to Update Progress. + /// + internal static string UpdateProgress { + get { + return ResourceManager.GetString("UpdateProgress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updating.... + /// + internal static string Updating { + get { + return ResourceManager.GetString("Updating", resourceCulture); + } + } + /// /// Looks up a localized string similar to Version {0}. /// diff --git a/NAPS2.Core/Lang/Resources/MiscResources.resx b/NAPS2.Core/Lang/Resources/MiscResources.resx index 65abb67d4..3eb80c73f 100644 --- a/NAPS2.Core/Lang/Resources/MiscResources.resx +++ b/NAPS2.Core/Lang/Resources/MiscResources.resx @@ -462,4 +462,19 @@ Running OCR... + + Update Progress + + + Updating... + + + No updates available. + + + Checking... + + + Install {0} + \ No newline at end of file diff --git a/NAPS2.Core/NAPS2.Core.csproj b/NAPS2.Core/NAPS2.Core.csproj index cd8a16437..076fbe96f 100644 --- a/NAPS2.Core/NAPS2.Core.csproj +++ b/NAPS2.Core/NAPS2.Core.csproj @@ -167,6 +167,7 @@ + @@ -191,6 +192,9 @@ + + + diff --git a/NAPS2.Core/Operation/OperationProgressType.cs b/NAPS2.Core/Operation/OperationProgressType.cs new file mode 100644 index 000000000..4f90630f3 --- /dev/null +++ b/NAPS2.Core/Operation/OperationProgressType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NAPS2.Operation +{ + public enum OperationProgressType + { + Default, + MB + } +} \ No newline at end of file diff --git a/NAPS2.Core/Operation/OperationStatus.cs b/NAPS2.Core/Operation/OperationStatus.cs index e3a941d92..4b3197837 100644 --- a/NAPS2.Core/Operation/OperationStatus.cs +++ b/NAPS2.Core/Operation/OperationStatus.cs @@ -16,5 +16,7 @@ namespace NAPS2.Operation public int MaxProgress { get; set; } public bool IndeterminateProgress { get; set; } + + public OperationProgressType ProgressType { get; set; } } } diff --git a/NAPS2.Core/Update/UpdateChecker.cs b/NAPS2.Core/Update/UpdateChecker.cs new file mode 100644 index 000000000..955f6a3ed --- /dev/null +++ b/NAPS2.Core/Update/UpdateChecker.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using NAPS2.Operation; +using Newtonsoft.Json.Linq; + +namespace NAPS2.Update +{ + public class UpdateChecker + { + private const string UPDATE_CHECK_ENDPOINT = "https://www.naps2.com/api/v1/update"; +#if STANDALONE + private const string UPDATE_FILE_EXT = "zip"; +#elif INSTALLER_MSI + private const string UPDATE_FILE_EXT = "msi"; +#else + private const string UPDATE_FILE_EXT = "exe"; +#endif + + private readonly IOperationFactory operationFactory; + private readonly IOperationProgress operationProgress; + + public UpdateChecker(IOperationFactory operationFactory, IOperationProgress operationProgress) + { + this.operationFactory = operationFactory; + this.operationProgress = operationProgress; + } + + public async Task CheckForUpdates() + { + var json = await GetJson(UPDATE_CHECK_ENDPOINT); + var currentVersion = Assembly.GetExecutingAssembly().GetName().Version; + foreach (var release in json.Value("versions")) + { + var versionName = release.Value("name"); + var version = ParseVersion(versionName); + var gte = release["requires"].Value("gte"); + var gteVersion = gte != null ? ParseVersion(gte) : null; + if ((gteVersion == null || currentVersion >= gteVersion))// && currentVersion < version) + { + var updateFile = release["files"].Value(UPDATE_FILE_EXT); + if (updateFile != null) + { + return new UpdateInfo + { + Name = versionName, + DownloadUrl = updateFile.Value("url"), + Sha1 = Convert.FromBase64String(updateFile.Value("sha1")), + Signature = Convert.FromBase64String(updateFile.Value("sig")) + }; + } + } + } + return null; + } + + public UpdateOperation StartUpdate(UpdateInfo update) + { + var op = operationFactory.Create(); + op.Start(update); + operationProgress.ShowModalProgress(op); + return op; + } + + private Version ParseVersion(string name) + { + return Version.Parse(name.Replace("b", ".")); + } + + private async Task GetJson(string url) + { + return await Task.Factory.StartNew(() => + { + using (var client = new WebClient()) + { + var response = client.DownloadString(url); + return JObject.Parse(response); + } + }); + } + } +} diff --git a/NAPS2.Core/Update/UpdateInfo.cs b/NAPS2.Core/Update/UpdateInfo.cs new file mode 100644 index 000000000..773e5495a --- /dev/null +++ b/NAPS2.Core/Update/UpdateInfo.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NAPS2.Update +{ + public class UpdateInfo + { + public string Name { get; set; } + + public string DownloadUrl { get; set; } + + public byte[] Sha1 { get; set; } + + public byte[] Signature { get; set; } + } +} diff --git a/NAPS2.Core/Update/UpdateOperation.cs b/NAPS2.Core/Update/UpdateOperation.cs new file mode 100644 index 000000000..4700998c0 --- /dev/null +++ b/NAPS2.Core/Update/UpdateOperation.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Windows.Forms; +using NAPS2.Lang.Resources; +using NAPS2.Operation; + +namespace NAPS2.Update +{ + public class UpdateOperation : OperationBase + { + private WebClient client; + private UpdateInfo update; + private string tempFolder; + private string tempPath; + + static UpdateOperation() + { + try + { + const int tls12 = 3072; + ServicePointManager.SecurityProtocol = (SecurityProtocolType)tls12; + } + catch (NotSupportedException) + { + } + } + + public UpdateOperation() + { + ProgressTitle = MiscResources.UpdateProgress; + AllowBackground = true; + AllowCancel = true; + Status = new OperationStatus + { + StatusText = MiscResources.Updating, + ProgressType = OperationProgressType.MB + }; + } + + public void Start(UpdateInfo updateInfo) + { + update = updateInfo; + tempFolder = Path.Combine(Paths.Temp, Path.GetRandomFileName()); + Directory.CreateDirectory(tempFolder); + tempPath = Path.Combine(tempFolder, updateInfo.DownloadUrl.Substring(updateInfo.DownloadUrl.LastIndexOf('/') + 1)); + client = new WebClient(); + client.DownloadProgressChanged += DownloadProgress; + client.DownloadFileCompleted += DownloadCompleted; + client.DownloadFileAsync(new Uri(updateInfo.DownloadUrl), tempPath); + } + + private void DownloadCompleted(object sender, AsyncCompletedEventArgs e) + { + // TODO: Verify sha1/sig + // TODO: Standalone install + Process.Start(tempPath); + // TODO: Clean up temp file somehow + InvokeFinished(); + Application.OpenForms.OfType
().FirstOrDefault()?.Close(); + } + + private void DownloadProgress(object sender, DownloadProgressChangedEventArgs e) + { + Status.CurrentProgress = (int)e.BytesReceived; + Status.MaxProgress = (int)e.TotalBytesToReceive; + InvokeStatusChanged(); + } + } +} diff --git a/NAPS2.Core/WinForms/FAbout.Designer.cs b/NAPS2.Core/WinForms/FAbout.Designer.cs index 820592c16..c1cbd9998 100644 --- a/NAPS2.Core/WinForms/FAbout.Designer.cs +++ b/NAPS2.Core/WinForms/FAbout.Designer.cs @@ -41,6 +41,9 @@ namespace NAPS2.WinForms this.label1 = new System.Windows.Forms.Label(); this.linkLabel2 = new System.Windows.Forms.LinkLabel(); this.btnDonate = new System.Windows.Forms.PictureBox(); + this.cbCheckForUpdates = new System.Windows.Forms.CheckBox(); + this.lblUpdateStatus = new System.Windows.Forms.Label(); + this.linkInstall = new System.Windows.Forms.LinkLabel(); ((System.ComponentModel.ISupportInitialize)(this.logoPictureBox)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.btnDonate)).BeginInit(); this.SuspendLayout(); @@ -100,10 +103,31 @@ namespace NAPS2.WinForms this.btnDonate.TabStop = false; this.btnDonate.Click += new System.EventHandler(this.btnDonate_Click); // + // cbCheckForUpdates + // + resources.ApplyResources(this.cbCheckForUpdates, "cbCheckForUpdates"); + this.cbCheckForUpdates.Name = "cbCheckForUpdates"; + this.cbCheckForUpdates.UseVisualStyleBackColor = true; + this.cbCheckForUpdates.CheckedChanged += new System.EventHandler(this.cbCheckForUpdates_CheckedChanged); + // + // lblUpdateStatus + // + resources.ApplyResources(this.lblUpdateStatus, "lblUpdateStatus"); + this.lblUpdateStatus.Name = "lblUpdateStatus"; + // + // linkInstall + // + resources.ApplyResources(this.linkInstall, "linkInstall"); + this.linkInstall.Name = "linkInstall"; + this.linkInstall.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkInstall_LinkClicked); + // // FAbout // resources.ApplyResources(this, "$this"); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.linkInstall); + this.Controls.Add(this.lblUpdateStatus); + this.Controls.Add(this.cbCheckForUpdates); this.Controls.Add(this.btnDonate); this.Controls.Add(this.linkLabel2); this.Controls.Add(this.label1); @@ -135,5 +159,8 @@ namespace NAPS2.WinForms private System.Windows.Forms.Label label1; private System.Windows.Forms.LinkLabel linkLabel2; private System.Windows.Forms.PictureBox btnDonate; + private System.Windows.Forms.CheckBox cbCheckForUpdates; + private System.Windows.Forms.Label lblUpdateStatus; + private System.Windows.Forms.LinkLabel linkInstall; } } diff --git a/NAPS2.Core/WinForms/FAbout.cs b/NAPS2.Core/WinForms/FAbout.cs index deb37277d..5c01f6037 100644 --- a/NAPS2.Core/WinForms/FAbout.cs +++ b/NAPS2.Core/WinForms/FAbout.cs @@ -7,13 +7,24 @@ using System.Reflection; using System.Windows.Forms; using NAPS2.Config; using NAPS2.Lang.Resources; +using NAPS2.Update; +using NAPS2.Util; namespace NAPS2.WinForms { partial class FAbout : FormBase { - public FAbout(AppConfigManager appConfigManager) + private readonly IUserConfigManager userConfigManager; + private readonly UpdateChecker updateChecker; + + private bool hasCheckedForUpdates; + private UpdateInfo update; + + public FAbout(AppConfigManager appConfigManager, IUserConfigManager userConfigManager, UpdateChecker updateChecker) { + this.userConfigManager = userConfigManager; + this.updateChecker = updateChecker; + RestoreFormState = false; InitializeComponent(); labelProductName.Text = AssemblyProduct; @@ -31,6 +42,85 @@ namespace NAPS2.WinForms } } + protected override void OnLoad(object sender, EventArgs eventArgs) + { + new LayoutManager(this) + .Bind(logoPictureBox) + .TopTo(() => Height / 2) + .Activate(); + + cbCheckForUpdates.Checked = userConfigManager.Config.CheckForUpdates; + UpdateControls(); + DoUpdateCheck(); + } + + private void DoUpdateCheck() + { + if (cbCheckForUpdates.Checked) + { + updateChecker.CheckForUpdates().ContinueWith(task => + { + update = task.Result; + hasCheckedForUpdates = true; + SafeInvoke(UpdateControls); + }); + } + } + + private void UpdateControls() + { + const int margin = 5; + if (cbCheckForUpdates.Checked) + { + if (lblUpdateStatus.Visible == false && linkInstall.Visible == false) + { + ConditionalControls.Show(lblUpdateStatus, margin); + } + if (hasCheckedForUpdates) + { + if (update == null) + { + lblUpdateStatus.Text = MiscResources.NoUpdates; + lblUpdateStatus.Visible = true; + linkInstall.Visible = false; + } + else + { + linkInstall.Text = string.Format(MiscResources.Install, update.Name); + lblUpdateStatus.Visible = false; + linkInstall.Visible = true; + } + } + else + { + lblUpdateStatus.Text = MiscResources.CheckingForUpdates; + lblUpdateStatus.Visible = true; + linkInstall.Visible = false; + } + } + else + { + ConditionalControls.Hide(lblUpdateStatus, margin); + ConditionalControls.Hide(linkInstall, margin); + } + } + + private void cbCheckForUpdates_CheckedChanged(object sender, EventArgs e) + { + userConfigManager.Config.CheckForUpdates = cbCheckForUpdates.Checked; + userConfigManager.Save(); + UpdateControls(); + DoUpdateCheck(); + } + + private void linkInstall_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + if (update != null) + { + updateChecker.StartUpdate(update); + } + } + #region Assembly Attribute Accessors private static string GetAssemblyAttributeValue(Func selector) diff --git a/NAPS2.Core/WinForms/FAbout.resx b/NAPS2.Core/WinForms/FAbout.resx index 2d66d093a..f6cd66094 100644 --- a/NAPS2.Core/WinForms/FAbout.resx +++ b/NAPS2.Core/WinForms/FAbout.resx @@ -148,7 +148,7 @@ $this - 6 + 9 True @@ -178,13 +178,13 @@ $this - 7 + 10 True - 139, 69 + 139, 116 6, 12, 3, 0 @@ -208,7 +208,7 @@ $this - 8 + 11 Bottom, Right @@ -220,7 +220,7 @@ NoControl - 336, 100 + 336, 145 72, 24 @@ -241,7 +241,7 @@ $this - 3 + 6 @@ -414,7 +414,7 @@ NoControl - 10, 8 + 10, 30 120, 120 @@ -435,7 +435,7 @@ $this - 5 + 8 True @@ -462,13 +462,13 @@ $this - 4 + 7 True - 139, 96 + 139, 143 59, 13 @@ -489,13 +489,13 @@ $this - 2 + 5 True - 140, 109 + 140, 156 171, 13 @@ -516,7 +516,7 @@ $this - 1 + 4 NoControl @@ -543,6 +543,84 @@ $this + 3 + + + True + + + 141, 70 + + + 113, 17 + + + 36 + + + Check for updates + + + cbCheckForUpdates + + + System.Windows.Forms.CheckBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 2 + + + True + + + 139, 90 + + + 0, 13 + + + 37 + + + lblUpdateStatus + + + System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + + 1 + + + True + + + 139, 90 + + + 0, 13 + + + 38 + + + False + + + linkInstall + + + System.Windows.Forms.LinkLabel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + $this + + 0 @@ -552,7 +630,7 @@ 6, 13 - 420, 136 + 420, 181 @@ -591,6 +669,6 @@ FAbout - NAPS2.WinForms.FormBase, NAPS2.Core, Version=5.8.2.103, Culture=neutral, PublicKeyToken=null + NAPS2.WinForms.FormBase, NAPS2.Core, Version=5.8.2.24548, Culture=neutral, PublicKeyToken=null \ No newline at end of file diff --git a/NAPS2.Core/WinForms/FProgress.cs b/NAPS2.Core/WinForms/FProgress.cs index aae3aad39..dd6e1409d 100644 --- a/NAPS2.Core/WinForms/FProgress.cs +++ b/NAPS2.Core/WinForms/FProgress.cs @@ -100,7 +100,9 @@ namespace NAPS2.WinForms } else { - labelNumber.Text = string.Format(MiscResources.ProgressFormat, status.CurrentProgress, status.MaxProgress); + labelNumber.Text = status.ProgressType == OperationProgressType.MB + ? string.Format(MiscResources.SizeProgress, (status.CurrentProgress / 1000000.0).ToString("f1"), (status.MaxProgress / 1000000.0).ToString("f1")) + : string.Format(MiscResources.ProgressFormat, status.CurrentProgress, status.MaxProgress); progressBar.Style = ProgressBarStyle.Continuous; progressBar.Maximum = status.MaxProgress; progressBar.Value = status.CurrentProgress;