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/GitLabClient.cs b/src/GitLabApiClient/GitLabClient.cs
index d64f7c34..856e6d8d 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();
@@ -76,6 +77,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);
Iterations = new IterationsClient(_httpFacade, iterationsBuilder);
@@ -147,6 +149,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 34b37849..0dd33728 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/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; }
+
+ }
+}
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);
+ }
+ }
+}