From 701ea4915eab8f49608de231130ccdbd8a89ad91 Mon Sep 17 00:00:00 2001 From: Nils Hjelte Date: Mon, 20 Sep 2021 11:01:46 +0200 Subject: [PATCH 1/2] Map the GitLab package API --- src/GitLabApiClient/GitLabClient.cs | 7 ++ src/GitLabApiClient/IGitLabClient.cs | 5 + src/GitLabApiClient/IPackagesClient.cs | 61 ++++++++++ .../Internal/Http/GitLabHttpFacade.cs | 3 + .../Internal/Http/GitlabApiRequestor.cs | 7 ++ .../Internal/Queries/PackagesQueryBuilder.cs | 69 +++++++++++ .../Models/Issues/Requests/PackagesOrder.cs | 10 ++ .../Models/Issues/Responses/PackageStatus.cs | 9 ++ .../Models/Issues/Responses/PackageType.cs | 15 +++ src/GitLabApiClient/PackagesClient.cs | 109 ++++++++++++++++++ 10 files changed, 295 insertions(+) create mode 100644 src/GitLabApiClient/IPackagesClient.cs create mode 100644 src/GitLabApiClient/Internal/Queries/PackagesQueryBuilder.cs create mode 100644 src/GitLabApiClient/Models/Issues/Requests/PackagesOrder.cs create mode 100644 src/GitLabApiClient/Models/Issues/Responses/PackageStatus.cs create mode 100644 src/GitLabApiClient/Models/Issues/Responses/PackageType.cs create mode 100644 src/GitLabApiClient/PackagesClient.cs diff --git a/src/GitLabApiClient/GitLabClient.cs b/src/GitLabApiClient/GitLabClient.cs index 63705304..934e31a1 100644 --- a/src/GitLabApiClient/GitLabClient.cs +++ b/src/GitLabApiClient/GitLabClient.cs @@ -43,6 +43,7 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage var projectIssueNotesQueryBuilder = new ProjectIssueNotesQueryBuilder(); var projectMergeRequestsNotesQueryBuilder = new ProjectMergeRequestsNotesQueryBuilder(); var issuesQueryBuilder = new IssuesQueryBuilder(); + var packagesQueryBuilder = new PackagesQueryBuilder(); var mergeRequestsQueryBuilder = new MergeRequestsQueryBuilder(); var projectMilestonesQueryBuilder = new MilestonesQueryBuilder(); var projectMergeRequestsQueryBuilder = new ProjectMergeRequestsQueryBuilder(); @@ -75,6 +76,7 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage Pipelines = new PipelineClient(_httpFacade, pipelineQueryBuilder, jobQueryBuilder); Trees = new TreesClient(_httpFacade, treeQueryBuilder); Files = new FilesClient(_httpFacade); + Packages = new PackagesClient(_httpFacade, packagesQueryBuilder); Runners = new RunnersClient(_httpFacade); ToDoList = new ToDoListClient(_httpFacade, toDoListBuilder); Connection = new ConnectionClient(_httpFacade); @@ -145,6 +147,11 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage /// public IFilesClient Files { get; } + /// + /// Access GitLab's packages API. + /// + public IPackagesClient Packages { get; } + /// /// Access GitLab's Markdown API. /// diff --git a/src/GitLabApiClient/IGitLabClient.cs b/src/GitLabApiClient/IGitLabClient.cs index 7ffbdf72..7c7d896e 100644 --- a/src/GitLabApiClient/IGitLabClient.cs +++ b/src/GitLabApiClient/IGitLabClient.cs @@ -70,6 +70,11 @@ public interface IGitLabClient /// IFilesClient Files { get; } + /// + /// Access GitLab's packages API. + /// + IPackagesClient Packages { get; } + /// /// Access GitLab's Markdown API. /// diff --git a/src/GitLabApiClient/IPackagesClient.cs b/src/GitLabApiClient/IPackagesClient.cs new file mode 100644 index 00000000..394772c3 --- /dev/null +++ b/src/GitLabApiClient/IPackagesClient.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Models.Packages.Responses; +using GitLabApiClient.Models.Packages.Requests; +using GitLabApiClient.Models.Uploads.Requests; + +namespace GitLabApiClient +{ + public interface IPackagesClient + { + + /// + /// Pyblish a generic package file. When you publish a package file, if the package does not exist, it is created. + /// + /// The ID, path or of the project. + /// The ID, path or of the package. + /// The upload request containing the filename and stream to be uploaded + Task UploadFileAsync(ProjectId projectId, string packageName, string packageVersion, string fileName, CreateUploadRequest uploadRequest, bool hidden = false); + + + /// + /// Download a generic package file. + /// + /// The ID, path or of the project. + /// The filename that should contain the contents of the download after the download completes + /// Status of the export + Task DownloadFileAsync(ProjectId projectId, string packageName, string packageVersion, string fileName, string outputPath); + + /// + /// Retrieves project package. + /// + /// The ID, path or of the project. + /// The ID, path or of the package. + Task GetAsync(ProjectId projectId, int packageId); + + + /// + /// List package files + /// + /// The ID, path or of the project. + /// The ID, path or of the package. + Task> GetPackageFilesAsync(ProjectId projectId, int packageId); + + /// The ID, path or of the project. + /// The ID, path or of the group. + /// Packages retrieval options. + /// Packages satisfying options. + Task> GetAllAsync(ProjectId projectId = null, GroupId groupId = null, Action options = null); + + + /// + /// Delete a package. + /// + /// The ID, path or of the project. + /// The ID, path or of the package. + Task DeletePackageAsync(ProjectId projectId, int packageId); + + } +} diff --git a/src/GitLabApiClient/Internal/Http/GitLabHttpFacade.cs b/src/GitLabApiClient/Internal/Http/GitLabHttpFacade.cs index 0ca82fd2..11bb9414 100644 --- a/src/GitLabApiClient/Internal/Http/GitLabHttpFacade.cs +++ b/src/GitLabApiClient/Internal/Http/GitLabHttpFacade.cs @@ -89,6 +89,9 @@ public Task Put(string uri, object data) => public Task Put(string uri, object data) => _requestor.Put(uri, data); + public Task PutFileBody(string uri, CreateUploadRequest uploadRequest) => + _requestor.PutFileBody(uri, uploadRequest); + public Task Delete(string uri) => _requestor.Delete(uri); public Task Delete(string uri, object data) => diff --git a/src/GitLabApiClient/Internal/Http/GitlabApiRequestor.cs b/src/GitLabApiClient/Internal/Http/GitlabApiRequestor.cs index c4d6947f..92b77aff 100644 --- a/src/GitLabApiClient/Internal/Http/GitlabApiRequestor.cs +++ b/src/GitLabApiClient/Internal/Http/GitlabApiRequestor.cs @@ -95,6 +95,13 @@ public async Task Put(string url, object data) await EnsureSuccessStatusCode(responseMessage); } + public async Task PutFileBody(string url, CreateUploadRequest uploadRequest) + { + var content = new StreamContent(uploadRequest.Stream); + var responseMessage = await _client.PutAsync(url, content); + await EnsureSuccessStatusCode(responseMessage); + } + public async Task Delete(string url) { var responseMessage = await _client.DeleteAsync(url); diff --git a/src/GitLabApiClient/Internal/Queries/PackagesQueryBuilder.cs b/src/GitLabApiClient/Internal/Queries/PackagesQueryBuilder.cs new file mode 100644 index 00000000..d1b19122 --- /dev/null +++ b/src/GitLabApiClient/Internal/Queries/PackagesQueryBuilder.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using GitLabApiClient.Internal.Utilities; +using GitLabApiClient.Models; +using GitLabApiClient.Models.Packages.Requests; +using GitLabApiClient.Models.Packages.Responses; + +namespace GitLabApiClient.Internal.Queries +{ + internal class PackagesQueryBuilder : QueryBuilder + { + protected override void BuildCore(Query query, PackagesQueryOptions options) + { + string stateQueryValue = GetStatusQueryValue(options.Status); + if (!stateQueryValue.IsNullOrEmpty()) + query.Add("status", stateQueryValue); + + string packageTypeQueryValue = GetPackageTypeQueryValue(options.PackageType); + if (!packageTypeQueryValue.IsNullOrEmpty()) + query.Add("package_type", packageTypeQueryValue); + + if (!string.IsNullOrEmpty(options.PackageName)) + query.Add("package_name", options.PackageName); + + if (options.Order != PackagesOrder.CreatedAt) + query.Add("order_by", GetPackagesOrderQueryValue(options.Order)); + + if (options.SortOrder != SortOrder.Descending) + query.Add("sort", GetSortOrderQueryValue(options.SortOrder)); + + if (options.IncludeVersionless) + query.Add("include_versionless", true); + } + + private static string GetPackageTypeQueryValue(PackageType type) => type == PackageType.All ? "" : type.ToString().ToLower(); + + private static string GetStatusQueryValue(PackageStatus status) + { + switch (status) + { + case PackageStatus.Default: + return ""; + case PackageStatus.Hidden: + return "hidden"; + case PackageStatus.Processing: + return "processing"; + default: + throw new NotSupportedException($"Status {status} is not supported"); + } + } + + private static string GetPackagesOrderQueryValue(PackagesOrder order) + { + switch (order) + { + case PackagesOrder.CreatedAt: + return "created_at"; + case PackagesOrder.Name: + return "name"; + case PackagesOrder.Version: + return "version"; + case PackagesOrder.Type: + return "type"; + default: + throw new NotSupportedException($"Order {order} is not supported"); + } + } + } +} diff --git a/src/GitLabApiClient/Models/Issues/Requests/PackagesOrder.cs b/src/GitLabApiClient/Models/Issues/Requests/PackagesOrder.cs new file mode 100644 index 00000000..eaa1d945 --- /dev/null +++ b/src/GitLabApiClient/Models/Issues/Requests/PackagesOrder.cs @@ -0,0 +1,10 @@ +namespace GitLabApiClient.Models.Packages.Requests +{ + public enum PackagesOrder + { + CreatedAt, + Name, + Version, + Type + } +} diff --git a/src/GitLabApiClient/Models/Issues/Responses/PackageStatus.cs b/src/GitLabApiClient/Models/Issues/Responses/PackageStatus.cs new file mode 100644 index 00000000..b29b93d2 --- /dev/null +++ b/src/GitLabApiClient/Models/Issues/Responses/PackageStatus.cs @@ -0,0 +1,9 @@ +namespace GitLabApiClient.Models.Packages.Responses +{ + public enum PackageStatus + { + Default, + Hidden, + Processing + } +} diff --git a/src/GitLabApiClient/Models/Issues/Responses/PackageType.cs b/src/GitLabApiClient/Models/Issues/Responses/PackageType.cs new file mode 100644 index 00000000..ec581f59 --- /dev/null +++ b/src/GitLabApiClient/Models/Issues/Responses/PackageType.cs @@ -0,0 +1,15 @@ +namespace GitLabApiClient.Models.Packages.Responses +{ + public enum PackageType + { + All, + Conan, + Maven, + Npm, + PyPi, + Composer, + Nuget, + Helm, + Golang + } +} diff --git a/src/GitLabApiClient/PackagesClient.cs b/src/GitLabApiClient/PackagesClient.cs new file mode 100644 index 00000000..98d4c8a5 --- /dev/null +++ b/src/GitLabApiClient/PackagesClient.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using GitLabApiClient.Internal.Http; +using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Internal.Utilities; +using GitLabApiClient.Models.Packages.Responses; +using GitLabApiClient.Models.Packages.Requests; +using GitLabApiClient.Internal.Queries; +using GitLabApiClient.Models.Uploads.Requests; + +namespace GitLabApiClient +{ + public sealed class PackagesClient : IPackagesClient + { + private readonly GitLabHttpFacade _httpFacade; + private readonly PackagesQueryBuilder _queryBuilder; + + + internal PackagesClient( + GitLabHttpFacade httpFacade, + PackagesQueryBuilder queryBuilder + ) + { + _httpFacade = httpFacade; + _queryBuilder = queryBuilder; + } + + + /// + /// Pyblish a generic package file. When you publish a package file, if the package does not exist, it is created. + /// + /// The ID, path or of the project. + /// The ID, path or of the package. + /// The upload request containing the filename and stream to be uploaded + public async Task UploadFileAsync(ProjectId projectId, string packageName, string packageVersion, string fileName, CreateUploadRequest uploadRequest, bool hidden = false) + { + string status = hidden ? "hidden" : "default"; + string url = $"projects/{projectId}/packages/generic/{packageName}/{packageVersion}/{fileName}?status={status}"; + + await _httpFacade.PutFileBody(url, uploadRequest); + } + + /// + /// Download a generic package file. + /// + /// The ID, path or of the project. + /// The filename that should contain the contents of the download after the download completes + /// Status of the export + public async Task DownloadFileAsync(ProjectId projectId, string packageName, string packageVersion, string fileName, string outputPath) + { + string url = $"projects/{projectId}/packages/generic/{packageName}/{packageVersion}/{fileName}"; + + await _httpFacade.GetFile(url, outputPath ?? fileName); + } + + /// + /// Retrieves project issue. + /// + public async Task GetAsync(ProjectId projectId, int packageId) + { + return await _httpFacade.Get($"projects/{projectId}/packages/{packageId}"); + } + + + /// + /// List package files + /// + /// The ID, path or of the project. + /// The ID, path or of the package. + public async Task> GetPackageFilesAsync(ProjectId projectId, int packageId) { + return await _httpFacade.Get>($"projects/{projectId}/packages/{packageId}/package_files"); + } + + + /// + /// Delete a package. + /// + /// The ID, path or of the project. + /// The ID, path or of the package. + public async Task DeletePackageAsync(ProjectId projectId, int packageId) => + await _httpFacade.Delete($"projects/{projectId}/packages/{packageId}"); + + /// The ID, path or of the project. + /// The ID, path or of the group. + /// Packages retrieval options. + /// Packages satisfying options. + public async Task> GetAllAsync(ProjectId projectId = null, GroupId groupId = null, + Action options = null) + { + var queryOptions = new PackagesQueryOptions(); + options?.Invoke(queryOptions); + + string path = "packages"; + if (projectId != null) + { + path = $"projects/{projectId}/packages"; + } + else if (groupId != null) + { + path = $"groups/{groupId}/packages"; + } + + string url = _queryBuilder.Build(path, queryOptions); + + return await _httpFacade.GetPagedList(url); + } + } +} From 44770b37373d7cecd96081cc0d9c14d2eda3b6ad Mon Sep 17 00:00:00 2001 From: Nils Hjelte Date: Mon, 20 Sep 2021 12:21:31 +0200 Subject: [PATCH 2/2] Add missing packages file + update gitignore --- .gitignore | 4 ++ .../Packages/Requests/PackagesQueryOptions.cs | 48 +++++++++++++++++ .../Models/Packages/Responses/Package.cs | 53 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/GitLabApiClient/Models/Packages/Requests/PackagesQueryOptions.cs create mode 100644 src/GitLabApiClient/Models/Packages/Responses/Package.cs diff --git a/.gitignore b/.gitignore index aa64e316..2f41b2e3 100755 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,10 @@ PublishScripts/ **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ + +# Do not ignore packages API! +!**/Models/Packages/* + # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files diff --git a/src/GitLabApiClient/Models/Packages/Requests/PackagesQueryOptions.cs b/src/GitLabApiClient/Models/Packages/Requests/PackagesQueryOptions.cs new file mode 100644 index 00000000..d8aba175 --- /dev/null +++ b/src/GitLabApiClient/Models/Packages/Requests/PackagesQueryOptions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using GitLabApiClient.Models.Packages.Responses; + +namespace GitLabApiClient.Models.Packages.Requests +{ + /// + /// Options for issues listing + /// + public class PackagesQueryOptions + { + internal PackagesQueryOptions() { } + + + /// + /// Return all packages, or packages with a specific status + /// Default is Default. + /// + public PackageStatus Status { get; set; } + + /// + /// Filter the returned packages by type. One of conan, maven, npm, pypi, composer, nuget, helm, or golang. (Introduced in GitLab 12.9) + /// Defaults to packages of all types. (Introduced in GitLab 9.5). + /// + public PackageType PackageType { get; set; } + + /// + /// Filter the project packages with a fuzzy search by name. (Introduced in GitLab 12.9) + /// + public string PackageName { get; set; } + + + /// + /// Specifies issues order. Default is Creation time. + /// + public PackagesOrder Order { get; set; } + + /// + /// Specifies project sort order. Default is descending. + /// + public SortOrder SortOrder { get; set; } + + /// + /// When set to true, versionless packages are included in the response. (Introduced in GitLab 13.8) + /// + public bool IncludeVersionless { get; set; } = false; + } +} diff --git a/src/GitLabApiClient/Models/Packages/Responses/Package.cs b/src/GitLabApiClient/Models/Packages/Responses/Package.cs new file mode 100644 index 00000000..6ee3d53d --- /dev/null +++ b/src/GitLabApiClient/Models/Packages/Responses/Package.cs @@ -0,0 +1,53 @@ +using System; +using Newtonsoft.Json; + + +namespace GitLabApiClient.Models.Packages.Responses +{ + public sealed class Package + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("version")] + public string Version { get; set; } + + [JsonProperty("package_type")] + public string PackageType { get; set; } + + } + + public sealed class PackageFile + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("package_id")] + public string PackageId { get; set; } + + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + + [JsonProperty("file_name")] + public string FileName { get; set; } + + [JsonProperty("size")] + public int Size { get; set; } + + [JsonProperty("file_md5")] + public string FileMD5 { get; set; } + + [JsonProperty("file_sha1")] + public string FileSHA1 { get; set; } + + [JsonProperty("file_sha256")] + public string FileSHA256 { get; set; } + + } +}