diff --git a/src/Squirrel/Sources/GitlabSource.cs b/src/Squirrel/Sources/GitlabSource.cs new file mode 100644 index 000000000..b3214d42d --- /dev/null +++ b/src/Squirrel/Sources/GitlabSource.cs @@ -0,0 +1,233 @@ +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. + /// Options: 'Package', 'Image', '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 GitLab 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 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 "GitlabReleaseEntry" + // containing a reference to the GitlabReleaseAsset 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 Gitlab 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(); + } + } +}