diff --git a/src/OrasProject.Oras/Extensions.cs b/src/OrasProject.Oras/Extensions.cs
index 505f7ff..6b048ac 100644
--- a/src/OrasProject.Oras/Extensions.cs
+++ b/src/OrasProject.Oras/Extensions.cs
@@ -52,8 +52,8 @@ public static async Task CopyGraphAsync(this ITarget src, ITarget dst, Descripto
{
// check if node exists in target
if (await dst.ExistsAsync(node, cancellationToken).ConfigureAwait(false))
- {
- return;
+ {
+ return;
}
// retrieve successors
diff --git a/src/OrasProject.Oras/Registry/IRegistry.cs b/src/OrasProject.Oras/Registry/IRegistry.cs
index c4db55a..92cc915 100644
--- a/src/OrasProject.Oras/Registry/IRegistry.cs
+++ b/src/OrasProject.Oras/Registry/IRegistry.cs
@@ -25,7 +25,7 @@ public interface IRegistry
///
///
///
- Task GetRepository(string name, CancellationToken cancellationToken = default);
+ Task GetRepositoryAsync(string name, CancellationToken cancellationToken = default);
///
/// Repositories lists the name of repositories available in the registry.
diff --git a/src/OrasProject.Oras/Registry/Reference.cs b/src/OrasProject.Oras/Registry/Reference.cs
index b048b75..54eca43 100644
--- a/src/OrasProject.Oras/Registry/Reference.cs
+++ b/src/OrasProject.Oras/Registry/Reference.cs
@@ -13,6 +13,7 @@
using OrasProject.Oras.Exceptions;
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace OrasProject.Oras.Registry;
@@ -82,11 +83,11 @@ public string Digest
{
if (_reference == null)
{
- throw new InvalidReferenceException("null content reference");
+ throw new InvalidReferenceException("Null content reference");
}
if (_isTag)
{
- throw new InvalidReferenceException("not a digest");
+ throw new InvalidReferenceException("Not a digest");
}
return _reference;
}
@@ -101,11 +102,11 @@ public string Tag
{
if (_reference == null)
{
- throw new InvalidReferenceException("null content reference");
+ throw new InvalidReferenceException("Null content reference");
}
if (!_isTag)
{
- throw new InvalidReferenceException("not a tag");
+ throw new InvalidReferenceException("Not a tag");
}
return _reference;
}
@@ -142,7 +143,7 @@ public static Reference Parse(string reference)
var parts = reference.Split('/', 2);
if (parts.Length == 1)
{
- throw new InvalidReferenceException("missing repository");
+ throw new InvalidReferenceException("Missing repository");
}
var registry = parts[0];
var path = parts[1];
@@ -186,6 +187,20 @@ public static Reference Parse(string reference)
return new Reference(registry, path);
}
+ public static bool TryParse(string reference, [NotNullWhen(true)] out Reference? parsedReference)
+ {
+ try
+ {
+ parsedReference = Parse(reference);
+ return true;
+ }
+ catch (InvalidReferenceException)
+ {
+ parsedReference = null;
+ return false;
+ }
+ }
+
public Reference(string registry) => _registry = ValidateRegistry(registry);
public Reference(string registry, string? repository) : this(registry)
@@ -199,7 +214,7 @@ private static string ValidateRegistry(string registry)
var url = "dummy://" + registry;
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute) || new Uri(url).Authority != registry)
{
- throw new InvalidReferenceException("invalid registry");
+ throw new InvalidReferenceException("Invalid registry");
}
return registry;
}
@@ -208,7 +223,7 @@ private static string ValidateRepository(string? repository)
{
if (repository == null || !_repositoryRegex.IsMatch(repository))
{
- throw new InvalidReferenceException("invalid respository");
+ throw new InvalidReferenceException("Invalid respository");
}
return repository;
}
@@ -219,7 +234,7 @@ private static string ValidateReferenceAsTag(string? reference)
{
if (reference == null || !_tagRegex.IsMatch(reference))
{
- throw new InvalidReferenceException("invalid tag");
+ throw new InvalidReferenceException("Invalid tag");
}
return reference;
}
diff --git a/src/OrasProject.Oras/Registry/Remote/Auth/HttpClientWithBasicAuth.cs b/src/OrasProject.Oras/Registry/Remote/Auth/HttpClientWithBasicAuth.cs
index 40e7633..ef451d7 100644
--- a/src/OrasProject.Oras/Registry/Remote/Auth/HttpClientWithBasicAuth.cs
+++ b/src/OrasProject.Oras/Registry/Remote/Auth/HttpClientWithBasicAuth.cs
@@ -16,26 +16,22 @@
using System.Net.Http.Headers;
using System.Text;
-namespace OrasProject.Oras.Remote.Auth
+namespace OrasProject.Oras.Registry.Remote.Auth;
+
+///
+/// HttpClientWithBasicAuth adds the Basic Auth Scheme to the Authorization Header
+///
+public class HttpClientWithBasicAuth : HttpClient
{
- ///
- /// HttpClientWithBasicAuth adds the Basic Auth Scheme to the Authorization Header
- ///
- public class HttpClientWithBasicAuth : HttpClient
- {
- public HttpClientWithBasicAuth(string username, string password)
- {
- this.AddUserAgent();
- DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
- Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")));
- }
+ public HttpClientWithBasicAuth(string username, string password) => Initialize(username, password);
- public HttpClientWithBasicAuth(string username, string password, HttpMessageHandler handler) : base(handler)
- {
- this.AddUserAgent();
- DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
- Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")));
- }
+ public HttpClientWithBasicAuth(string username, string password, HttpMessageHandler handler) : base(handler)
+ => Initialize(username, password);
+ private void Initialize(string username, string password)
+ {
+ this.AddUserAgent();
+ DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
+ Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")));
}
}
diff --git a/src/OrasProject.Oras/Registry/Remote/BlobStore.cs b/src/OrasProject.Oras/Registry/Remote/BlobStore.cs
index 3824445..96bf805 100644
--- a/src/OrasProject.Oras/Registry/Remote/BlobStore.cs
+++ b/src/OrasProject.Oras/Registry/Remote/BlobStore.cs
@@ -11,52 +11,92 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-using OrasProject.Oras.Content;
using OrasProject.Oras.Exceptions;
using OrasProject.Oras.Oci;
-using OrasProject.Oras.Remote;
using System;
using System.IO;
-using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using System.Web;
namespace OrasProject.Oras.Registry.Remote;
-internal class BlobStore : IBlobStore
+public class BlobStore(Repository repository) : IBlobStore
{
+ public Repository Repository { get; init; } = repository;
- public Repository Repository { get; set; }
-
- public BlobStore(Repository repository)
+ public async Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default)
{
- Repository = repository;
-
+ var remoteReference = Repository.ParseReferenceFromDigest(target.Digest);
+ var url = new UriFactory(remoteReference, Repository.Options.PlainHttp).BuildRepositoryBlob();
+ var response = await Repository.Options.HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ switch (response.StatusCode)
+ {
+ case HttpStatusCode.OK:
+ // server does not support seek as `Range` was ignored.
+ if (response.Content.Headers.ContentLength is var size && size != -1 && size != target.Size)
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: mismatch Content-Length");
+ }
+ return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
+ case HttpStatusCode.NotFound:
+ throw new NotFoundException($"{target.Digest}: not found");
+ default:
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch
+ {
+ response.Dispose();
+ throw;
+ }
}
-
- public async Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default)
+ ///
+ /// FetchReferenceAsync fetches the blob identified by the reference.
+ /// The reference must be a digest.
+ ///
+ ///
+ ///
+ ///
+ public async Task<(Descriptor Descriptor, Stream Stream)> FetchAsync(string reference, CancellationToken cancellationToken = default)
{
- var remoteReference = Repository.RemoteReference;
- Digest.Validate(target.Digest);
- remoteReference.ContentReference = target.Digest;
- var url = URLUtiliity.BuildRepositoryBlobURL(Repository.PlainHTTP, remoteReference);
- var resp = await Repository.HttpClient.GetAsync(url, cancellationToken);
- switch (resp.StatusCode)
+ var remoteReference = Repository.ParseReference(reference);
+ var refDigest = remoteReference.Digest;
+ var url = new UriFactory(remoteReference, Repository.Options.PlainHttp).BuildRepositoryBlob();
+ var response = await Repository.Options.HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ try
{
- case HttpStatusCode.OK:
- // server does not support seek as `Range` was ignored.
- if (resp.Content.Headers.ContentLength is var size && size != -1 && size != target.Size)
- {
- throw new Exception($"{resp.RequestMessage.Method} {resp.RequestMessage.RequestUri}: mismatch Content-Length");
- }
- return await resp.Content.ReadAsStreamAsync();
- case HttpStatusCode.NotFound:
- throw new NotFoundException($"{target.Digest}: not found");
- default:
- throw await ErrorUtility.ParseErrorResponse(resp);
+ switch (response.StatusCode)
+ {
+ case HttpStatusCode.OK:
+ // server does not support seek as `Range` was ignored.
+ Descriptor desc;
+ if (response.Content.Headers.ContentLength == -1)
+ {
+ desc = await ResolveAsync(refDigest, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ desc = response.GenerateBlobDescriptor(refDigest);
+ }
+ return (desc, await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false));
+ case HttpStatusCode.NotFound:
+ throw new NotFoundException();
+ default:
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch
+ {
+ response.Dispose();
+ throw;
}
}
@@ -70,14 +110,13 @@ public async Task ExistsAsync(Descriptor target, CancellationToken cancell
{
try
{
- await ResolveAsync(target.Digest, cancellationToken);
+ await ResolveAsync(target.Digest, cancellationToken).ConfigureAwait(false);
return true;
}
catch (NotFoundException)
{
return false;
}
-
}
///
@@ -98,64 +137,37 @@ public async Task ExistsAsync(Descriptor target, CancellationToken cancell
///
public async Task PushAsync(Descriptor expected, Stream content, CancellationToken cancellationToken = default)
{
- var url = URLUtiliity.BuildRepositoryBlobUploadURL(Repository.PlainHTTP, Repository.RemoteReference);
- using var resp = await Repository.HttpClient.PostAsync(url, null, cancellationToken);
- var reqHostname = resp.RequestMessage.RequestUri.Host;
- var reqPort = resp.RequestMessage.RequestUri.Port;
- if (resp.StatusCode != HttpStatusCode.Accepted)
+ var url = new UriFactory(Repository.Options).BuildRepositoryBlobUpload();
+ using (var response = await Repository.Options.HttpClient.PostAsync(url, null, cancellationToken).ConfigureAwait(false))
{
- throw await ErrorUtility.ParseErrorResponse(resp);
+ if (response.StatusCode != HttpStatusCode.Accepted)
+ {
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ var location = response.Headers.Location ?? throw new HttpRequestException("missing location header");
+ url = location.IsAbsoluteUri ? location : new Uri(url, location);
}
- string location;
// monolithic upload
- if (!resp.Headers.Location.IsAbsoluteUri)
- {
- location = resp.RequestMessage.RequestUri.Scheme + "://" + resp.RequestMessage.RequestUri.Authority + resp.Headers.Location;
- }
- else
- {
- location = resp.Headers.Location.ToString();
- }
- // work-around solution for https://github.com/oras-project/oras-go/issues/177
- // For some registries, if the port 443 is explicitly set to the hostname plicitly set to the hostname
- // like registry.wabbit-networks.io:443/myrepo, blob push will fail since
- // the hostname of the Location header in the response is set to
- // registry.wabbit-networks.io instead of registry.wabbit-networks.io:443.
- var uri = new UriBuilder(location);
- var locationHostname = uri.Host;
- var locationPort = uri.Port;
- // if location port 443 is missing, add it back
- if (reqPort == 443 && locationHostname == reqHostname && locationPort != reqPort)
+ // add digest key to query string with expected digest value
+ var req = new HttpRequestMessage(HttpMethod.Put, new UriBuilder(url)
{
- location = new UriBuilder($"{locationHostname}:{reqPort}").ToString();
- }
-
- url = location;
-
- var req = new HttpRequestMessage(HttpMethod.Put, url);
+ Query = $"digest={HttpUtility.UrlEncode(expected.Digest)}"
+ }.Uri);
req.Content = new StreamContent(content);
req.Content.Headers.ContentLength = expected.Size;
// the expected media type is ignored as in the API doc.
- req.Content.Headers.Add("Content-Type", "application/octet-stream");
-
- // add digest key to query string with expected digest value
- req.RequestUri = new UriBuilder($"{req.RequestUri}?digest={expected.Digest}").Uri;
+ req.Content.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet);
- //reuse credential from previous POST request
- resp.Headers.TryGetValues("Authorization", out var auth);
- if (auth != null)
+ using (var response = await Repository.Options.HttpClient.SendAsync(req, cancellationToken).ConfigureAwait(false))
{
- req.Headers.Add("Authorization", auth.FirstOrDefault());
+ if (response.StatusCode != HttpStatusCode.Created)
+ {
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
+ }
}
- using var resp2 = await Repository.HttpClient.SendAsync(req, cancellationToken);
- if (resp2.StatusCode != HttpStatusCode.Created)
- {
- throw await ErrorUtility.ParseErrorResponse(resp2);
- }
-
- return;
}
///
@@ -168,14 +180,14 @@ public async Task ResolveAsync(string reference, CancellationToken c
{
var remoteReference = Repository.ParseReference(reference);
var refDigest = remoteReference.Digest;
- var url = URLUtiliity.BuildRepositoryBlobURL(Repository.PlainHTTP, remoteReference);
+ var url = new UriFactory(remoteReference, Repository.Options.PlainHttp).BuildRepositoryBlob();
var requestMessage = new HttpRequestMessage(HttpMethod.Head, url);
- using var resp = await Repository.HttpClient.SendAsync(requestMessage, cancellationToken);
+ using var resp = await Repository.Options.HttpClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
return resp.StatusCode switch
{
- HttpStatusCode.OK => Repository.GenerateBlobDescriptor(resp, refDigest),
+ HttpStatusCode.OK => resp.GenerateBlobDescriptor(refDigest),
HttpStatusCode.NotFound => throw new NotFoundException($"{remoteReference.ContentReference}: not found"),
- _ => throw await ErrorUtility.ParseErrorResponse(resp)
+ _ => throw await resp.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false)
};
}
@@ -186,42 +198,5 @@ public async Task ResolveAsync(string reference, CancellationToken c
///
///
public async Task DeleteAsync(Descriptor target, CancellationToken cancellationToken = default)
- {
- await Repository.DeleteAsync(target, false, cancellationToken);
- }
-
- ///
- /// FetchReferenceAsync fetches the blob identified by the reference.
- /// The reference must be a digest.
- ///
- ///
- ///
- ///
- public async Task<(Descriptor Descriptor, Stream Stream)> FetchAsync(string reference, CancellationToken cancellationToken = default)
- {
- var remoteReference = Repository.ParseReference(reference);
- var refDigest = remoteReference.Digest;
- var url = URLUtiliity.BuildRepositoryBlobURL(Repository.PlainHTTP, remoteReference);
- var resp = await Repository.HttpClient.GetAsync(url, cancellationToken);
- switch (resp.StatusCode)
- {
- case HttpStatusCode.OK:
- // server does not support seek as `Range` was ignored.
- Descriptor desc;
- if (resp.Content.Headers.ContentLength == -1)
- {
- desc = await ResolveAsync(refDigest, cancellationToken);
- }
- else
- {
- desc = Repository.GenerateBlobDescriptor(resp, refDigest);
- }
-
- return (desc, await resp.Content.ReadAsStreamAsync());
- case HttpStatusCode.NotFound:
- throw new NotFoundException();
- default:
- throw await ErrorUtility.ParseErrorResponse(resp);
- }
- }
+ => await Repository.DeleteAsync(target, false, cancellationToken).ConfigureAwait(false);
}
diff --git a/src/OrasProject.Oras/Registry/Remote/ErrorUtility.cs b/src/OrasProject.Oras/Registry/Remote/ErrorUtility.cs
deleted file mode 100644
index b2c3de5..0000000
--- a/src/OrasProject.Oras/Registry/Remote/ErrorUtility.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright The ORAS Authors.
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace OrasProject.Oras.Remote
-{
- internal class ErrorUtility
- {
- ///
- /// ParseErrorResponse parses the error returned by the remote registry.
- ///
- ///
- ///
- internal static async Task ParseErrorResponse(HttpResponseMessage response)
- {
- var body = await response.Content.ReadAsStringAsync();
- return new Exception(new
- {
- response.RequestMessage.Method,
- URL = response.RequestMessage.RequestUri,
- response.StatusCode,
- Errors = body
- }.ToString());
- }
- }
-}
diff --git a/src/OrasProject.Oras/Registry/Remote/HttpClientExtensions.cs b/src/OrasProject.Oras/Registry/Remote/HttpClientExtensions.cs
index 6ba4295..8a92617 100644
--- a/src/OrasProject.Oras/Registry/Remote/HttpClientExtensions.cs
+++ b/src/OrasProject.Oras/Registry/Remote/HttpClientExtensions.cs
@@ -13,13 +13,15 @@
using System.Net.Http;
-namespace OrasProject.Oras.Remote
+namespace OrasProject.Oras.Registry.Remote;
+
+internal static class HttpClientExtensions
{
- internal static class HttpClientExtensions
+ private const string _userAgent = "oras-dotnet";
+
+ public static HttpClient AddUserAgent(this HttpClient client)
{
- public static void AddUserAgent(this HttpClient client)
- {
- client.DefaultRequestHeaders.Add("User-Agent", new string[] { "oras-dotnet" });
- }
+ client.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent);
+ return client;
}
}
diff --git a/src/OrasProject.Oras/Registry/Remote/HttpResponseMessageExtensions.cs b/src/OrasProject.Oras/Registry/Remote/HttpResponseMessageExtensions.cs
new file mode 100644
index 0000000..a7271c9
--- /dev/null
+++ b/src/OrasProject.Oras/Registry/Remote/HttpResponseMessageExtensions.cs
@@ -0,0 +1,244 @@
+// Copyright The ORAS Authors.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using OrasProject.Oras.Content;
+using OrasProject.Oras.Oci;
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace OrasProject.Oras.Registry.Remote;
+
+internal static class HttpResponseMessageExtensions
+{
+ ///
+ /// Parses the error returned by the remote registry.
+ ///
+ ///
+ ///
+ public static async Task ParseErrorResponseAsync(this HttpResponseMessage response, CancellationToken cancellationToken)
+ {
+ var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ return new Exception(new
+ {
+ response.RequestMessage!.Method,
+ URL = response.RequestMessage.RequestUri,
+ response.StatusCode,
+ Errors = body
+ }.ToString());
+ }
+
+ ///
+ /// Returns the URL of the response's "Link" header, if present.
+ ///
+ /// next link or null if not present
+ public static Uri? ParseLink(this HttpResponseMessage response)
+ {
+ if (!response.Headers.TryGetValues("Link", out var values))
+ {
+ return null;
+ }
+
+ var link = values.FirstOrDefault();
+ if (string.IsNullOrEmpty(link) || link[0] != '<')
+ {
+ throw new Exception($"invalid next link {link}: missing '<");
+ }
+ if (link.IndexOf('>') is var index && index == -1)
+ {
+ throw new Exception($"invalid next link {link}: missing '>'");
+ }
+ link = link[1..index];
+ if (!Uri.IsWellFormedUriString(link, UriKind.RelativeOrAbsolute))
+ {
+ throw new Exception($"invalid next link {link}");
+ }
+
+ return new Uri(response.RequestMessage!.RequestUri!, link);
+ }
+
+ ///
+ /// VerifyContentDigest verifies "Docker-Content-Digest" header if present.
+ /// OCI distribution-spec states the Docker-Content-Digest header is optional.
+ /// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#legacy-docker-support-http-headers
+ ///
+ ///
+ ///
+ ///
+ public static void VerifyContentDigest(this HttpResponseMessage response, string expected)
+ {
+ if (!response.Content.Headers.TryGetValues("Docker-Content-Digest", out var digestValues))
+ {
+ return;
+ }
+ var digestStr = digestValues.FirstOrDefault();
+ if (string.IsNullOrEmpty(digestStr))
+ {
+ return;
+ }
+
+ string contentDigest;
+ try
+ {
+ contentDigest = Digest.Validate(digestStr);
+ }
+ catch (Exception)
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: invalid response header: `Docker-Content-Digest: {digestStr}`");
+ }
+ if (contentDigest != expected)
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: invalid response; digest mismatch in Docker-Content-Digest: received {contentDigest} when expecting {digestStr}");
+ }
+ }
+
+ ///
+ /// Returns a descriptor generated from the response.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static Descriptor GenerateBlobDescriptor(this HttpResponseMessage response, string expectedDigest)
+ {
+ var mediaType = response.Content.Headers.ContentType?.MediaType;
+ if (string.IsNullOrEmpty(mediaType))
+ {
+ mediaType = MediaTypeNames.Application.Octet;
+ }
+ var size = response.Content.Headers.ContentLength ?? -1;
+ if (size == -1)
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: unknown response Content-Length");
+ }
+ response.VerifyContentDigest(expectedDigest);
+ return new Descriptor
+ {
+ MediaType = mediaType,
+ Digest = expectedDigest,
+ Size = size
+ };
+ }
+
+ ///
+ /// Returns a descriptor generated from the response.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static async Task GenerateDescriptorAsync(this HttpResponseMessage response, Reference reference, CancellationToken cancellationToken)
+ {
+ // 1. Validate Content-Type
+ var mediaType = response.Content.Headers.ContentType?.MediaType;
+ if (!MediaTypeHeaderValue.TryParse(mediaType, out _))
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: invalid response `Content-Type` header");
+ }
+
+ // 2. Validate Size
+ var size = response.Content.Headers.ContentLength ?? -1;
+ if (size == -1)
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: unknown response Content-Length");
+ }
+
+ // 3. Validate Client Reference
+ string? refDigest = null;
+ try
+ {
+ refDigest = reference.Digest;
+ }
+ catch { }
+
+ // 4. Validate Server Digest (if present)
+ string? serverDigest = null;
+ if (response.Content.Headers.TryGetValues("Docker-Content-Digest", out var serverHeaderDigest))
+ {
+ serverDigest = serverHeaderDigest.FirstOrDefault();
+ if (!string.IsNullOrEmpty(serverDigest))
+ {
+ try
+ {
+ response.VerifyContentDigest(serverDigest);
+ }
+ catch
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: invalid response header value: `Docker-Content-Digest: {serverHeaderDigest}`");
+ }
+ }
+ }
+
+ // 5. Now, look for specific error conditions;
+ string contentDigest;
+ if (string.IsNullOrEmpty(serverDigest))
+ {
+ if (response.RequestMessage!.Method == HttpMethod.Head)
+ {
+ if (string.IsNullOrEmpty(refDigest))
+ {
+ // HEAD without server `Docker-Content-Digest`
+ // immediate fail
+ throw new Exception($"{response.RequestMessage.Method} {response.RequestMessage.RequestUri}: missing required header {serverHeaderDigest}");
+ }
+ // Otherwise, just trust the client-supplied digest
+ contentDigest = refDigest;
+ }
+ else
+ {
+ // GET without server `Docker-Content-Digest header forces the
+ // expensive calculation
+ try
+ {
+ contentDigest = await response.CalculateDigestFromResponse(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ throw new Exception($"failed to calculate digest on response body; {e.Message}");
+ }
+ }
+ }
+ else
+ {
+ contentDigest = serverDigest;
+ }
+ if (!string.IsNullOrEmpty(refDigest) && refDigest != contentDigest)
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: invalid response; digest mismatch in {serverHeaderDigest}: received {contentDigest} when expecting {refDigest}");
+ }
+
+ // 6. Finally, if we made it this far, then all is good; return the descriptor
+ return new Descriptor
+ {
+ MediaType = mediaType,
+ Digest = contentDigest,
+ Size = size
+ };
+ }
+
+ ///
+ /// CalculateDigestFromResponse calculates the actual digest of the response body
+ /// taking care not to destroy it in the process
+ ///
+ ///
+ private static async Task CalculateDigestFromResponse(this HttpResponseMessage response, CancellationToken cancellationToken)
+ {
+ var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
+ return Digest.ComputeSHA256(bytes);
+ }
+}
diff --git a/src/OrasProject.Oras/Registry/Remote/LinkUtility.cs b/src/OrasProject.Oras/Registry/Remote/LinkUtility.cs
deleted file mode 100644
index a9bc555..0000000
--- a/src/OrasProject.Oras/Registry/Remote/LinkUtility.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright The ORAS Authors.
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Linq;
-using System.Net.Http;
-
-namespace OrasProject.Oras.Remote
-{
- internal class LinkUtility
- {
- ///
- /// ParseLink returns the URL of the response's "Link" header, if present.
- ///
- ///
- ///
- internal static string ParseLink(HttpResponseMessage resp)
- {
- string link;
- if (resp.Headers.TryGetValues("Link", out var values))
- {
- link = values.FirstOrDefault();
- }
- else
- {
- throw new NoLinkHeaderException();
- }
-
- if (link[0] != '<')
- {
- throw new Exception($"invalid next link {link}: missing '<");
- }
- if (link.IndexOf('>') is var index && index == -1)
- {
- throw new Exception($"invalid next link {link}: missing '>'");
- }
- else
- {
- link = link[1..index];
- }
-
- if (!Uri.IsWellFormedUriString(link, UriKind.RelativeOrAbsolute))
- {
- throw new Exception($"invalid next link {link}");
- }
-
- var scheme = resp.RequestMessage.RequestUri.Scheme;
- var authority = resp.RequestMessage.RequestUri.Authority;
- Uri baseUri = new Uri(scheme + "://" + authority);
- Uri resolvedUri = new Uri(baseUri, link);
-
- return resolvedUri.AbsoluteUri;
- }
-
-
- ///
- /// NoLinkHeaderException is thrown when a link header is missing.
- ///
- internal class NoLinkHeaderException : Exception
- {
- public NoLinkHeaderException()
- {
- }
-
- public NoLinkHeaderException(string message)
- : base(message)
- {
- }
-
- public NoLinkHeaderException(string message, Exception inner)
- : base(message, inner)
- {
- }
- }
- }
-}
diff --git a/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs b/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs
index 7170d50..a47e331 100644
--- a/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs
+++ b/src/OrasProject.Oras/Registry/Remote/ManifestStore.cs
@@ -11,33 +11,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-using OrasProject.Oras.Content;
using OrasProject.Oras.Exceptions;
using OrasProject.Oras.Oci;
-using OrasProject.Oras.Remote;
using System;
-using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace OrasProject.Oras.Registry.Remote;
-public class ManifestStore : IManifestStore
+public class ManifestStore(Repository repository) : IManifestStore
{
- public Repository Repository { get; set; }
-
- public ManifestStore(Repository repository)
- {
- Repository = repository;
- }
+ public Repository Repository { get; init; } = repository;
///
- /// FetchASync fetches the content identified by the descriptor.
+ /// Fetches the content identified by the descriptor.
///
///
///
@@ -46,39 +36,84 @@ public ManifestStore(Repository repository)
///
public async Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default)
{
- var remoteReference = Repository.RemoteReference;
- remoteReference.ContentReference = target.Digest;
- var url = URLUtiliity.BuildRepositoryManifestURL(Repository.PlainHTTP, remoteReference);
- var req = new HttpRequestMessage(HttpMethod.Get, url);
- req.Headers.Add("Accept", target.MediaType);
- var resp = await Repository.HttpClient.SendAsync(req, cancellationToken);
-
- switch (resp.StatusCode)
+ var remoteReference = Repository.ParseReferenceFromDigest(target.Digest);
+ var url = new UriFactory(remoteReference, Repository.Options.PlainHttp).BuildRepositoryManifest();
+ var request = new HttpRequestMessage(HttpMethod.Get, url);
+ request.Headers.Accept.ParseAdd(target.MediaType);
+ var response = await Repository.Options.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ try
{
- case HttpStatusCode.OK:
- break;
- case HttpStatusCode.NotFound:
- throw new NotFoundException($"digest {target.Digest} not found");
- default:
- throw await ErrorUtility.ParseErrorResponse(resp);
+ switch (response.StatusCode)
+ {
+ case HttpStatusCode.OK:
+ break;
+ case HttpStatusCode.NotFound:
+ throw new NotFoundException($"digest {target.Digest} not found");
+ default:
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
+ }
+ var mediaType = response.Content.Headers.ContentType?.MediaType;
+ if (mediaType != target.MediaType)
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: mismatch response Content-Type {mediaType}: expect {target.MediaType}");
+ }
+ if (response.Content.Headers.ContentLength is var size && size != -1 && size != target.Size)
+ {
+ throw new Exception($"{response.RequestMessage!.Method} {response.RequestMessage.RequestUri}: mismatch Content-Length");
+ }
+ response.VerifyContentDigest(target.Digest);
+ return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
}
- var mediaType = resp.Content.Headers?.ContentType.MediaType;
- if (mediaType != target.MediaType)
+ catch
{
- throw new Exception(
- $"{resp.RequestMessage.Method} {resp.RequestMessage.RequestUri}: mismatch response Content-Type {mediaType}: expect {target.MediaType}");
+ response.Dispose();
+ throw;
}
- if (resp.Content.Headers.ContentLength is var size && size != -1 && size != target.Size)
+ }
+
+ ///
+ /// Fetches the manifest identified by the reference.
+ ///
+ ///
+ ///
+ ///
+ public async Task<(Descriptor Descriptor, Stream Stream)> FetchAsync(string reference, CancellationToken cancellationToken = default)
+ {
+ var remoteReference = Repository.ParseReference(reference);
+ var url = new UriFactory(remoteReference, Repository.Options.PlainHttp).BuildRepositoryManifest();
+ var request = new HttpRequestMessage(HttpMethod.Get, url);
+ request.Headers.Accept.ParseAdd(Repository.ManifestAcceptHeader());
+ var response = await Repository.Options.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ try
{
- throw new Exception(
- $"{resp.RequestMessage.Method} {resp.RequestMessage.RequestUri}: mismatch Content-Length");
+ switch (response.StatusCode)
+ {
+ case HttpStatusCode.OK:
+ Descriptor desc;
+ if (response.Content.Headers.ContentLength == -1)
+ {
+ desc = await ResolveAsync(reference, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ desc = await response.GenerateDescriptorAsync(remoteReference, cancellationToken).ConfigureAwait(false);
+ }
+ return (desc, await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false));
+ case HttpStatusCode.NotFound:
+ throw new NotFoundException($"{request.Method} {request.RequestUri}: manifest unknown");
+ default:
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch
+ {
+ response.Dispose();
+ throw;
}
- Repository.VerifyContentDigest(resp, target.Digest);
- return await resp.Content.ReadAsStreamAsync();
}
///
- /// ExistsAsync returns true if the described content exists.
+ /// Returns true if the described content exists.
///
///
///
@@ -87,258 +122,98 @@ public async Task ExistsAsync(Descriptor target, CancellationToken cancell
{
try
{
- await ResolveAsync(target.Digest, cancellationToken);
+ await ResolveAsync(target.Digest, cancellationToken).ConfigureAwait(false);
return true;
}
catch (NotFoundException)
{
return false;
}
-
}
///
- /// PushAsync pushes the content, matching the expected descriptor.
+ /// Pushes the content, matching the expected descriptor.
///
///
///
///
///
public async Task PushAsync(Descriptor expected, Stream content, CancellationToken cancellationToken = default)
- {
- await InternalPushAsync(expected, content, expected.Digest, cancellationToken);
- }
-
+ => await InternalPushAsync(expected, content, expected.Digest, cancellationToken).ConfigureAwait(false);
///
- /// PushAsync pushes the manifest content, matching the expected descriptor.
+ /// PushReferenceASync pushes the manifest with a reference tag.
///
///
- ///
+ ///
///
///
- private async Task InternalPushAsync(Descriptor expected, Stream stream, string reference, CancellationToken cancellationToken)
- {
- var remoteReference = Repository.RemoteReference;
- remoteReference.ContentReference = reference;
- var url = URLUtiliity.BuildRepositoryManifestURL(Repository.PlainHTTP, remoteReference);
- var req = new HttpRequestMessage(HttpMethod.Put, url);
- req.Content = new StreamContent(stream);
- req.Content.Headers.ContentLength = expected.Size;
- req.Content.Headers.Add("Content-Type", expected.MediaType);
- var client = Repository.HttpClient;
- using var resp = await client.SendAsync(req, cancellationToken);
- if (resp.StatusCode != HttpStatusCode.Created)
- {
- throw await ErrorUtility.ParseErrorResponse(resp);
- }
- Repository.VerifyContentDigest(resp, expected.Digest);
- }
-
- public async Task ResolveAsync(string reference, CancellationToken cancellationToken = default)
- {
- var remoteReference = Repository.ParseReference(reference);
- var url = URLUtiliity.BuildRepositoryManifestURL(Repository.PlainHTTP, remoteReference);
- var req = new HttpRequestMessage(HttpMethod.Head, url);
- req.Headers.Add("Accept", ManifestUtility.ManifestAcceptHeader(Repository.ManifestMediaTypes));
- using var res = await Repository.HttpClient.SendAsync(req, cancellationToken);
-
- return res.StatusCode switch
- {
- HttpStatusCode.OK => await GenerateDescriptorAsync(res, remoteReference, req.Method),
- HttpStatusCode.NotFound => throw new NotFoundException($"reference {reference} not found"),
- _ => throw await ErrorUtility.ParseErrorResponse(res)
- };
- }
-
- ///
- /// GenerateDescriptor returns a descriptor generated from the response.
- ///
- ///
- ///
- ///
///
- ///
- public async Task GenerateDescriptorAsync(HttpResponseMessage res, Reference reference, HttpMethod httpMethod)
- {
- string mediaType;
- try
- {
- // 1. Validate Content-Type
- mediaType = res.Content.Headers.ContentType.MediaType;
- MediaTypeHeaderValue.Parse(mediaType);
- }
- catch (Exception e)
- {
- throw new Exception($"{res.RequestMessage.Method} {res.RequestMessage.RequestUri}: invalid response `Content-Type` header; {e.Message}");
- }
-
- // 2. Validate Size
- if (!res.Content.Headers.ContentLength.HasValue || res.Content.Headers.ContentLength == -1)
- {
- throw new Exception($"{res.RequestMessage.Method} {res.RequestMessage.RequestUri}: unknown response Content-Length");
- }
-
- // 3. Validate Client Reference
- string refDigest = string.Empty;
- try
- {
- refDigest = reference.Digest;
- }
- catch (Exception)
- {
- }
-
-
- // 4. Validate Server Digest (if present)
- res.Content.Headers.TryGetValues("Docker-Content-Digest", out IEnumerable serverHeaderDigest);
- var serverDigest = serverHeaderDigest?.First();
- if (!string.IsNullOrEmpty(serverDigest))
- {
- try
- {
- Repository.VerifyContentDigest(res, serverDigest);
- }
- catch (Exception)
- {
- throw new Exception($"{res.RequestMessage.Method} {res.RequestMessage.RequestUri}: invalid response header value: `Docker-Content-Digest: {serverHeaderDigest}`");
- }
- }
-
- // 5. Now, look for specific error conditions;
- string contentDigest;
-
- if (string.IsNullOrEmpty(serverDigest))
- {
- if (httpMethod == HttpMethod.Head)
- {
- if (string.IsNullOrEmpty(refDigest))
- {
- // HEAD without server `Docker-Content-Digest`
- // immediate fail
- throw new Exception($"{res.RequestMessage.Method} {res.RequestMessage.RequestUri}: HTTP {httpMethod} request missing required header {serverHeaderDigest}");
- }
- // Otherwise, just trust the client-supplied digest
- contentDigest = refDigest;
- }
- else
- {
- // GET without server `Docker-Content-Digest header forces the
- // expensive calculation
- string calculatedDigest;
- try
- {
- calculatedDigest = await CalculateDigestFromResponse(res);
- }
- catch (Exception e)
- {
- throw new Exception($"failed to calculate digest on response body; {e.Message}");
- }
- contentDigest = calculatedDigest;
- }
- }
- else
- {
- contentDigest = serverDigest;
- }
- if (!string.IsNullOrEmpty(refDigest) && refDigest != contentDigest)
- {
- throw new Exception($"{res.RequestMessage.Method} {res.RequestMessage.RequestUri}: invalid response; digest mismatch in {serverHeaderDigest}: received {contentDigest} when expecting {refDigest}");
- }
-
- // 6. Finally, if we made it this far, then all is good; return the descriptor
- return new Descriptor
- {
- MediaType = mediaType,
- Digest = contentDigest,
- Size = res.Content.Headers.ContentLength.Value
- };
- }
-
- ///
- /// CalculateDigestFromResponse calculates the actual digest of the response body
- /// taking care not to destroy it in the process
- ///
- ///
- static async Task CalculateDigestFromResponse(HttpResponseMessage res)
+ public async Task PushAsync(Descriptor expected, Stream content, string reference, CancellationToken cancellationToken = default)
{
- var bytes = await res.Content.ReadAsByteArrayAsync();
- return Digest.ComputeSHA256(bytes);
+ var contentReference = Repository.ParseReference(reference).ContentReference!;
+ await InternalPushAsync(expected, content, contentReference, cancellationToken).ConfigureAwait(false);
}
///
- /// DeleteAsync removes the manifest content identified by the descriptor.
+ /// Pushes the manifest content, matching the expected descriptor.
///
- ///
+ ///
+ ///
+ ///
///
- ///
- public async Task DeleteAsync(Descriptor target, CancellationToken cancellationToken = default)
+ private async Task InternalPushAsync(Descriptor expected, Stream stream, string contentReference, CancellationToken cancellationToken)
{
- await Repository.DeleteAsync(target, true, cancellationToken);
+ var remoteReference = Repository.ParseReference(contentReference);
+ var url = new UriFactory(remoteReference, Repository.Options.PlainHttp).BuildRepositoryManifest();
+ var request = new HttpRequestMessage(HttpMethod.Put, url);
+ request.Content = new StreamContent(stream);
+ request.Content.Headers.ContentLength = expected.Size;
+ request.Content.Headers.Add("Content-Type", expected.MediaType);
+ var client = Repository.Options.HttpClient;
+ using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ if (response.StatusCode != HttpStatusCode.Created)
+ {
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
+ }
+ response.VerifyContentDigest(expected.Digest);
}
-
- ///
- /// FetchReferenceAsync fetches the manifest identified by the reference.
- ///
- ///
- ///
- ///
- public async Task<(Descriptor Descriptor, Stream Stream)> FetchAsync(string reference, CancellationToken cancellationToken = default)
+ public async Task ResolveAsync(string reference, CancellationToken cancellationToken = default)
{
var remoteReference = Repository.ParseReference(reference);
- var url = URLUtiliity.BuildRepositoryManifestURL(Repository.PlainHTTP, remoteReference);
- var req = new HttpRequestMessage(HttpMethod.Get, url);
- req.Headers.Add("Accept", ManifestUtility.ManifestAcceptHeader(Repository.ManifestMediaTypes));
- var resp = await Repository.HttpClient.SendAsync(req, cancellationToken);
- switch (resp.StatusCode)
+ var url = new UriFactory(remoteReference, Repository.Options.PlainHttp).BuildRepositoryManifest();
+ var request = new HttpRequestMessage(HttpMethod.Head, url);
+ request.Headers.Accept.ParseAdd(Repository.ManifestAcceptHeader());
+ using var response = await Repository.Options.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ return response.StatusCode switch
{
- case HttpStatusCode.OK:
- Descriptor desc;
- if (resp.Content.Headers.ContentLength == -1)
- {
- desc = await ResolveAsync(reference, cancellationToken);
- }
- else
- {
- desc = await GenerateDescriptorAsync(resp, remoteReference, HttpMethod.Get);
- }
-
- return (desc, await resp.Content.ReadAsStreamAsync());
- case HttpStatusCode.NotFound:
- throw new NotFoundException($"{req.Method} {req.RequestUri}: manifest unknown");
- default:
- throw await ErrorUtility.ParseErrorResponse(resp);
-
- }
+ HttpStatusCode.OK => await response.GenerateDescriptorAsync(remoteReference, cancellationToken).ConfigureAwait(false),
+ HttpStatusCode.NotFound => throw new NotFoundException($"reference {reference} not found"),
+ _ => throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false)
+ };
}
///
- /// PushReferenceASync pushes the manifest with a reference tag.
+ /// Tags a manifest descriptor with a reference string.
///
- ///
- ///
+ ///
///
///
///
- public async Task PushAsync(Descriptor expected, Stream content, string reference,
- CancellationToken cancellationToken = default)
+ public async Task TagAsync(Descriptor descriptor, string reference, CancellationToken cancellationToken = default)
{
var remoteReference = Repository.ParseReference(reference);
- await InternalPushAsync(expected, content, remoteReference.ContentReference, cancellationToken);
+ using var contentStream = await FetchAsync(descriptor, cancellationToken).ConfigureAwait(false);
+ await InternalPushAsync(descriptor, contentStream, remoteReference.ContentReference!, cancellationToken).ConfigureAwait(false);
}
///
- /// TagAsync tags a manifest descriptor with a reference string.
+ /// Removes the manifest content identified by the descriptor.
///
- ///
- ///
+ ///
///
///
- public async Task TagAsync(Descriptor descriptor, string reference, CancellationToken cancellationToken = default)
- {
- var remoteReference = Repository.ParseReference(reference);
- var rc = await FetchAsync(descriptor, cancellationToken);
- await InternalPushAsync(descriptor, rc, remoteReference.ContentReference, cancellationToken);
- }
+ public async Task DeleteAsync(Descriptor target, CancellationToken cancellationToken = default)
+ => await Repository.DeleteAsync(target, true, cancellationToken).ConfigureAwait(false);
}
diff --git a/src/OrasProject.Oras/Registry/Remote/ManifestUtility.cs b/src/OrasProject.Oras/Registry/Remote/ManifestUtility.cs
deleted file mode 100644
index 154faa2..0000000
--- a/src/OrasProject.Oras/Registry/Remote/ManifestUtility.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright The ORAS Authors.
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using OrasProject.Oras.Oci;
-using System.Linq;
-
-namespace OrasProject.Oras.Remote
-{
- internal static class ManifestUtility
- {
- internal static string[] DefaultManifestMediaTypes = new[]
- {
- Docker.MediaType.Manifest,
- Docker.MediaType.ManifestList,
- Oci.MediaType.ImageIndex,
- Oci.MediaType.ImageManifest
- };
-
- ///
- /// isManifest determines if the given descriptor is a manifest.
- ///
- ///
- ///
- ///
- internal static bool IsManifest(string[] manifestMediaTypes, Descriptor desc)
- {
- if (manifestMediaTypes == null || manifestMediaTypes.Length == 0)
- {
- manifestMediaTypes = DefaultManifestMediaTypes;
- }
-
- if (manifestMediaTypes.Any((mediaType) => mediaType == desc.MediaType))
- {
- return true;
- }
-
- return false;
- }
-
- ///
- /// ManifestAcceptHeader returns the accept header for the given manifest media types.
- ///
- ///
- ///
- internal static string ManifestAcceptHeader(string[] manifestMediaTypes)
- {
- if (manifestMediaTypes == null || manifestMediaTypes.Length == 0)
- {
- manifestMediaTypes = DefaultManifestMediaTypes;
- }
-
- return string.Join(',', manifestMediaTypes);
- }
- }
-}
diff --git a/src/OrasProject.Oras/Registry/Remote/Registry.cs b/src/OrasProject.Oras/Registry/Remote/Registry.cs
index 297795c..4a2a76a 100644
--- a/src/OrasProject.Oras/Registry/Remote/Registry.cs
+++ b/src/OrasProject.Oras/Registry/Remote/Registry.cs
@@ -20,139 +20,125 @@
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
-using static System.Web.HttpUtility;
+using System.Web;
-namespace OrasProject.Oras.Remote
+namespace OrasProject.Oras.Remote;
+
+public class Registry : IRegistry
{
- public class Registry : IRegistry, IRepositoryOption
- {
+ public RepositoryOptions RepositoryOptions => _opts;
- public HttpClient HttpClient { get; set; }
- public Reference RemoteReference { get; set; }
- public bool PlainHTTP { get; set; }
- public string[] ManifestMediaTypes { get; set; }
- public int TagListPageSize { get; set; }
+ private RepositoryOptions _opts;
- public Registry(string name)
- {
- RemoteReference = new Reference(name);
- HttpClient = new HttpClient();
- HttpClient.AddUserAgent();
- }
+ public Registry(string registry) : this(registry, new HttpClient().AddUserAgent()) { }
- public Registry(string name, HttpClient httpClient)
- {
- RemoteReference = new Reference(name);
- HttpClient = httpClient;
- }
+ public Registry(string registry, HttpClient httpClient) => _opts = new()
+ {
+ Reference = new Reference(registry),
+ HttpClient = httpClient,
+ };
- ///
- /// PingAsync checks whether or not the registry implement Docker Registry API V2 or
- /// OCI Distribution Specification.
- /// Ping can be used to check authentication when an auth client is configured.
- /// References:
- /// - https://docs.docker.com/registry/spec/api/#base
- /// - https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#api
- ///
- ///
- ///
- public async Task PingAsync(CancellationToken cancellationToken)
- {
- var url = URLUtiliity.BuildRegistryBaseURL(PlainHTTP, RemoteReference);
- using var resp = await HttpClient.GetAsync(url, cancellationToken);
- switch (resp.StatusCode)
- {
- case HttpStatusCode.OK:
- return;
- case HttpStatusCode.NotFound:
- throw new NotFoundException($"Repository {RemoteReference} not found");
- default:
- throw await ErrorUtility.ParseErrorResponse(resp);
- }
- }
+ public Registry(RepositoryOptions options) => _opts = options;
- ///
- /// Repository returns a repository object for the given repository name.
- ///
- ///
- ///
- ///
- public Task GetRepository(string name, CancellationToken cancellationToken)
+ ///
+ /// PingAsync checks whether or not the registry implement Docker Registry API V2 or
+ /// OCI Distribution Specification.
+ /// Ping can be used to check authentication when an auth client is configured.
+ /// References:
+ /// - https://docs.docker.com/registry/spec/api/#base
+ /// - https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#api
+ ///
+ ///
+ ///
+ public async Task PingAsync(CancellationToken cancellationToken = default)
+ {
+ var url = new UriFactory(_opts).BuildRegistryBase();
+ using var resp = await _opts.HttpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ switch (resp.StatusCode)
{
- var reference = new Reference(RemoteReference.Registry, name);
-
- return Task.FromResult(new Repository(reference, this));
+ case HttpStatusCode.OK:
+ return;
+ case HttpStatusCode.NotFound:
+ throw new NotFoundException($"Repository {_opts.Reference} not found");
+ default:
+ throw await resp.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
}
+ }
+ ///
+ /// Repository returns a repository object for the given repository name.
+ ///
+ ///
+ ///
+ ///
+ public Task GetRepositoryAsync(string name, CancellationToken cancellationToken)
+ {
+ var reference = new Reference(_opts.Reference.Registry, name);
+ var options = _opts; // shallow copy
+ options.Reference = reference;
+ return Task.FromResult(new Repository(options));
+ }
- ///
- /// Repositories returns a list of repositories from the remote registry.
- ///
- ///
- ///
- ///
- public async IAsyncEnumerable ListRepositoriesAsync(string? last = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ ///
+ /// Repositories returns a list of repositories from the remote registry.
+ ///
+ ///
+ ///
+ ///
+ public async IAsyncEnumerable ListRepositoriesAsync(string? last = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var url = new UriFactory(_opts).BuildRegistryCatalog();
+ do
{
- var url = URLUtiliity.BuildRegistryCatalogURL(PlainHTTP, RemoteReference);
- var done = false;
- while (!done)
+ (var repositories, url) = await FetchRepositoryPageAsync(last, url!, cancellationToken).ConfigureAwait(false);
+ last = null;
+ foreach (var repository in repositories)
{
- IEnumerable repositories = Array.Empty();
- try
- {
- url = await RepositoryPageAsync(last, values => repositories = values, url, cancellationToken);
- last = "";
- }
- catch (LinkUtility.NoLinkHeaderException)
- {
- done = true;
- }
- foreach (var repository in repositories)
- {
- yield return repository;
- }
+ yield return repository;
}
- }
+ } while (url != null);
+ }
- ///
- /// RepositoryPageAsync returns a returns a single page of repositories list with the next link
- ///
- ///
- ///
- ///
- ///
- ///
- private async Task RepositoryPageAsync(string? last, Action fn, string url, CancellationToken cancellationToken)
+ ///
+ /// Returns a returns a single page of repositories list with the next link
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task<(string[], Uri?)> FetchRepositoryPageAsync(string? last, Uri url, CancellationToken cancellationToken)
+ {
+ var uriBuilder = new UriBuilder(url);
+ if (_opts.TagListPageSize > 0 || !string.IsNullOrEmpty(last))
{
- var uriBuilder = new UriBuilder(url);
- var query = ParseQueryString(uriBuilder.Query);
- if (TagListPageSize > 0 || !string.IsNullOrEmpty(last))
+ var query = HttpUtility.ParseQueryString(uriBuilder.Query);
+ if (_opts.TagListPageSize > 0)
{
- if (TagListPageSize > 0)
- {
- query["n"] = TagListPageSize.ToString();
-
-
- }
- if (!string.IsNullOrEmpty(last))
- {
- query["last"] = last;
- }
+ query["n"] = _opts.TagListPageSize.ToString();
}
-
- uriBuilder.Query = query.ToString();
- using var response = await HttpClient.GetAsync(uriBuilder.ToString(), cancellationToken);
- if (response.StatusCode != HttpStatusCode.OK)
+ if (!string.IsNullOrEmpty(last))
{
- throw await ErrorUtility.ParseErrorResponse(response);
-
+ query["last"] = last;
}
- var data = await response.Content.ReadAsStringAsync();
- var repositories = JsonSerializer.Deserialize(data);
- fn(repositories.Repositories);
- return LinkUtility.ParseLink(response);
+ uriBuilder.Query = query.ToString();
+ }
+
+ using var response = await _opts.HttpClient.GetAsync(uriBuilder.ToString(), cancellationToken).ConfigureAwait(false);
+ if (response.StatusCode != HttpStatusCode.OK)
+ {
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
}
+ var data = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ var repositories = JsonSerializer.Deserialize(data);
+ return (repositories.Repositories, response.ParseLink());
+ }
+
+ internal struct RepositoryList
+ {
+ [JsonPropertyName("repositories")]
+ public string[] Repositories { get; set; }
}
}
diff --git a/src/OrasProject.Oras/Registry/Remote/Repository.cs b/src/OrasProject.Oras/Registry/Remote/Repository.cs
index adab7dc..fd2a4f3 100644
--- a/src/OrasProject.Oras/Registry/Remote/Repository.cs
+++ b/src/OrasProject.Oras/Registry/Remote/Repository.cs
@@ -11,10 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-using OrasProject.Oras.Content;
using OrasProject.Oras.Exceptions;
using OrasProject.Oras.Oci;
-using OrasProject.Oras.Remote;
using System;
using System.Collections.Generic;
using System.IO;
@@ -23,104 +21,70 @@
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
-using static System.Web.HttpUtility;
+using System.Web;
namespace OrasProject.Oras.Registry.Remote;
///
/// Repository is an HTTP client to a remote repository
///
-public class Repository : IRepository, IRepositoryOption
+public class Repository : IRepository
{
///
- /// HttpClient is the underlying HTTP client used to access the remote registry.
+ /// Blobs provides access to the blob CAS only, which contains
+ /// layers, and other generic blobs.
///
- public HttpClient HttpClient { get; set; }
+ public IBlobStore Blobs => new BlobStore(this);
///
- /// ReferenceObj references the remote repository.
+ /// Manifests provides access to the manifest CAS only.
///
- public Reference RemoteReference { get; set; }
+ ///
+ public IManifestStore Manifests => new ManifestStore(this);
- ///
- /// PlainHTTP signals the transport to access the remote repository via HTTP
- /// instead of HTTPS.
- ///
- public bool PlainHTTP { get; set; }
+ public RepositoryOptions Options => _opts;
+ internal static readonly string[] DefaultManifestMediaTypes =
+ [
+ Docker.MediaType.Manifest,
+ Docker.MediaType.ManifestList,
+ MediaType.ImageIndex,
+ MediaType.ImageManifest
+ ];
- ///
- /// ManifestMediaTypes is used in `Accept` header for resolving manifests
- /// from references. It is also used in identifying manifests and blobs from
- /// descriptors. If an empty list is present, default manifest media types
- /// are used.
- ///
- public string[] ManifestMediaTypes { get; set; }
-
- ///
- /// TagListPageSize specifies the page size when invoking the tag list API.
- /// If zero, the page size is determined by the remote registry.
- /// Reference: https://docs.docker.com/registry/spec/api/#tags
- ///
- public int TagListPageSize { get; set; }
+ private RepositoryOptions _opts;
///
/// Creates a client to the remote repository identified by a reference
/// Example: localhost:5000/hello-world
///
///
- public Repository(string reference)
- {
- RemoteReference = Reference.Parse(reference);
- HttpClient = new HttpClient();
- HttpClient.DefaultRequestHeaders.Add("User-Agent", new string[] { "oras-dotnet" });
- }
+ public Repository(string reference) : this(reference, new HttpClient().AddUserAgent()) { }
///
/// Creates a client to the remote repository using a reference and a HttpClient
///
///
///
- public Repository(string reference, HttpClient httpClient)
+ public Repository(string reference, HttpClient httpClient) : this(new RepositoryOptions()
{
- RemoteReference = Reference.Parse(reference);
- HttpClient = httpClient;
- }
+ Reference = Reference.Parse(reference),
+ HttpClient = httpClient,
+ })
+ { }
- ///
- /// This constructor customizes the HttpClient and sets the properties
- /// using values from the parameter.
- ///
- ///
- ///
- internal Repository(Reference reference, IRepositoryOption option)
- {
- HttpClient = option.HttpClient;
- RemoteReference = reference;
- ManifestMediaTypes = option.ManifestMediaTypes;
- PlainHTTP = option.PlainHTTP;
- TagListPageSize = option.TagListPageSize;
- }
-
- ///
- /// BlobStore detects the blob store for the given descriptor.
- ///
- ///
- ///
- private IBlobStore BlobStore(Descriptor desc)
+ public Repository(RepositoryOptions options)
{
- if (ManifestUtility.IsManifest(ManifestMediaTypes, desc))
+ if (string.IsNullOrEmpty(options.Reference.Repository))
{
- return Manifests;
+ throw new InvalidReferenceException("Missing repository");
}
-
- return Blobs;
+ _opts = options;
}
-
-
///
/// FetchAsync fetches the content identified by the descriptor.
///
@@ -128,9 +92,7 @@ private IBlobStore BlobStore(Descriptor desc)
///
///
public async Task FetchAsync(Descriptor target, CancellationToken cancellationToken = default)
- {
- return await BlobStore(target).FetchAsync(target, cancellationToken);
- }
+ => await BlobStore(target).FetchAsync(target, cancellationToken).ConfigureAwait(false);
///
/// ExistsAsync returns true if the described content exists.
@@ -139,9 +101,7 @@ public async Task FetchAsync(Descriptor target, CancellationToken cancel
///
///
public async Task ExistsAsync(Descriptor target, CancellationToken cancellationToken = default)
- {
- return await BlobStore(target).ExistsAsync(target, cancellationToken);
- }
+ => await BlobStore(target).ExistsAsync(target, cancellationToken).ConfigureAwait(false);
///
/// PushAsync pushes the content, matching the expected descriptor.
@@ -151,9 +111,7 @@ public async Task ExistsAsync(Descriptor target, CancellationToken cancell
///
///
public async Task PushAsync(Descriptor expected, Stream content, CancellationToken cancellationToken = default)
- {
- await BlobStore(expected).PushAsync(expected, content, cancellationToken);
- }
+ => await BlobStore(expected).PushAsync(expected, content, cancellationToken).ConfigureAwait(false);
///
/// ResolveAsync resolves a reference to a manifest descriptor
@@ -163,9 +121,7 @@ public async Task PushAsync(Descriptor expected, Stream content, CancellationTok
///
///
public async Task ResolveAsync(string reference, CancellationToken cancellationToken = default)
- {
- return await Manifests.ResolveAsync(reference, cancellationToken);
- }
+ => await Manifests.ResolveAsync(reference, cancellationToken).ConfigureAwait(false);
///
/// TagAsync tags a manifest descriptor with a reference string.
@@ -175,9 +131,7 @@ public async Task ResolveAsync(string reference, CancellationToken c
///
///
public async Task TagAsync(Descriptor descriptor, string reference, CancellationToken cancellationToken = default)
- {
- await Manifests.TagAsync(descriptor, reference, cancellationToken);
- }
+ => await Manifests.TagAsync(descriptor, reference, cancellationToken).ConfigureAwait(false);
///
/// FetchReference fetches the manifest identified by the reference.
@@ -187,9 +141,7 @@ public async Task TagAsync(Descriptor descriptor, string reference, Cancellation
///
///
public async Task<(Descriptor Descriptor, Stream Stream)> FetchAsync(string reference, CancellationToken cancellationToken = default)
- {
- return await Manifests.FetchAsync(reference, cancellationToken);
- }
+ => await Manifests.FetchAsync(reference, cancellationToken).ConfigureAwait(false);
///
/// PushReference pushes the manifest with a reference tag.
@@ -199,11 +151,8 @@ public async Task TagAsync(Descriptor descriptor, string reference, Cancellation
///
///
///
- public async Task PushAsync(Descriptor descriptor, Stream content, string reference,
- CancellationToken cancellationToken = default)
- {
- await Manifests.PushAsync(descriptor, content, reference, cancellationToken);
- }
+ public async Task PushAsync(Descriptor descriptor, Stream content, string reference, CancellationToken cancellationToken = default)
+ => await Manifests.PushAsync(descriptor, content, reference, cancellationToken).ConfigureAwait(false);
///
/// DeleteAsync removes the content identified by the descriptor.
@@ -212,25 +161,7 @@ public async Task PushAsync(Descriptor descriptor, Stream content, string refere
///
///
public async Task DeleteAsync(Descriptor target, CancellationToken cancellationToken = default)
- {
- await BlobStore(target).DeleteAsync(target, cancellationToken);
- }
-
- ///
- /// TagsAsync returns a list of tags in a repository
- ///
- ///
- ///
- ///
- public async Task> TagsAsync(ITagListable repo, CancellationToken cancellationToken)
- {
- var tags = new List();
- await foreach (var tag in repo.ListTagsAsync().WithCancellation(cancellationToken))
- {
- tags.Add(tag);
- }
- return tags;
- }
+ => await BlobStore(target).DeleteAsync(target, cancellationToken).ConfigureAwait(false);
///
/// TagsAsync lists the tags available in the repository.
@@ -243,66 +174,60 @@ public async Task> TagsAsync(ITagListable repo, CancellationToken c
/// - https://docs.docker.com/registry/spec/api/#tags
///
///
- ///
///
///
public async IAsyncEnumerable ListTagsAsync(string? last = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
- var url = URLUtiliity.BuildRepositoryTagListURL(PlainHTTP, RemoteReference);
- var done = false;
- while (!done)
+ var url = new UriFactory(_opts).BuildRepositoryTagList();
+ do
{
- IEnumerable tags = Array.Empty();
- try
- {
- url = await TagsPageAsync(last, values => tags = values, url, cancellationToken);
- last = "";
- }
- catch (LinkUtility.NoLinkHeaderException)
- {
- done = true;
- }
+ (var tags, url) = await FetchTagsPageAsync(last, url!, cancellationToken).ConfigureAwait(false);
+ last = null;
foreach (var tag in tags)
{
yield return tag;
}
- }
+ } while (url != null);
}
///
- /// TagsPageAsync returns a single page of tag list with the next link.
+ /// Returns a single page of tag list with the next link.
///
///
///
///
///
- private async Task TagsPageAsync(string? last, Action fn, string url, CancellationToken cancellationToken)
+ private async Task<(string[], Uri?)> FetchTagsPageAsync(string? last, Uri url, CancellationToken cancellationToken)
{
var uriBuilder = new UriBuilder(url);
- var query = ParseQueryString(uriBuilder.Query);
- if (TagListPageSize > 0 || !string.IsNullOrEmpty(last))
+ if (_opts.TagListPageSize > 0 || !string.IsNullOrEmpty(last))
{
- if (TagListPageSize > 0)
+ var query = HttpUtility.ParseQueryString(uriBuilder.Query);
+ if (_opts.TagListPageSize > 0)
{
- query["n"] = TagListPageSize.ToString();
+ query["n"] = _opts.TagListPageSize.ToString();
}
if (!string.IsNullOrEmpty(last))
{
query["last"] = last;
}
+ uriBuilder.Query = query.ToString();
}
- uriBuilder.Query = query.ToString();
- using var resp = await HttpClient.GetAsync(uriBuilder.ToString(), cancellationToken);
- if (resp.StatusCode != HttpStatusCode.OK)
+ using var response = await _opts.HttpClient.GetAsync(uriBuilder.ToString(), cancellationToken).ConfigureAwait(false);
+ if (response.StatusCode != HttpStatusCode.OK)
{
- throw await ErrorUtility.ParseErrorResponse(resp);
-
+ throw await response.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
}
- var data = await resp.Content.ReadAsStringAsync();
- var tagList = JsonSerializer.Deserialize(data);
- fn(tagList.Tags);
- return LinkUtility.ParseLink(resp);
+ var data = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ var tagList = JsonSerializer.Deserialize(data);
+ return (tagList.Tags, response.ParseLink());
+ }
+
+ internal struct TagList
+ {
+ [JsonPropertyName("tags")]
+ public string[] Tags { get; set; }
}
///
@@ -314,82 +239,25 @@ private async Task TagsPageAsync(string? last, Action fn, stri
///
///
///
- ///
internal async Task DeleteAsync(Descriptor target, bool isManifest, CancellationToken cancellationToken)
{
- var remoteReference = RemoteReference;
- remoteReference.ContentReference = target.Digest;
- string url;
- if (isManifest)
- {
- url = URLUtiliity.BuildRepositoryManifestURL(PlainHTTP, remoteReference);
- }
- else
- {
- url = URLUtiliity.BuildRepositoryBlobURL(PlainHTTP, remoteReference);
- }
-
- using var resp = await HttpClient.DeleteAsync(url, cancellationToken);
+ var remoteReference = ParseReferenceFromDigest(target.Digest);
+ var uriFactory = new UriFactory(remoteReference, _opts.PlainHttp);
+ var url = isManifest ? uriFactory.BuildRepositoryManifest() : uriFactory.BuildRepositoryBlob();
+ using var resp = await _opts.HttpClient.DeleteAsync(url, cancellationToken).ConfigureAwait(false);
switch (resp.StatusCode)
{
case HttpStatusCode.Accepted:
- VerifyContentDigest(resp, target.Digest);
+ resp.VerifyContentDigest(target.Digest);
break;
case HttpStatusCode.NotFound:
- throw new NotFoundException($"digest {target.Digest} not found");
+ throw new NotFoundException($"Digest {target.Digest} not found");
default:
- throw await ErrorUtility.ParseErrorResponse(resp);
+ throw await resp.ParseErrorResponseAsync(cancellationToken).ConfigureAwait(false);
}
}
-
- ///
- /// VerifyContentDigest verifies "Docker-Content-Digest" header if present.
- /// OCI distribution-spec states the Docker-Content-Digest header is optional.
- /// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#legacy-docker-support-http-headers
- ///
- ///
- ///
- ///
- internal static void VerifyContentDigest(HttpResponseMessage resp, string expected)
- {
- if (!resp.Content.Headers.TryGetValues("Docker-Content-Digest", out var digestValues)) return;
- var digestStr = digestValues.FirstOrDefault();
- if (string.IsNullOrEmpty(digestStr))
- {
- return;
- }
-
- string contentDigest;
- try
- {
- contentDigest = Digest.Validate(digestStr);
- }
- catch (Exception)
- {
- throw new Exception($"{resp.RequestMessage.Method} {resp.RequestMessage.RequestUri}: invalid response header: `Docker-Content-Digest: {digestStr}`");
- }
- if (contentDigest != expected)
- {
- throw new Exception($"{resp.RequestMessage.Method} {resp.RequestMessage.RequestUri}: invalid response; digest mismatch in Docker-Content-Digest: received {contentDigest} when expecting {digestStr}");
- }
- }
-
-
- ///
- /// Blobs provides access to the blob CAS only, which contains
- /// layers, and other generic blobs.
- ///
- public IBlobStore Blobs => new BlobStore(this);
-
-
- ///
- /// Manifests provides access to the manifest CAS only.
- ///
- ///
- public IManifestStore Manifests => new ManifestStore(this);
-
///
/// ParseReference resolves a tag or a digest reference to a fully qualified
/// reference from a base reference Reference.
@@ -400,77 +268,68 @@ internal static void VerifyContentDigest(HttpResponseMessage resp, string expect
/// error, InvalidReferenceException.
///
///
- ///
- public Reference ParseReference(string reference)
+ internal Reference ParseReference(string reference)
{
- Reference remoteReference;
- var hasError = false;
- try
- {
- remoteReference = Reference.Parse(reference);
- }
- catch (Exception)
+ if (Reference.TryParse(reference, out var remoteReference))
{
- hasError = true;
- //reference is not a FQDN
- var index = reference.IndexOf("@");
- if (index != -1)
- {
- // `@` implies *digest*, so drop the *tag* (irrespective of what it is).
- reference = reference[(index + 1)..];
- }
- remoteReference = new Reference(RemoteReference.Registry, RemoteReference.Repository, reference);
- if (index != -1)
- {
- _ = remoteReference.Digest;
+ if (remoteReference.Registry != _opts.Reference.Registry || remoteReference.Repository != _opts.Reference.Repository)
+ {
+ throw new InvalidReferenceException(
+ $"mismatch between received {JsonSerializer.Serialize(remoteReference)} and expected {JsonSerializer.Serialize(_opts.Reference)}");
}
}
-
- if (!hasError)
+ else
{
- if (remoteReference.Registry != RemoteReference.Registry ||
- remoteReference.Repository != RemoteReference.Repository)
+ var index = reference.IndexOf("@");
+ if (index != -1)
{
- throw new InvalidReferenceException(
- $"mismatch between received {JsonSerializer.Serialize(remoteReference)} and expected {JsonSerializer.Serialize(RemoteReference)}");
+ // `@` implies *digest*, so drop the *tag* (irrespective of what it is).
+ reference = reference[(index + 1)..];
+ }
+ remoteReference = new Reference(_opts.Reference.Registry, _opts.Reference.Repository, reference);
+ if (index != -1)
+ {
+ _ = remoteReference.Digest;
}
}
if (string.IsNullOrEmpty(remoteReference.ContentReference))
{
- throw new InvalidReferenceException();
+ throw new InvalidReferenceException("Empty content reference");
}
return remoteReference;
-
}
+ internal Reference ParseReferenceFromDigest(string digest)
+ {
+ var reference = new Reference(_opts.Reference.Registry, _opts.Reference.Repository, digest);
+ _ = reference.Digest;
+ return reference;
+ }
- ///
- /// GenerateBlobDescriptor returns a descriptor generated from the response.
- ///
- ///
- ///
- ///
- ///
- public static Descriptor GenerateBlobDescriptor(HttpResponseMessage resp, string refDigest)
+ internal Reference ParseReferenceFromContentReference(string reference)
{
- var mediaType = resp.Content.Headers.ContentType.MediaType;
- if (string.IsNullOrEmpty(mediaType))
- {
- mediaType = "application/octet-stream";
- }
- var size = resp.Content.Headers.ContentLength.Value;
- if (size == -1)
+ if (string.IsNullOrEmpty(reference))
{
- throw new Exception($"{resp.RequestMessage.Method} {resp.RequestMessage.RequestUri}: unknown response Content-Length");
+ throw new InvalidReferenceException("Empty content reference");
}
+ return new Reference(_opts.Reference.Registry, _opts.Reference.Repository, reference);
+ }
- VerifyContentDigest(resp, refDigest);
+ ///
+ /// Returns the accept header for manifest media types.
+ ///
+ internal string ManifestAcceptHeader() => string.Join(',', _opts.ManifestMediaTypes ?? DefaultManifestMediaTypes);
- return new Descriptor
- {
- MediaType = mediaType,
- Digest = refDigest,
- Size = size
- };
- }
+ ///
+ /// Determines if the given descriptor is a manifest.
+ ///
+ ///
+ private bool IsManifest(Descriptor desc) => (_opts.ManifestMediaTypes ?? DefaultManifestMediaTypes).Any(mediaType => mediaType == desc.MediaType);
+
+ ///
+ /// Detects the blob store for the given descriptor.
+ ///
+ ///
+ ///
+ private IBlobStore BlobStore(Descriptor desc) => IsManifest(desc) ? Manifests : Blobs;
}
diff --git a/src/OrasProject.Oras/Registry/Remote/IRepositoryOption.cs b/src/OrasProject.Oras/Registry/Remote/RepositoryOptions.cs
similarity index 75%
rename from src/OrasProject.Oras/Registry/Remote/IRepositoryOption.cs
rename to src/OrasProject.Oras/Registry/Remote/RepositoryOptions.cs
index 29d72b0..0302ea6 100644
--- a/src/OrasProject.Oras/Registry/Remote/IRepositoryOption.cs
+++ b/src/OrasProject.Oras/Registry/Remote/RepositoryOptions.cs
@@ -11,39 +11,38 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System.Collections.Generic;
using System.Net.Http;
namespace OrasProject.Oras.Registry.Remote;
///
-/// IRepositoryOption is used to configure a remote repository.
+/// RepositoryOption is used to configure a remote repository.
///
-public interface IRepositoryOption
+public struct RepositoryOptions
{
///
/// Client is the underlying HTTP client used to access the remote registry.
///
- public HttpClient HttpClient { get; set; }
+ public required HttpClient HttpClient { get; set; }
///
/// Reference references the remote repository.
///
- public Reference RemoteReference { get; set; }
+ public required Reference Reference { get; set; }
///
- /// PlainHTTP signals the transport to access the remote repository via HTTP
+ /// PlainHttp signals the transport to access the remote repository via HTTP
/// instead of HTTPS.
///
- public bool PlainHTTP { get; set; }
-
+ public bool PlainHttp { get; set; }
///
/// ManifestMediaTypes is used in `Accept` header for resolving manifests
/// from references. It is also used in identifying manifests and blobs from
- /// descriptors. If an empty list is present, default manifest media types
- /// are used.
+ /// descriptors. If null, default manifest media types are used.
///
- public string[] ManifestMediaTypes { get; set; }
+ public IEnumerable? ManifestMediaTypes { get; set; }
///
/// TagListPageSize specifies the page size when invoking the tag list API.
@@ -51,5 +50,4 @@ public interface IRepositoryOption
/// Reference: https://docs.docker.com/registry/spec/api/#tags
///
public int TagListPageSize { get; set; }
-
}
diff --git a/src/OrasProject.Oras/Registry/Remote/ResponseTypes.cs b/src/OrasProject.Oras/Registry/Remote/ResponseTypes.cs
deleted file mode 100644
index 402751d..0000000
--- a/src/OrasProject.Oras/Registry/Remote/ResponseTypes.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright The ORAS Authors.
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System.Text.Json.Serialization;
-
-namespace OrasProject.Oras.Remote
-{
- internal static class ResponseTypes
- {
- internal struct RepositoryList
- {
- [JsonPropertyName("repositories")]
- public string[] Repositories { get; set; }
- }
-
- internal struct TagList
- {
- [JsonPropertyName("tags")]
- public string[] Tags { get; set; }
- }
- }
-}
diff --git a/src/OrasProject.Oras/Registry/Remote/URLUtiliity.cs b/src/OrasProject.Oras/Registry/Remote/URLUtiliity.cs
deleted file mode 100644
index 25882fb..0000000
--- a/src/OrasProject.Oras/Registry/Remote/URLUtiliity.cs
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright The ORAS Authors.
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using OrasProject.Oras.Registry;
-
-namespace OrasProject.Oras.Remote
-{
-
- internal static class URLUtiliity
- {
- ///
- /// BuildScheme returns HTTP scheme used to access the remote registry.
- ///
- ///
- ///
- internal static string BuildScheme(bool plainHTTP)
- {
- if (plainHTTP)
- {
- return "http";
- }
-
- return "https";
- }
-
- ///
- /// BuildRegistryBaseURL builds the URL for accessing the base API.
- /// Format: :///v2/
- /// Reference: https://docs.docker.com/registry/spec/api/#base
- ///
- ///
- ///
- ///
- internal static string BuildRegistryBaseURL(bool plainHTTP, Reference reference)
- {
- return $"{BuildScheme(plainHTTP)}://{reference.Host}/v2/";
- }
-
- ///
- /// BuildManifestURL builds the URL for accessing the catalog API.
- /// Format: :///v2/_catalog
- /// Reference: https://docs.docker.com/registry/spec/api/#catalog
- ///
- ///
- ///
- ///
- internal static string BuildRegistryCatalogURL(bool plainHTTP, Reference reference)
- {
- return $"{BuildScheme(plainHTTP)}://{reference.Host}/v2/_catalog";
- }
-
- ///
- /// BuildRepositoryBaseURL builds the base endpoint of the remote repository.
- /// Format: :///v2/
- ///
- ///
- ///
- ///
- internal static string BuildRepositoryBaseURL(bool plainHTTP, Reference reference)
- {
- return $"{BuildScheme(plainHTTP)}://{reference.Host}/v2/{reference.Repository}";
- }
-
- ///
- /// BuildRepositoryTagListURL builds the URL for accessing the tag list API.
- /// Format: :///v2//tags/list
- /// Reference: https://docs.docker.com/registry/spec/api/#tags
- ///
- ///
- ///
- ///
- internal static string BuildRepositoryTagListURL(bool plainHTTP, Reference reference)
- {
- return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/tags/list";
- }
-
- ///
- /// BuildRepositoryManifestURL builds the URL for accessing the manifest API.
- /// Format: :///v2//manifests/
- /// Reference: https://docs.docker.com/registry/spec/api/#manifest
- ///
- ///
- ///
- ///
- internal static string BuildRepositoryManifestURL(bool plainHTTP, Reference reference)
- {
- return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/manifests/{reference.ContentReference}";
- }
-
- ///
- /// BuildRepositoryBlobURL builds the URL for accessing the blob API.
- /// Format: :///v2//blobs/
- /// Reference: https://docs.docker.com/registry/spec/api/#blob
- ///
- ///
- ///
- ///
- internal static string BuildRepositoryBlobURL(bool plainHTTP, Reference reference)
- {
- return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/blobs/{reference.ContentReference}";
- }
-
- ///
- /// BuildRepositoryBlobUploadURL builds the URL for accessing the blob upload API.
- /// Format: :///v2//blobs/uploads/
- /// Reference: https://docs.docker.com/registry/spec/api/#initiate-blob-upload
-
- ///
- ///
- ///
- ///
- internal static string BuildRepositoryBlobUploadURL(bool plainHTTP, Reference reference)
- {
- return $"{BuildRepositoryBaseURL(plainHTTP, reference)}/blobs/uploads/";
- }
-
- }
-}
diff --git a/src/OrasProject.Oras/Registry/Remote/UriFactory.cs b/src/OrasProject.Oras/Registry/Remote/UriFactory.cs
new file mode 100644
index 0000000..149be0b
--- /dev/null
+++ b/src/OrasProject.Oras/Registry/Remote/UriFactory.cs
@@ -0,0 +1,122 @@
+// Copyright The ORAS Authors.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using OrasProject.Oras.Exceptions;
+using System;
+
+namespace OrasProject.Oras.Registry.Remote;
+
+internal class UriFactory : UriBuilder
+{
+ private readonly Reference _reference;
+ private readonly Uri _base;
+
+ public UriFactory(Reference reference, bool plainHttp = false)
+ {
+ _reference = reference;
+ var scheme = plainHttp ? "http" : "https";
+ _base = new Uri($"{scheme}://{_reference.Host}");
+ }
+
+ public UriFactory(RepositoryOptions options) : this(options.Reference, options.PlainHttp) { }
+
+ ///
+ /// Builds the URL for accessing the base API.
+ /// Format: :///v2/
+ /// Reference: https://docs.docker.com/registry/spec/api/#base
+ ///
+ public Uri BuildRegistryBase() => new UriBuilder(_base)
+ {
+ Path = "/v2/"
+ }.Uri;
+
+ ///
+ /// Builds the URL for accessing the catalog API.
+ /// Format: :///v2/_catalog
+ /// Reference: https://docs.docker.com/registry/spec/api/#catalog
+ ///
+ public Uri BuildRegistryCatalog() => new UriBuilder(_base)
+ {
+ Path = "/v2/_catalog"
+ }.Uri;
+
+ ///
+ /// Builds the URL for accessing the tag list API.
+ /// Format: :///v2//tags/list
+ /// Reference: https://docs.docker.com/registry/spec/api/#tags
+ ///
+ public Uri BuildRepositoryTagList()
+ {
+ var builder = NewRepositoryBaseBuilder();
+ builder.Path += "/tags/list";
+ return builder.Uri;
+ }
+
+ ///
+ /// Builds the URL for accessing the manifest API.
+ /// Format: :///v2//manifests/
+ /// Reference: https://docs.docker.com/registry/spec/api/#manifest
+ ///
+ public Uri BuildRepositoryManifest()
+ {
+ if (string.IsNullOrEmpty(_reference.Repository))
+ {
+ throw new InvalidReferenceException("missing manifest reference");
+ }
+ var builder = NewRepositoryBaseBuilder();
+ builder.Path += $"/manifests/{_reference.ContentReference}";
+ return builder.Uri;
+ }
+
+ ///
+ /// Builds the URL for accessing the blob API.
+ /// Format: :///v2//blobs/
+ /// Reference: https://docs.docker.com/registry/spec/api/#blob
+ ///
+ public Uri BuildRepositoryBlob()
+ {
+ var builder = NewRepositoryBaseBuilder();
+ builder.Path += $"/blobs/{_reference.Digest}";
+ return builder.Uri;
+ }
+
+ ///
+ /// Builds the URL for accessing the blob upload API.
+ /// Format: :///v2//blobs/uploads/
+ /// Reference: https://docs.docker.com/registry/spec/api/#initiate-blob-upload
+ ///
+ public Uri BuildRepositoryBlobUpload()
+ {
+ var builder = NewRepositoryBaseBuilder();
+ builder.Path += "/blobs/uploads/";
+ return builder.Uri;
+ }
+
+ ///
+ /// Generates a UriBuilder with the base endpoint of the remote repository.
+ /// Format: :///v2/
+ ///
+ /// Repository-scoped base UriBuilder
+ protected UriBuilder NewRepositoryBaseBuilder()
+ {
+ if (string.IsNullOrEmpty(_reference.Repository))
+ {
+ throw new InvalidReferenceException("missing repository");
+ }
+ var builder = new UriBuilder(_base)
+ {
+ Path = $"/v2/{_reference.Repository}"
+ };
+ return builder;
+ }
+}
diff --git a/tests/OrasProject.Oras.Tests/RemoteTest/AuthTest.cs b/tests/OrasProject.Oras.Tests/RemoteTest/AuthTest.cs
index 861a938..86365dc 100644
--- a/tests/OrasProject.Oras.Tests/RemoteTest/AuthTest.cs
+++ b/tests/OrasProject.Oras.Tests/RemoteTest/AuthTest.cs
@@ -13,7 +13,7 @@
using Moq;
using Moq.Protected;
-using OrasProject.Oras.Remote.Auth;
+using OrasProject.Oras.Registry.Remote.Auth;
using System.Net;
using System.Text;
using Xunit;
diff --git a/tests/OrasProject.Oras.Tests/RemoteTest/RegistryTest.cs b/tests/OrasProject.Oras.Tests/RemoteTest/RegistryTest.cs
index 75a3c05..b51586c 100644
--- a/tests/OrasProject.Oras.Tests/RemoteTest/RegistryTest.cs
+++ b/tests/OrasProject.Oras.Tests/RemoteTest/RegistryTest.cs
@@ -13,7 +13,8 @@
using Moq;
using Moq.Protected;
-using OrasProject.Oras.Remote;
+using OrasProject.Oras.Registry;
+using OrasProject.Oras.Registry.Remote;
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
@@ -23,7 +24,6 @@ namespace OrasProject.Oras.Tests.RemoteTest
{
public class RegistryTest
{
-
public static HttpClient CustomClient(Func func)
{
var moqHandler = new Mock();
@@ -35,6 +35,18 @@ public static HttpClient CustomClient(Func
+ /// Test registry constructor
+ ///
+ [Fact]
+ public void Registry()
+ {
+ var registryName = "foobar";
+ var registry = new Remote.Registry(registryName);
+ var options = registry.RepositoryOptions;
+ Assert.Equal(registryName, options.Reference.Registry);
+ }
+
///
/// PingAsync tests the PingAsync method of the Registry class.
///
@@ -65,9 +77,12 @@ public async Task PingAsync()
return res;
}
};
- var registry = new OrasProject.Oras.Remote.Registry("localhost:5000");
- registry.PlainHTTP = true;
- registry.HttpClient = CustomClient(func);
+ var registry = new Remote.Registry(new RepositoryOptions()
+ {
+ Reference = new Reference("localhost:5000"),
+ PlainHttp = true,
+ HttpClient = CustomClient(func),
+ });
var cancellationToken = new CancellationToken();
await registry.PingAsync(cancellationToken);
V2Implemented = false;
@@ -129,7 +144,7 @@ public async Task Repositories()
break;
}
- var repositoryList = new ResponseTypes.RepositoryList
+ var repositoryList = new Remote.Registry.RepositoryList
{
Repositories = repos.ToArray()
};
@@ -138,21 +153,24 @@ public async Task Repositories()
};
- var registry = new OrasProject.Oras.Remote.Registry("localhost:5000");
- registry.PlainHTTP = true;
- registry.HttpClient = CustomClient(func);
+ var registry = new Remote.Registry(new RepositoryOptions()
+ {
+ Reference = new Reference("localhost:5000"),
+ PlainHttp = true,
+ HttpClient = CustomClient(func),
+ TagListPageSize = 4,
+ });
var cancellationToken = new CancellationToken();
- registry.TagListPageSize = 4;
var wantRepositories = new List();
- foreach (var set in repoSet)
- {
- wantRepositories.AddRange(set);
+ foreach (var set in repoSet)
+ {
+ wantRepositories.AddRange(set);
}
var gotRepositories = new List();
- await foreach (var repo in registry.ListRepositoriesAsync().WithCancellation(cancellationToken))
- {
- gotRepositories.Add(repo);
+ await foreach (var repo in registry.ListRepositoriesAsync().WithCancellation(cancellationToken))
+ {
+ gotRepositories.Add(repo);
}
Assert.Equal(wantRepositories, gotRepositories);
}
diff --git a/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs b/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs
index e8e8715..5881f8f 100644
--- a/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs
+++ b/tests/OrasProject.Oras.Tests/RemoteTest/RepositoryTest.cs
@@ -18,7 +18,6 @@
using OrasProject.Oras.Oci;
using OrasProject.Oras.Registry;
using OrasProject.Oras.Registry.Remote;
-using OrasProject.Oras.Remote;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Net;
@@ -217,9 +216,12 @@ public async Task Repository_FetchAsync()
resp.StatusCode = HttpStatusCode.NotFound;
return resp;
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var stream = await repo.FetchAsync(blobDesc, cancellationToken);
var buf = new byte[stream.Length];
@@ -278,7 +280,8 @@ public async Task Repository_PushAsync()
}
- if (!req.RequestUri.Query.Contains("digest=" + blobDesc.Digest))
+ var queries = HttpUtility.ParseQueryString(req.RequestUri.Query);
+ if (queries["digest"] != blobDesc.Digest)
{
resp.StatusCode = HttpStatusCode.BadRequest;
return resp;
@@ -314,9 +317,12 @@ public async Task Repository_PushAsync()
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
await repo.PushAsync(blobDesc, new MemoryStream(blob), cancellationToken);
Assert.Equal(blob, gotBlob);
@@ -378,9 +384,12 @@ public async Task Repository_ExistsAsync()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var exists = await repo.ExistsAsync(blobDesc, cancellationToken);
Assert.True(exists);
@@ -438,9 +447,12 @@ public async Task Repository_DeleteAsync()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
await repo.DeleteAsync(blobDesc, cancellationToken);
Assert.True(blobDeleted);
@@ -503,9 +515,12 @@ public async Task Repository_ResolveAsync()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
await Assert.ThrowsAsync(async () =>
await repo.ResolveAsync(blobDesc.Digest, cancellationToken));
@@ -593,9 +608,12 @@ public async Task Repository_TagAsync()
return new HttpResponseMessage(HttpStatusCode.Forbidden);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
await Assert.ThrowsAnyAsync(
async () => await repo.TagAsync(blobDesc, reference, cancellationToken));
@@ -645,9 +663,12 @@ public async Task Repository_PushReferenceAsync()
return new HttpResponseMessage(HttpStatusCode.Forbidden);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var streamContent = new MemoryStream(index);
await repo.PushAsync(indexDesc, streamContent, reference, cancellationToken);
@@ -708,9 +729,12 @@ public async Task Repository_FetchReferenceAsyc()
return new HttpResponseMessage(HttpStatusCode.Found);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
// test with blob digest
@@ -804,7 +828,7 @@ public async Task Repository_TagsAsync()
break;
}
- var listOfTags = new ResponseTypes.TagList
+ var listOfTags = new Repository.TagList
{
Tags = tags.ToArray()
};
@@ -813,10 +837,13 @@ public async Task Repository_TagsAsync()
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
- repo.TagListPageSize = 4;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ TagListPageSize = 4,
+ });
var cancellationToken = new CancellationToken();
@@ -867,9 +894,12 @@ public async Task BlobStore_FetchAsync()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
var stream = await store.FetchAsync(blobDesc, cancellationToken);
@@ -957,9 +987,12 @@ public async Task BlobStore_FetchAsync_CanSeek()
return res;
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
var stream = await store.FetchAsync(blobDesc, cancellationToken);
@@ -1018,9 +1051,12 @@ public async Task BlobStore_FetchAsync_ZeroSizedBlob()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
var stream = await store.FetchAsync(blobDesc, cancellationToken);
@@ -1079,9 +1115,12 @@ public async Task BlobStore_PushAsync()
return new HttpResponseMessage(HttpStatusCode.Forbidden);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
await store.PushAsync(blobDesc, new MemoryStream(blob), cancellationToken);
@@ -1129,9 +1168,12 @@ public async Task BlobStore_ExistsAsync()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
var exists = await store.ExistsAsync(blobDesc, cancellationToken);
@@ -1175,9 +1217,12 @@ public async Task BlobStore_DeleteAsync()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
await store.DeleteAsync(blobDesc, cancellationToken);
@@ -1227,9 +1272,12 @@ public async Task BlobStore_ResolveAsync()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
var got = await store.ResolveAsync(blobDesc.Digest, cancellationToken);
@@ -1286,9 +1334,12 @@ public async Task BlobStore_FetchReferenceAsync()
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
@@ -1385,9 +1436,12 @@ public async Task BlobStore_FetchReferenceAsync_Seek()
return res;
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new BlobStore(repo);
@@ -1483,8 +1537,7 @@ public void GenerateBlobDescriptor_WithVariousDockerContentDigestHeaders()
var err = false;
try
{
- Repository.GenerateBlobDescriptor(resp, d);
-
+ resp.GenerateBlobDescriptor(d);
}
catch (Exception e)
{
@@ -1542,9 +1595,12 @@ public async Task ManifestStore_FetchAsync()
}
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new ManifestStore(repo);
var data = await store.FetchAsync(manifestDesc, cancellationToken);
@@ -1604,9 +1660,12 @@ public async Task ManifestStore_PushAsync()
return new HttpResponseMessage(HttpStatusCode.Forbidden);
}
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new ManifestStore(repo);
await store.PushAsync(manifestDesc, new MemoryStream(manifest), cancellationToken);
@@ -1648,9 +1707,12 @@ public async Task ManifestStore_ExistAsync()
}
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new ManifestStore(repo);
var exist = await store.ExistsAsync(manifestDesc, cancellationToken);
@@ -1709,9 +1771,12 @@ public async Task ManifestStore_DeleteAsync()
}
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new ManifestStore(repo);
await store.DeleteAsync(manifestDesc, cancellationToken);
@@ -1763,9 +1828,12 @@ public async Task ManifestStore_ResolveAsync()
}
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new ManifestStore(repo);
var got = await store.ResolveAsync(manifestDesc.Digest, cancellationToken);
@@ -1829,9 +1897,12 @@ public async Task ManifestStore_FetchReferenceAsync()
}
return new HttpResponseMessage(HttpStatusCode.NotFound);
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new ManifestStore(repo);
@@ -1938,9 +2009,12 @@ public async Task ManifestStore_TagAsync()
return res;
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new ManifestStore(repo);
@@ -1998,9 +2072,12 @@ public async Task ManifestStore_PushReferenceAsync()
res.StatusCode = HttpStatusCode.Forbidden;
return res;
};
- var repo = new Repository("localhost:5000/test");
- repo.HttpClient = CustomClient(func);
- repo.PlainHTTP = true;
+ var repo = new Repository(new RepositoryOptions()
+ {
+ Reference = Reference.Parse("localhost:5000/test"),
+ HttpClient = CustomClient(func),
+ PlainHttp = true,
+ });
var cancellationToken = new CancellationToken();
var store = new ManifestStore(repo);
await store.PushAsync(indexDesc, new MemoryStream(index), reference, cancellationToken);
@@ -2093,9 +2170,12 @@ public async Task CopyFromRepositoryToMemory()
return res;
};
- var reg = new Remote.Registry("localhost:5000");
- reg.HttpClient = CustomClient(func);
- var src = await reg.GetRepository("source", CancellationToken.None);
+ var reg = new Remote.Registry(new RepositoryOptions()
+ {
+ Reference = new Reference("localhost:5000"),
+ HttpClient = CustomClient(func),
+ });
+ var src = await reg.GetRepositoryAsync("source", CancellationToken.None);
var dst = new MemoryStore();
var tagName = "latest";
@@ -2137,7 +2217,7 @@ public async Task ManifestStore_generateDescriptorWithVariousDockerContentDigest
var err = false;
try
{
- await s.GenerateDescriptorAsync(resp, reference, method);
+ await resp.GenerateDescriptorAsync(reference, CancellationToken.None);
}
catch (Exception e)
{