From 0df78430417242b7aa069a28b3ca5f7143d028c0 Mon Sep 17 00:00:00 2001 From: Nick Bull Date: Wed, 25 Oct 2023 09:21:14 +0100 Subject: [PATCH 1/3] Create GitlabSource.cs Created GitlabSource, to be able to obtain the latest releases from a Gitlab repository and update the application. --- src/Squirrel/Sources/GitlabSource.cs | 229 +++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/Squirrel/Sources/GitlabSource.cs diff --git a/src/Squirrel/Sources/GitlabSource.cs b/src/Squirrel/Sources/GitlabSource.cs new file mode 100644 index 000000000..33d94c4ae --- /dev/null +++ b/src/Squirrel/Sources/GitlabSource.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using Squirrel.Json; + +namespace Squirrel.Sources +{ + /// + /// Describes a Gitlab release, plus any assets that are attached. + /// + [DataContract] + public class GitlabRelease + { + /// + /// The name of the release. + /// + [DataMember(Name = "name")] + public string Name { get; set; } + + /// + /// True if this is intended for an upcoming release. + /// + [DataMember(Name = "upcoming_release")] + public bool UpcomingRelease { get; set; } + + /// + /// The date which this release was published publically. + /// + [DataMember(Name = "released_at")] + public DateTime ReleasedAt { get; set; } + + /// + /// A container for the assets (files) uploaded to this release. + /// + [DataMember(Name = "assets")] + public GitlabReleaseAsset Assets { get; set; } + } + + /// + /// Describes a container for the assets attached to a release. + /// + [DataContract] + public class GitlabReleaseAsset + { + /// + /// The amount of assets linked to the release. + /// + [DataMember(Name = "count")] + public int Count { get; set; } + + /// + /// A list of asset (file) links. + /// + [DataMember(Name = "links")] + public GitlabReleaseLink[] Links { get; set; } + } + + /// + /// Describes a container for the links of assets attached to a release. + /// + [DataContract] + public class GitlabReleaseLink + { + /// + /// Name of the asset (file) linked. + /// + [DataMember(Name = "name")] + public string Name { get; set; } + + /// + /// The url for the asset. This make use of the Gitlab API. + /// + [DataMember(Name = "url")] + public string Url { get; set; } + + /// + /// A direct url to the asset, via a traditional URl. + /// As a posed to using the API. + /// This links directly to the raw asset (file). + /// + [DataMember(Name = "direct_asset_url")] + public string DirectAssetUrl { get; set; } + + /// + /// The category type that the asset is listed under. + /// Examples: 'Packages', 'Runbook', 'Other' + /// + [DataMember(Name = "link_type")] + public string Type { get; set; } + + } + + /// + /// Retrieves available releases from a GitLab repository. This class only + /// downloads assets from the very latest GitLab release. + /// + public class GitlabSource : IUpdateSource + { + /// + /// The URL of the GitLab repository to download releases from + /// (e.g. https://gitlab.com/api/v4/projects/ProjectId) + /// + public virtual Uri RepoUri { get; } + + /// + /// If true, the latest upcoming release will be downloaded. If false, the latest + /// stable release will be downloaded. + /// + public virtual bool UpcomingRelease { get; } + + /// + /// The file downloader used to perform HTTP requests. + /// + public virtual IFileDownloader Downloader { get; } + + /// + /// The GitLab release which this class should download assets from when + /// executing . This property can be set + /// explicitly, otherwise it will also be set automatically when executing + /// . + /// + public virtual GitlabRelease Release { get; set; } + + /// + /// The GitLab access token to use with the request to download releases. + /// + protected virtual string AccessToken { get; } + + /// + /// The Bearer token used in the request. + /// + protected virtual string Authorization => string.IsNullOrWhiteSpace(AccessToken) ? null : "Bearer " + AccessToken; + + /// + /// + /// The URL of the GitLab repository to download releases from + /// (e.g. https://gitlab.com/api/v4/projects/ProjectId) + /// + /// + /// The GitLab access token to use with the request to download releases. + /// + /// + /// If true, the latest upcoming release will be downloaded. If false, the latest + /// stable release will be downloaded. + /// + /// + /// The file downloader used to perform HTTP requests. + /// + public GitlabSource(string repoUrl, string accessToken, bool upcomingRelease, IFileDownloader downloader = null) + { + RepoUri = new Uri(repoUrl); + AccessToken = accessToken; + UpcomingRelease = upcomingRelease; + Downloader = downloader ?? Utility.CreateDefaultDownloader(); + } + + /// + public Task DownloadReleaseEntry(ReleaseEntry releaseEntry, string localFile, Action progress) + { + if (Release == null) { + throw new InvalidOperationException("No GiitLab Release specified. Call GetReleaseFeed or set " + + "GitLabSource.Release before calling this function."); + } + + var assetUrl = GetAssetUrlFromName(Release, releaseEntry.Filename); + return Downloader.DownloadFile(assetUrl, localFile, progress, Authorization, "application/octet-stream"); + } + + /// + public async Task GetReleaseFeed(Guid? stagingId = null, ReleaseEntry latestLocalRelease = null) + { + var releases = await GetReleases(UpcomingRelease).ConfigureAwait(false); + if (releases == null || releases.Count() == 0) + throw new Exception($"No GitHub releases found at '{RepoUri}'."); + + // CS: we 'cache' the release here, so subsequent calls to DownloadReleaseEntry + // will download assets from the same release in which we returned ReleaseEntry's + // from. A better architecture would be to return an array of "GithubReleaseEntry" + // containing a reference to the GithubReleaseAsset instead. + Release = releases.First(); + + var assetUrl = GetAssetUrlFromName(Release, "RELEASES"); + var releaseBytes = await Downloader.DownloadBytes(assetUrl, Authorization, "application/octet-stream").ConfigureAwait(false); + var txt = Utility.RemoveByteOrderMarkerIfPresent(releaseBytes); + return ReleaseEntry.ParseReleaseFileAndApplyStaging(txt, stagingId).ToArray(); + } + + /// + /// Given a and an asset filename (eg. 'RELEASES') this + /// function will return either or + /// , depending whether an access token is available + /// or not. Throws if the specified release has no matching assets. + /// + protected virtual string GetAssetUrlFromName(GitlabRelease release, string assetName) + { + if (release.Assets == null || release.Assets.Count == 0) { + throw new ArgumentException($"No assets found in Github Release '{release.Name}'."); + } + + GitlabReleaseLink packageFile = + release.Assets.Links.FirstOrDefault(a => a.Name.Equals(assetName, StringComparison.InvariantCultureIgnoreCase)); + if (packageFile == null) { + throw new ArgumentException($"Could not find asset called '{assetName}' in GitLab Release '{release.Name}'."); + } + + if (String.IsNullOrWhiteSpace(AccessToken)) { + return packageFile.DirectAssetUrl; + } else { + return packageFile.Url; + } + } + + /// + /// Retrieves a list of from the current repository. + /// + public virtual async Task GetReleases(bool includePrereleases, int perPage = 30, int page = 1) + { + // https://docs.gitlab.com/ee/api/releases/ + var releasesPath = $"{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}"; + var baseUri = new Uri("https://gitlab.com"); + var getReleasesUri = new Uri(baseUri, releasesPath); + var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization).ConfigureAwait(false); + var releases = SimpleJson.DeserializeObject>(response); + return releases.OrderByDescending(d => d.ReleasedAt).Where(x => includePrereleases || !x.UpcomingRelease).ToArray(); + } + } +} From b0b7d91e8f345b53d940d0e88d73cc2e74c74ae8 Mon Sep 17 00:00:00 2001 From: Nick Bull Date: Wed, 25 Oct 2023 09:56:06 +0100 Subject: [PATCH 2/3] Fix formatting --- src/Squirrel/Sources/GitlabSource.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Squirrel/Sources/GitlabSource.cs b/src/Squirrel/Sources/GitlabSource.cs index 33d94c4ae..f24cc3c10 100644 --- a/src/Squirrel/Sources/GitlabSource.cs +++ b/src/Squirrel/Sources/GitlabSource.cs @@ -85,11 +85,10 @@ public class GitlabReleaseLink /// /// The category type that the asset is listed under. - /// Examples: 'Packages', 'Runbook', 'Other' + /// Options: 'Package', 'Image', 'Runbook', 'Other' /// [DataMember(Name = "link_type")] public string Type { get; set; } - } /// @@ -195,19 +194,24 @@ public async Task GetReleaseFeed(Guid? stagingId = null, Release /// protected virtual string GetAssetUrlFromName(GitlabRelease release, string assetName) { - if (release.Assets == null || release.Assets.Count == 0) { + if (release.Assets == null || release.Assets.Count == 0) + { throw new ArgumentException($"No assets found in Github Release '{release.Name}'."); } GitlabReleaseLink packageFile = release.Assets.Links.FirstOrDefault(a => a.Name.Equals(assetName, StringComparison.InvariantCultureIgnoreCase)); - if (packageFile == null) { + if (packageFile == null) + { throw new ArgumentException($"Could not find asset called '{assetName}' in GitLab Release '{release.Name}'."); } - if (String.IsNullOrWhiteSpace(AccessToken)) { + if (String.IsNullOrWhiteSpace(AccessToken)) + { return packageFile.DirectAssetUrl; - } else { + } + else + { return packageFile.Url; } } From 30a19ea361389f120610560b4ab25b5cfdf154da Mon Sep 17 00:00:00 2001 From: Nick Bull Date: Wed, 25 Oct 2023 10:21:24 +0100 Subject: [PATCH 3/3] fix comments --- src/Squirrel/Sources/GitlabSource.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Squirrel/Sources/GitlabSource.cs b/src/Squirrel/Sources/GitlabSource.cs index f24cc3c10..b3214d42d 100644 --- a/src/Squirrel/Sources/GitlabSource.cs +++ b/src/Squirrel/Sources/GitlabSource.cs @@ -159,7 +159,7 @@ public GitlabSource(string repoUrl, string accessToken, bool upcomingRelease, IF public Task DownloadReleaseEntry(ReleaseEntry releaseEntry, string localFile, Action progress) { if (Release == null) { - throw new InvalidOperationException("No GiitLab Release specified. Call GetReleaseFeed or set " + + throw new InvalidOperationException("No GitLab Release specified. Call GetReleaseFeed or set " + "GitLabSource.Release before calling this function."); } @@ -172,12 +172,12 @@ public async Task GetReleaseFeed(Guid? stagingId = null, Release { var releases = await GetReleases(UpcomingRelease).ConfigureAwait(false); if (releases == null || releases.Count() == 0) - throw new Exception($"No GitHub releases found at '{RepoUri}'."); + throw new Exception($"No Gitlab releases found at '{RepoUri}'."); // CS: we 'cache' the release here, so subsequent calls to DownloadReleaseEntry // will download assets from the same release in which we returned ReleaseEntry's - // from. A better architecture would be to return an array of "GithubReleaseEntry" - // containing a reference to the GithubReleaseAsset instead. + // from. A better architecture would be to return an array of "GitlabReleaseEntry" + // containing a reference to the GitlabReleaseAsset instead. Release = releases.First(); var assetUrl = GetAssetUrlFromName(Release, "RELEASES"); @@ -196,7 +196,7 @@ protected virtual string GetAssetUrlFromName(GitlabRelease release, string asset { if (release.Assets == null || release.Assets.Count == 0) { - throw new ArgumentException($"No assets found in Github Release '{release.Name}'."); + throw new ArgumentException($"No assets found in Gitlab Release '{release.Name}'."); } GitlabReleaseLink packageFile =