Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
Add GithubUpdateManager class to replace the static factory
Browse files Browse the repository at this point in the history
  • Loading branch information
caesay committed Jan 2, 2022
1 parent 5f1cb66 commit 014d583
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 13 deletions.
196 changes: 196 additions & 0 deletions src/Squirrel/GithubUpdateManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using Squirrel.Json;

namespace Squirrel
{
/// <summary>
/// An implementation of UpdateManager which supports checking updates and
/// downloading releases directly from GitHub releases
/// </summary>
#if NET5_0_OR_GREATER
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
#endif
public class GithubUpdateManager : UpdateManager
{
private readonly string repoUrl;
private readonly string accessToken;
private readonly bool prerelease;

/// <inheritdoc cref="UpdateManager(string, string)"/>
/// <param name="repoUrl">
/// The URL of the GitHub repository to download releases from
/// (e.g. https://github.com/myuser/myrepo)
/// </param>
/// <param name="applicationName">
/// The name of your application should correspond with the
/// appdata directory name, and the name used with Squirrel releasify/pack.
/// </param>
/// <param name="prerelease">
/// If true, the latest pre-release will be downloaded. If false, the latest
/// stable release will be downloaded.
/// </param>
/// <param name="accessToken">
/// The GitHub access token to use with the request to download releases.
/// If left empty, the GitHub rate limit for unauthenticated requests allows
/// for up to 60 requests per hour, limited by IP address.
/// </param>
public GithubUpdateManager(string repoUrl, string applicationName, bool prerelease = false, string accessToken = null)
: this(repoUrl, applicationName, null, null, prerelease, accessToken)
{
}

/// <inheritdoc cref="UpdateManager(string, string)"/>
/// <param name="repoUrl">
/// The URL of the GitHub repository to download releases from
/// (e.g. https://github.com/myuser/myrepo)
/// </param>
/// <param name="applicationName">
/// The name of your application should correspond with the
/// appdata directory name, and the name used with Squirrel releasify/pack.
/// </param>
/// <param name="urlDownloader">
/// A custom file downloader, for using non-standard package sources or adding
/// proxy configurations.
/// </param>
/// <param name="prerelease">
/// If true, the latest pre-release will be downloaded. If false, the latest
/// stable release will be downloaded.
/// </param>
/// <param name="accessToken">
/// The GitHub access token to use with the request to download releases.
/// If left empty, the GitHub rate limit for unauthenticated requests allows
/// for up to 60 requests per hour, limited by IP address.
/// </param>
public GithubUpdateManager(string repoUrl, string applicationName, bool prerelease = false, string accessToken = null, IFileDownloader urlDownloader = null)
: this(repoUrl, applicationName, null, urlDownloader, prerelease, accessToken)
{
}

/// <inheritdoc cref="UpdateManager(string, string)"/>
/// <param name="repoUrl">
/// The URL of the GitHub repository to download releases from
/// (e.g. https://github.com/myuser/myrepo)
/// </param>
/// <param name="applicationName">
/// The name of your application should correspond with the
/// appdata directory name, and the name used with Squirrel releasify/pack.
/// </param>
/// <param name="urlDownloader">
/// A custom file downloader, for using non-standard package sources or adding
/// proxy configurations.
/// </param>
/// <param name="localAppDataDirectoryOverride">
/// Provide a custom location for the system LocalAppData, it will be used
/// instead of <see cref="Environment.SpecialFolder.LocalApplicationData"/>.
/// </param>
/// <param name="prerelease">
/// If true, the latest pre-release will be downloaded. If false, the latest
/// stable release will be downloaded.
/// </param>
/// <param name="accessToken">
/// The GitHub access token to use with the request to download releases.
/// If left empty, the GitHub rate limit for unauthenticated requests allows
/// for up to 60 requests per hour, limited by IP address.
/// </param>
public GithubUpdateManager(string repoUrl,
string applicationName,
string localAppDataDirectoryOverride,
IFileDownloader urlDownloader,
bool prerelease,
string accessToken)
: base(null, applicationName, localAppDataDirectoryOverride, urlDownloader)
{
this.repoUrl = repoUrl;
this.accessToken = accessToken;
this.prerelease = prerelease;
}

/// <inheritdoc />
public override async Task<UpdateInfo> CheckForUpdate(bool ignoreDeltaUpdates = false, Action<int> progress = null, UpdaterIntention intention = UpdaterIntention.Update)
{
await EnsureReleaseUrl().ConfigureAwait(false);
return await base.CheckForUpdate(ignoreDeltaUpdates, progress, intention).ConfigureAwait(false);
}

/// <inheritdoc />
public override async Task DownloadReleases(IEnumerable<ReleaseEntry> releasesToDownload, Action<int> progress = null)
{
await EnsureReleaseUrl().ConfigureAwait(false);
await base.DownloadReleases(releasesToDownload, progress).ConfigureAwait(false);
}

private async Task EnsureReleaseUrl()
{
if (this.updateUrlOrPath == null) {
this.updateUrlOrPath = await GetLatestGithubRelease().ConfigureAwait(false);
}
}

private async Task<string> GetLatestGithubRelease()
{
var repoUri = new Uri(repoUrl);
var userAgent = new ProductInfoHeaderValue("Squirrel", AssemblyRuntimeInfo.ExecutingAssemblyName.Version.ToString());

if (repoUri.Segments.Length != 3) {
throw new Exception("Repo URL must be to the root URL of the repo e.g. https://github.com/myuser/myrepo");
}

var releasesApiBuilder = new StringBuilder("repos")
.Append(repoUri.AbsolutePath)
.Append("/releases");

if (!string.IsNullOrWhiteSpace(accessToken))
releasesApiBuilder.Append("?access_token=").Append(accessToken);

Uri baseAddress;

if (repoUri.Host.EndsWith("github.com", StringComparison.OrdinalIgnoreCase)) {
baseAddress = new Uri("https://api.github.com/");
} else {
// if it's not github.com, it's probably an Enterprise server
// now the problem with Enterprise is that the API doesn't come prefixed
// it comes suffixed
// so the API path of http://internal.github.server.local API location is
// http://interal.github.server.local/api/v3.
baseAddress = new Uri(string.Format("{0}{1}{2}/api/v3/", repoUri.Scheme, Uri.SchemeDelimiter, repoUri.Host));
}

// above ^^ notice the end slashes for the baseAddress, explained here: http://stackoverflow.com/a/23438417/162694

using var client = new HttpClient() { BaseAddress = baseAddress };
client.DefaultRequestHeaders.UserAgent.Add(userAgent);
var response = await client.GetAsync(releasesApiBuilder.ToString()).ConfigureAwait(false);
response.EnsureSuccessStatusCode();

var releases = SimpleJson.DeserializeObject<List<Release>>(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
var latestRelease = releases
.Where(x => prerelease || !x.Prerelease)
.OrderByDescending(x => x.PublishedAt)
.First();

var latestReleaseUrl = latestRelease.HtmlUrl.Replace("/tag/", "/download/");
return latestReleaseUrl;
}

[DataContract]
private class Release
{
[DataMember(Name = "prerelease")]
public bool Prerelease { get; set; }

[DataMember(Name = "published_at")]
public DateTime PublishedAt { get; set; }

[DataMember(Name = "html_url")]
public string HtmlUrl { get; set; }
}
}
}
2 changes: 1 addition & 1 deletion src/Squirrel/UpdateManager.ApplyReleases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

namespace Squirrel
{
public sealed partial class UpdateManager
public partial class UpdateManager
{
internal class ApplyReleasesImpl : IEnableLogger
{
Expand Down
2 changes: 1 addition & 1 deletion src/Squirrel/UpdateManager.CheckForUpdates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Squirrel
{
public sealed partial class UpdateManager
public partial class UpdateManager
{
internal class CheckForUpdateImpl : IEnableLogger
{
Expand Down
2 changes: 1 addition & 1 deletion src/Squirrel/UpdateManager.DownloadReleases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Squirrel
{
public sealed partial class UpdateManager
public partial class UpdateManager
{
internal class DownloadReleasesImpl : IEnableLogger
{
Expand Down
8 changes: 7 additions & 1 deletion src/Squirrel/UpdateManager.Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Squirrel
{
public sealed partial class UpdateManager
public partial class UpdateManager
{
[DataContract]
private class Release
Expand All @@ -26,6 +26,12 @@ private class Release
public string HtmlUrl { get; set; }
}

/// <summary>
/// This function is obsolete and will be removed in a future version,
/// see the <see cref="GithubUpdateManager" /> class for a replacement.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
[Obsolete("Use 'new GithubUpdateManager(...)' instead")]
public static async Task<UpdateManager> GitHubUpdateManager(
string repoUrl,
string applicationName = null,
Expand Down
2 changes: 1 addition & 1 deletion src/Squirrel/UpdateManager.InstallHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace Squirrel
{
public sealed partial class UpdateManager
public partial class UpdateManager
{
internal class InstallHelperImpl : IEnableLogger
{
Expand Down
22 changes: 14 additions & 8 deletions src/Squirrel/UpdateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace Squirrel
#if NET5_0_OR_GREATER
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
#endif
public sealed partial class UpdateManager : IUpdateManager, IEnableLogger
public partial class UpdateManager : IUpdateManager, IEnableLogger
{
/// <summary>The name of the application.</summary>
public string ApplicationName => applicationName;
Expand All @@ -38,7 +38,9 @@ public sealed partial class UpdateManager : IUpdateManager, IEnableLogger
private readonly string rootAppDirectory;
private readonly string applicationName;
private readonly IFileDownloader urlDownloader;
private readonly string updateUrlOrPath;

/// <summary>The url to use when checking for or downloading updates</summary>
protected string updateUrlOrPath;

private readonly object lockobj = new object();
private IDisposable updateLock;
Expand All @@ -53,7 +55,8 @@ public sealed partial class UpdateManager : IUpdateManager, IEnableLogger
/// </param>
/// <param name="applicationName">
/// The name of your application should correspond with the
/// appdata directory name, and the name used with Squirrel releasify/pack.</param>
/// appdata directory name, and the name used with Squirrel releasify/pack.
/// </param>
public UpdateManager(string urlOrPath, string applicationName)
: this(urlOrPath, applicationName, null, null)
{
Expand All @@ -65,7 +68,8 @@ public UpdateManager(string urlOrPath, string applicationName)
/// </param>
/// <param name="applicationName">
/// The name of your application should correspond with the
/// appdata directory name, and the name used with Squirrel releasify/pack.</param>
/// appdata directory name, and the name used with Squirrel releasify/pack.
/// </param>
/// <param name="urlDownloader">
/// A custom file downloader, for using non-standard package sources or adding proxy configurations.
/// </param>
Expand All @@ -80,15 +84,17 @@ public UpdateManager(string urlOrPath, string applicationName, IFileDownloader u
/// </param>
/// <param name="applicationName">
/// The name of your application should correspond with the
/// appdata directory name, and the name used with Squirrel releasify/pack.</param>
/// appdata directory name, and the name used with Squirrel releasify/pack.
/// </param>
/// <param name="urlDownloader">
/// A custom file downloader, for using non-standard package sources or adding proxy configurations.
/// </param>
/// <param name="localAppDataDirectoryOverride">
/// Provide a custom location for the system LocalAppData, it will be used
/// instead of <see cref="Environment.SpecialFolder.LocalApplicationData"/>.
/// </param>
public UpdateManager(string urlOrPath,
public UpdateManager(
string urlOrPath,
string applicationName,
string localAppDataDirectoryOverride,
IFileDownloader urlDownloader)
Expand All @@ -109,7 +115,7 @@ public UpdateManager(string urlOrPath,
}

/// <inheritdoc/>
public async Task<UpdateInfo> CheckForUpdate(bool ignoreDeltaUpdates = false, Action<int> progress = null, UpdaterIntention intention = UpdaterIntention.Update)
public virtual async Task<UpdateInfo> CheckForUpdate(bool ignoreDeltaUpdates = false, Action<int> progress = null, UpdaterIntention intention = UpdaterIntention.Update)
{
var checkForUpdate = new CheckForUpdateImpl(rootAppDirectory);

Expand All @@ -118,7 +124,7 @@ public async Task<UpdateInfo> CheckForUpdate(bool ignoreDeltaUpdates = false, Ac
}

/// <inheritdoc/>
public async Task DownloadReleases(IEnumerable<ReleaseEntry> releasesToDownload, Action<int> progress = null)
public virtual async Task DownloadReleases(IEnumerable<ReleaseEntry> releasesToDownload, Action<int> progress = null)
{
var downloadReleases = new DownloadReleasesImpl(rootAppDirectory);
await acquireUpdateLock().ConfigureAwait(false);
Expand Down

0 comments on commit 014d583

Please sign in to comment.