Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to upload to Generic Packages Repository #524

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NGitLab.Mock/Clients/GitLabClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions NGitLab.Mock/Clients/PackageClient.cs
Original file line number Diff line number Diff line change
@@ -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<Package> PublishAsync(int projectId, PackagePublish packagePublish, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}

public IEnumerable<PackageSearchResult> Get(int projectId, PackageQuery packageQuery)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meziantou I couldn't see any prior art for returning an IEnumerable<T> in any of the existing clients. Happy to make a change here, just wanted to make sure that we are on the same page before making any changes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other implementations use GitLabCollectionResponse<T>. This supports sync and async enumeration!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo! Missed that one! Will get that fixed up just now!

{
throw new System.NotImplementedException();
}

public Task<PackageSearchResult> GetByIdAsync(int projectId, int packageId, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
}
}
40 changes: 40 additions & 0 deletions NGitLab.Tests/PackageTests.cs
Original file line number Diff line number Diff line change
@@ -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"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to dispose the stream. Also, don't rely on the file system, you can use a MemoryStream to create a dummy content.

};

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);
}
}
}
3 changes: 3 additions & 0 deletions NGitLab/GitLabClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class GitLabClient : IGitLabClient
{
private readonly API _api;

public IPackageClient Packages { get; }

public IUserClient Users { get; }

public IProjectClient Projects { get; }
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions NGitLab/IGitLabClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
{
public interface IGitLabClient
{
IPackageClient Packages { get; }

IUserClient Users { get; }

IProjectClient Projects { get; }
Expand Down
34 changes: 34 additions & 0 deletions NGitLab/IPackageClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NGitLab.Models;

namespace NGitLab
{
public interface IPackageClient
{
/// <summary>
/// Add a package file with the proposed information to the GitLab Generic Package Repository for the selected Project Id.
/// </summary>
/// <param name="packagePublish">The information about the package file to publish.</param>
/// <returns>The package if it was created. Null if not.</returns>
Task<Package> PublishAsync(int projectId, PackagePublish packagePublish, CancellationToken cancellationToken = default);

/// <summary>
/// Gets all project packages based on the provided query parameters.
/// </summary>
/// <param name="projectId">The project id to search for packages in.</param>
/// <param name="packageQuery">The query parameters to be used for the search.</param>
/// <returns></returns>
IEnumerable<PackageSearchResult> Get(int projectId, PackageQuery packageQuery);

/// <summary>
/// Gets a single project package using the provided project and package ids.
/// </summary>
/// <param name="projectId">The project id that the package resides in.</param>
/// <param name="packageId">The package id that is being selected.</param>
/// <param name="cancellationToken">The cancellation token used to halt the request.</param>
/// <returns></returns>
Task<PackageSearchResult> GetByIdAsync(int projectId, int packageId, CancellationToken cancellationToken = default);
}
}
19 changes: 17 additions & 2 deletions NGitLab/Impl/HttpRequestor.GitLabRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ private sealed class GitLabRequest

public string JsonData { get; }

public FileContent FileContent { get; }

public FormDataContent FormData { get; }

private MethodType Method { get; }
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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";
Expand Down
72 changes: 72 additions & 0 deletions NGitLab/Impl/PackageClient.cs
Original file line number Diff line number Diff line change
@@ -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<Package> PublishAsync(int projectId, PackagePublish packagePublish, CancellationToken cancellationToken = default)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the url, I think the method name should be PublishGenericPackageAsync

Suggested change
public Task<Package> PublishAsync(int projectId, PackagePublish packagePublish, CancellationToken cancellationToken = default)
public Task<Package> PublishGenericPackageAsync(int projectId, PackagePublish packagePublish, CancellationToken cancellationToken = default)

{
var formData = new FileContent(packagePublish.PackageStream);

return _api.Put().With(formData).ToAsync<Package>(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<PackageSearchResult> Get(int projectId, PackageQuery packageQuery)
{
var url = CreateGetUrl(projectId, packageQuery);
return _api.Get().GetAllAsync<PackageSearchResult>(url);
}

public Task<PackageSearchResult> GetByIdAsync(int projectId, int packageId, CancellationToken cancellationToken = default)
{
return _api.Get().ToAsync<PackageSearchResult>(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;
}
}
}
17 changes: 17 additions & 0 deletions NGitLab/Models/FileContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.IO;

namespace NGitLab.Models
{
public sealed class FileContent
{
public FileContent(Stream stream)
{
Stream = stream;
}

/// <summary>
/// The stream to be uploaded.
/// </summary>
public Stream Stream { get; }
}
}
74 changes: 74 additions & 0 deletions NGitLab/Models/Package.cs
Original file line number Diff line number Diff line change
@@ -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; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New classes should use long for ids

Suggested change
public int Id { get; set; }
public long Id { get; set; }


[JsonPropertyName("package_id")]
public int PackageId { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public int PackageId { get; set; }
public long 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; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File size is a long. I'm not sure GitLab would allow such big files, but you never know

Suggested change
public int Size { get; set; }
public long Size { get; set; }


[JsonPropertyName("file_store")]
public int FileStore { get; set; }

[JsonPropertyName("file_md5")]
public string FileMD5 { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public string FileMD5 { get; set; }
public string FileMd5 { get; set; }


[JsonPropertyName("file_sha1")]
public string FileSHA1 { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public string FileSHA1 { get; set; }
public string FileSha1 { get; set; }


[JsonPropertyName("file_sha256")]
public string FileSHA256 { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public string FileSHA256 { get; set; }
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; }
}
}
10 changes: 10 additions & 0 deletions NGitLab/Models/PackageFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;

namespace NGitLab.Models
{
public class PackageFile
{
[JsonPropertyName("url")]
gep13 marked this conversation as resolved.
Show resolved Hide resolved
public string Url { get; set; }
}
}
13 changes: 13 additions & 0 deletions NGitLab/Models/PackageLink.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
10 changes: 10 additions & 0 deletions NGitLab/Models/PackageOrderBy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace NGitLab.Models
{
public enum PackageOrderBy
{
created_at,
name,
version,
type,
}
}
Loading