From cf00481f6d401fcc0b6728833160ede386310fa5 Mon Sep 17 00:00:00 2001 From: Gary Ewan Park Date: Mon, 11 Sep 2023 20:36:14 +0100 Subject: [PATCH] Ability to upload to Generic Packages Repository --- NGitLab.Mock/Clients/GitLabClient.cs | 2 + NGitLab.Mock/Clients/PackageClient.cs | 30 +++++++++ NGitLab.Tests/PackageTests.cs | 40 +++++++++++ NGitLab/GitLabClient.cs | 3 + NGitLab/IGitLabClient.cs | 2 + NGitLab/IPackageClient.cs | 34 ++++++++++ NGitLab/Impl/HttpRequestor.GitLabRequest.cs | 19 +++++- NGitLab/Impl/PackageClient.cs | 72 ++++++++++++++++++++ NGitLab/Models/FileContent.cs | 17 +++++ NGitLab/Models/Package.cs | 74 +++++++++++++++++++++ NGitLab/Models/PackageFile.cs | 10 +++ NGitLab/Models/PackageLink.cs | 13 ++++ NGitLab/Models/PackageOrderBy.cs | 10 +++ NGitLab/Models/PackagePublish.cs | 17 +++++ NGitLab/Models/PackageQuery.cs | 23 +++++++ NGitLab/Models/PackageSearchResult.cs | 35 ++++++++++ NGitLab/Models/PackageSort.cs | 8 +++ NGitLab/Models/PackageStatus.cs | 11 +++ NGitLab/Models/PackageType.cs | 17 +++++ 19 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 NGitLab.Mock/Clients/PackageClient.cs create mode 100644 NGitLab.Tests/PackageTests.cs create mode 100644 NGitLab/IPackageClient.cs create mode 100644 NGitLab/Impl/PackageClient.cs create mode 100644 NGitLab/Models/FileContent.cs create mode 100644 NGitLab/Models/Package.cs create mode 100644 NGitLab/Models/PackageFile.cs create mode 100644 NGitLab/Models/PackageLink.cs create mode 100644 NGitLab/Models/PackageOrderBy.cs create mode 100644 NGitLab/Models/PackagePublish.cs create mode 100644 NGitLab/Models/PackageQuery.cs create mode 100644 NGitLab/Models/PackageSearchResult.cs create mode 100644 NGitLab/Models/PackageSort.cs create mode 100644 NGitLab/Models/PackageStatus.cs create mode 100644 NGitLab/Models/PackageType.cs diff --git a/NGitLab.Mock/Clients/GitLabClient.cs b/NGitLab.Mock/Clients/GitLabClient.cs index b5363a23..9d5283d0 100644 --- a/NGitLab.Mock/Clients/GitLabClient.cs +++ b/NGitLab.Mock/Clients/GitLabClient.cs @@ -13,6 +13,8 @@ public GitLabClient(ClientContext context) public IGroupsClient Groups => new GroupClient(Context); + public IPackageClient Packages => new PackageClient(Context); + public IUserClient Users => new UserClient(Context); public IProjectClient Projects => new ProjectClient(Context); diff --git a/NGitLab.Mock/Clients/PackageClient.cs b/NGitLab.Mock/Clients/PackageClient.cs new file mode 100644 index 00000000..e34b96f7 --- /dev/null +++ b/NGitLab.Mock/Clients/PackageClient.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NGitLab.Models; + +namespace NGitLab.Mock.Clients +{ + internal sealed class PackageClient : ClientBase, IPackageClient + { + public PackageClient(ClientContext context) + : base(context) + { + } + + public Task PublishAsync(int projectId, PackagePublish packagePublish, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + + public IEnumerable Get(int projectId, PackageQuery packageQuery) + { + throw new System.NotImplementedException(); + } + + public Task GetByIdAsync(int projectId, int packageId, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/NGitLab.Tests/PackageTests.cs b/NGitLab.Tests/PackageTests.cs new file mode 100644 index 00000000..9c6a45d8 --- /dev/null +++ b/NGitLab.Tests/PackageTests.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NGitLab.Models; +using NGitLab.Tests.Docker; +using NUnit.Framework; + +namespace NGitLab.Tests +{ + public class PackageTests + { + [Test] + [NGitLabRetry] + public async Task Test_publish_package() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var packagesClient = context.Client.Packages; + + var packagePublish = new PackagePublish + { + FileName = "README.md", + PackageName = "Packages", + PackageVersion = "1.0.0", + Status = "default", + PackageStream = File.OpenRead("../../../../README.md"), + }; + + var newGenericPackage = await packagesClient.PublishAsync(project.Id, packagePublish); + + var packageQuery = new PackageQuery { PackageType = PackageType.generic }; + var genericPackages = packagesClient.Get(project.Id, packageQuery).ToList(); + var singleGenericPackage = await packagesClient.GetByIdAsync(project.Id, newGenericPackage.PackageId); + + Assert.AreEqual(1, genericPackages.Count); + Assert.AreEqual(newGenericPackage.PackageId, genericPackages[0].PackageId); + Assert.AreEqual(singleGenericPackage.PackageId, newGenericPackage.PackageId); + } + } +} diff --git a/NGitLab/GitLabClient.cs b/NGitLab/GitLabClient.cs index 9bb61b5e..ea85a5e4 100644 --- a/NGitLab/GitLabClient.cs +++ b/NGitLab/GitLabClient.cs @@ -8,6 +8,8 @@ public class GitLabClient : IGitLabClient { private readonly API _api; + public IPackageClient Packages { get; } + public IUserClient Users { get; } public IProjectClient Projects { get; } @@ -73,6 +75,7 @@ public GitLabClient(string hostUrl, string userName, string password, RequestOpt private GitLabClient(GitLabCredentials credentials, RequestOptions options) { _api = new API(credentials, options); + Packages = new PackageClient(_api); Users = new UserClient(_api); Projects = new ProjectClient(_api); MergeRequests = new MergeRequestClient(_api); diff --git a/NGitLab/IGitLabClient.cs b/NGitLab/IGitLabClient.cs index fd60a0da..b71b3fbc 100644 --- a/NGitLab/IGitLabClient.cs +++ b/NGitLab/IGitLabClient.cs @@ -2,6 +2,8 @@ { public interface IGitLabClient { + IPackageClient Packages { get; } + IUserClient Users { get; } IProjectClient Projects { get; } diff --git a/NGitLab/IPackageClient.cs b/NGitLab/IPackageClient.cs new file mode 100644 index 00000000..37175a37 --- /dev/null +++ b/NGitLab/IPackageClient.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NGitLab.Models; + +namespace NGitLab +{ + public interface IPackageClient + { + /// + /// Add a package file with the proposed information to the GitLab Generic Package Repository for the selected Project Id. + /// + /// The information about the package file to publish. + /// The package if it was created. Null if not. + Task PublishAsync(int projectId, PackagePublish packagePublish, CancellationToken cancellationToken = default); + + /// + /// Gets all project packages based on the provided query parameters. + /// + /// The project id to search for packages in. + /// The query parameters to be used for the search. + /// + IEnumerable Get(int projectId, PackageQuery packageQuery); + + /// + /// Gets a single project package using the provided project and package ids. + /// + /// The project id that the package resides in. + /// The package id that is being selected. + /// The cancellation token used to halt the request. + /// + Task GetByIdAsync(int projectId, int packageId, CancellationToken cancellationToken = default); + } +} diff --git a/NGitLab/Impl/HttpRequestor.GitLabRequest.cs b/NGitLab/Impl/HttpRequestor.GitLabRequest.cs index 54972426..d50a97c3 100644 --- a/NGitLab/Impl/HttpRequestor.GitLabRequest.cs +++ b/NGitLab/Impl/HttpRequestor.GitLabRequest.cs @@ -25,6 +25,8 @@ private sealed class GitLabRequest public string JsonData { get; } + public FileContent FileContent { get; } + public FormDataContent FormData { get; } private MethodType Method { get; } @@ -60,7 +62,11 @@ public GitLabRequest(Uri url, MethodType method, object data, string apiToken, R Headers.Add("User-Agent", options.UserAgent); } - if (data is FormDataContent formData) + if (data is FileContent fileContent) + { + FileContent = fileContent; + } + else if (data is FormDataContent formData) { FormData = formData; } @@ -165,7 +171,11 @@ private HttpWebRequest CreateRequest(RequestOptions options) if (HasOutput) { - if (FormData != null) + if (FileContent != null) + { + AddFileContent(request, options); + } + else if (FormData != null) { AddFileData(request, options); } @@ -182,6 +192,11 @@ private HttpWebRequest CreateRequest(RequestOptions options) return request; } + private void AddFileContent(HttpWebRequest request, RequestOptions options) + { + FileContent.Stream.CopyTo(options.GetRequestStream(request)); + } + private void AddJsonData(HttpWebRequest request, RequestOptions options) { request.ContentType = "application/json"; diff --git a/NGitLab/Impl/PackageClient.cs b/NGitLab/Impl/PackageClient.cs new file mode 100644 index 00000000..0675c895 --- /dev/null +++ b/NGitLab/Impl/PackageClient.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using NGitLab.Models; + +namespace NGitLab.Impl +{ + public class PackageClient : IPackageClient + { + private const string PublishPackageUrl = "/projects/{0}/packages/generic/{1}/{2}/{3}?status={4}&select=package_file"; + private const string GetPackagesUrl = "/projects/{0}/packages"; + private const string GetPackageUrl = "/projects/{0}/packages/{1}"; + + private readonly API _api; + + public PackageClient(API api) + { + _api = api; + } + + public Task PublishAsync(int projectId, PackagePublish packagePublish, CancellationToken cancellationToken = default) + { + var formData = new FileContent(packagePublish.PackageStream); + + return _api.Put().With(formData).ToAsync(string.Format(CultureInfo.InvariantCulture, + PublishPackageUrl, projectId, Uri.EscapeDataString(packagePublish.PackageName), + Uri.EscapeDataString(packagePublish.PackageVersion), Uri.EscapeDataString(packagePublish.FileName), + Uri.EscapeDataString(packagePublish.Status)), cancellationToken); + } + + public IEnumerable Get(int projectId, PackageQuery packageQuery) + { + var url = CreateGetUrl(projectId, packageQuery); + return _api.Get().GetAllAsync(url); + } + + public Task GetByIdAsync(int projectId, int packageId, CancellationToken cancellationToken = default) + { + return _api.Get().ToAsync(string.Format(CultureInfo.InvariantCulture, GetPackageUrl, projectId, packageId), cancellationToken); + } + + private static string CreateGetUrl(int projectId, PackageQuery query) + { + var url = string.Format(CultureInfo.InvariantCulture, GetPackagesUrl, projectId); + + url = Utils.AddParameter(url, "order_by", query.OrderBy); + url = Utils.AddParameter(url, "sort", query.Sort); + url = Utils.AddParameter(url, "status", query.Status); + url = Utils.AddParameter(url, "page", query.Page); + url = Utils.AddParameter(url, "per_page", query.PerPage); + + if (query.PackageType != PackageType.all) + { + url = Utils.AddParameter(url, "package_type", query.PackageType); + } + + if (!string.IsNullOrWhiteSpace(query.PackageName)) + { + url = Utils.AddParameter(url, "package_name", query.PackageName); + } + + if (query.IncludeVersionless) + { + url = Utils.AddParameter(url, "include_versionless", true); + } + + return url; + } + } +} diff --git a/NGitLab/Models/FileContent.cs b/NGitLab/Models/FileContent.cs new file mode 100644 index 00000000..d99c8feb --- /dev/null +++ b/NGitLab/Models/FileContent.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace NGitLab.Models +{ + public sealed class FileContent + { + public FileContent(Stream stream) + { + Stream = stream; + } + + /// + /// The stream to be uploaded. + /// + public Stream Stream { get; } + } +} diff --git a/NGitLab/Models/Package.cs b/NGitLab/Models/Package.cs new file mode 100644 index 00000000..8ad2ae64 --- /dev/null +++ b/NGitLab/Models/Package.cs @@ -0,0 +1,74 @@ +using System; +using System.Text.Json.Serialization; +using NGitLab.Impl.Json; + +namespace NGitLab.Models +{ + public class Package + { + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("package_id")] + public int PackageId { get; set; } + + [JsonPropertyName("created_at")] + [JsonConverter(typeof(DateOnlyConverter))] + public DateTime CreatedAt { get; set; } + + [JsonPropertyName("updated_at")] + [JsonConverter(typeof(DateOnlyConverter))] + public DateTime? UpdatedAt { get; set; } + + [JsonPropertyName("size")] + public int Size { get; set; } + + [JsonPropertyName("file_store")] + public int FileStore { get; set; } + + [JsonPropertyName("file_md5")] + public string FileMD5 { get; set; } + + [JsonPropertyName("file_sha1")] + public string FileSHA1 { get; set; } + + [JsonPropertyName("file_sha256")] + public string FileSHA256 { get; set; } + + [JsonPropertyName("file_name")] + public string FileName { get; set; } + + [JsonPropertyName("verification_retry_at")] + [JsonConverter(typeof(DateOnlyConverter))] + public DateTime? VerificationRetryAt { get; set; } + + [JsonPropertyName("verified_at")] + [JsonConverter(typeof(DateOnlyConverter))] + public DateTime? VerifiedAt { get; set; } + + [JsonPropertyName("verification_failure")] + public string VerificationFailure { get; set; } + + [JsonPropertyName("verification_retry_count")] + public string VerificationRetryCount { get; set; } + + [JsonPropertyName("verification_checksum")] + public string VerificationChecksum { get; set; } + + [JsonPropertyName("verification_state")] + public int VerificationState { get; set; } + + [JsonPropertyName("verification_started_at")] + [JsonConverter(typeof(DateOnlyConverter))] + public DateTime? VerificationStartedAt { get; set; } + + [JsonPropertyName("new_file_path")] + public string NewFilePath { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("file")] + public PackageFile File { get; set; } + } +} diff --git a/NGitLab/Models/PackageFile.cs b/NGitLab/Models/PackageFile.cs new file mode 100644 index 00000000..8093a488 --- /dev/null +++ b/NGitLab/Models/PackageFile.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace NGitLab.Models +{ + public class PackageFile + { + [JsonPropertyName("url")] + public string Url { get; set; } + } +} diff --git a/NGitLab/Models/PackageLink.cs b/NGitLab/Models/PackageLink.cs new file mode 100644 index 00000000..e1f9fb5b --- /dev/null +++ b/NGitLab/Models/PackageLink.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace NGitLab.Models +{ + public class PackageLinks + { + [JsonPropertyName("web_path")] + public string WebPath { get; set; } + + [JsonPropertyName("delete_api_path")] + public string DeleteApiPath { get; set; } + } +} diff --git a/NGitLab/Models/PackageOrderBy.cs b/NGitLab/Models/PackageOrderBy.cs new file mode 100644 index 00000000..6474d988 --- /dev/null +++ b/NGitLab/Models/PackageOrderBy.cs @@ -0,0 +1,10 @@ +namespace NGitLab.Models +{ + public enum PackageOrderBy + { + created_at, + name, + version, + type, + } +} diff --git a/NGitLab/Models/PackagePublish.cs b/NGitLab/Models/PackagePublish.cs new file mode 100644 index 00000000..eccc0a54 --- /dev/null +++ b/NGitLab/Models/PackagePublish.cs @@ -0,0 +1,17 @@ +using System.IO; + +namespace NGitLab.Models +{ + public class PackagePublish + { + public string PackageName { get; set; } + + public string PackageVersion { get; set; } + + public Stream PackageStream { get; set; } + + public string FileName { get; set; } + + public string Status { get; set; } + } +} diff --git a/NGitLab/Models/PackageQuery.cs b/NGitLab/Models/PackageQuery.cs new file mode 100644 index 00000000..aeae084b --- /dev/null +++ b/NGitLab/Models/PackageQuery.cs @@ -0,0 +1,23 @@ +namespace NGitLab.Models +{ + public class PackageQuery + { + public int PackageId { get; set; } + + public PackageOrderBy OrderBy { get; set; } + + public PackageSort Sort { get; set; } + + public PackageType PackageType { get; set; } + + public string PackageName { get; set; } + + public bool IncludeVersionless { get; set; } + + public PackageStatus Status { get; set; } + + public int? PerPage { get; set; } + + public int? Page { get; set; } + } +} diff --git a/NGitLab/Models/PackageSearchResult.cs b/NGitLab/Models/PackageSearchResult.cs new file mode 100644 index 00000000..f0117bd3 --- /dev/null +++ b/NGitLab/Models/PackageSearchResult.cs @@ -0,0 +1,35 @@ +using System; +using System.Text.Json.Serialization; +using NGitLab.Impl.Json; + +namespace NGitLab.Models +{ + public class PackageSearchResult + { + [JsonPropertyName("id")] + public int PackageId { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } + + [JsonPropertyName("package_type")] + public PackageType PackageType { get; set; } + + [JsonPropertyName("status")] + public PackageStatus Status { get; set; } + + [JsonPropertyName("_links")] + public PackageLinks Links { get; set; } + + [JsonPropertyName("created_at")] + [JsonConverter(typeof(DateOnlyConverter))] + public DateTime CreatedAt { get; set; } + + [JsonPropertyName("last_downloaded_at")] + [JsonConverter(typeof(DateOnlyConverter))] + public DateTime? LastDownloadedAt { get; set; } + } +} diff --git a/NGitLab/Models/PackageSort.cs b/NGitLab/Models/PackageSort.cs new file mode 100644 index 00000000..2f7c153b --- /dev/null +++ b/NGitLab/Models/PackageSort.cs @@ -0,0 +1,8 @@ +namespace NGitLab.Models +{ + public enum PackageSort + { + asc, + desc, + } +} diff --git a/NGitLab/Models/PackageStatus.cs b/NGitLab/Models/PackageStatus.cs new file mode 100644 index 00000000..60620ad0 --- /dev/null +++ b/NGitLab/Models/PackageStatus.cs @@ -0,0 +1,11 @@ +namespace NGitLab.Models +{ + public enum PackageStatus + { + @default, + hidden, + processing, + error, + pending_destruction, + } +} diff --git a/NGitLab/Models/PackageType.cs b/NGitLab/Models/PackageType.cs new file mode 100644 index 00000000..c576600a --- /dev/null +++ b/NGitLab/Models/PackageType.cs @@ -0,0 +1,17 @@ +namespace NGitLab.Models +{ + public enum PackageType + { + all, + conan, + maven, + npm, + pypi, + composer, + nuget, + helm, + terraform_module, + golang, + generic, + } +}