Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Support Copy #16

Merged
merged 11 commits into from
Apr 28, 2023
Merged
25 changes: 25 additions & 0 deletions Oras.Tests/ContentTest/ContentTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static Oras.Content.Content;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace Oras.Tests.ContentTest
{
public class ContentTest
{
/// <summary>
/// This method tests if the digest is calculated properly
/// </summary>
[Fact]
public void Content_EnsureThatTheDigestIsCalculatedProperly()
sammychinedu2ky marked this conversation as resolved.
Show resolved Hide resolved
{
var helloWorldDigest = "sha256:11d4ddc357e0822968dbfd226b6e1c2aac018d076a54da4f65e1dc8180684ac3";
var content = Encoding.UTF8.GetBytes("helloWorld");
var calculateHelloWorldDigest = CalculateDigest(content);
Assert.Equal(helloWorldDigest,calculateHelloWorldDigest);
}
}
}
125 changes: 125 additions & 0 deletions Oras.Tests/CopyTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Oras.Constants;
using Oras.Memory;
using Oras.Models;
using System.Text;
using System.Text.Json;
using Xunit;
using static Oras.Content.Content;
using Index = Oras.Models.Index;

namespace Oras.Tests
{
public class CopyTest
{
/// <summary>
/// Can copy a rooted directed acyclic graph (DAG) with the tagged root node
/// in the source Memory Target to the destination Memory Target.
/// </summary>
/// <returns></returns>
[Fact]
public async Task Copy_CanCopyBetweenMemoryTargetsWithTaggedNode()
{
var sourceTarget = new MemoryTarget();
var cancellationToken = new CancellationToken();
var blobs = new List<byte[]>();
var descs = new List<Descriptor>();
var appendBlob = (string mediaType, byte[] blob) =>
{
blobs.Add(blob);
var desc = new Descriptor
{
MediaType = mediaType,
Digest = CalculateDigest(blob),
Size = blob.Length
};
descs.Add(desc);
};
var generateManifest = (Descriptor config, List<Descriptor> layers) =>
{
var manifest = new Manifest
{
Config = config,
Layers = layers
};
var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest));
appendBlob(OCIMediaTypes.ImageManifest, manifestBytes);
};
var getBytes = (string data) => Encoding.UTF8.GetBytes(data);
appendBlob(OCIMediaTypes.ImageConfig, getBytes("config")); // blob 0
appendBlob(OCIMediaTypes.ImageLayer, getBytes("foo")); // blob 1
appendBlob(OCIMediaTypes.ImageLayer, getBytes("bar")); // blob 2
generateManifest(descs[0], descs.GetRange(1, 2)); // blob 3

for (var i = 0; i < blobs.Count; i++)
{
await sourceTarget.PushAsync(descs[i], new MemoryStream(blobs[i]), cancellationToken);

}

var root = descs[3];
var reference = "foobar";
await sourceTarget.TagAsync(root, reference, cancellationToken);
var destinationTarget = new MemoryTarget();
var gotDesc = await Copy.CopyAsync(sourceTarget, reference, destinationTarget, "", cancellationToken);
Assert.Equal(gotDesc, root);
sammychinedu2ky marked this conversation as resolved.
Show resolved Hide resolved

foreach (var des in descs)
{
Assert.True(await destinationTarget.ExistsAsync(des, cancellationToken));
}
}
sammychinedu2ky marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Can copy a rooted directed acyclic graph (DAG) from the source Memory Target to the destination Memory Target.
/// </summary>
/// <returns></returns>
[Fact]
public async Task Copy_CanCopyBetweenMemoryTargets()
{
var sourceTarget = new MemoryTarget();
var cancellationToken = new CancellationToken();
var blobs = new List<byte[]>();
var descs = new List<Descriptor>();
var appendBlob = (string mediaType, byte[] blob) =>
{
blobs.Add(blob);
var desc = new Descriptor
{
MediaType = mediaType,
Digest = CalculateDigest(blob),
Size = blob.Length
};
descs.Add(desc);
};
var generateManifest = (Descriptor config, List<Descriptor> layers) =>
{
var manifest = new Manifest
{
Config = config,
Layers = layers
};
var manifestBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(manifest));
appendBlob(OCIMediaTypes.ImageManifest, manifestBytes);
};
var getBytes = (string data) => Encoding.UTF8.GetBytes(data);
appendBlob(OCIMediaTypes.ImageConfig, getBytes("config")); // blob 0
appendBlob(OCIMediaTypes.ImageLayer, getBytes("foo")); // blob 1
appendBlob(OCIMediaTypes.ImageLayer, getBytes("bar")); // blob 2
generateManifest(descs[0], descs.GetRange(1, 2)); // blob 3

for (var i = 0; i < blobs.Count; i++)
{
await sourceTarget.PushAsync(descs[i], new MemoryStream(blobs[i]), cancellationToken);

}
var root = descs[3];
var destinationTarget = new MemoryTarget();
await Copy.CopyGraphAsync(sourceTarget, destinationTarget, root, cancellationToken);

foreach (var des in descs)
{
Assert.True(await destinationTarget.ExistsAsync(des, cancellationToken));
}
}
}
}
10 changes: 5 additions & 5 deletions Oras.Tests/MemoryTest/MemoryTargetTest.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using Oras.Constants;
using static Oras.Content.Content;
using Oras.Exceptions;
using Oras.Memory;
using Oras.Models;
using System.Text;
using System.Text.Json;
using Xunit;
using static Oras.Content.Content;
using Index = Oras.Models.Index;

namespace Oras.Tests.MemoryTest
Expand Down Expand Up @@ -55,6 +55,7 @@ public async Task MemoryTarget_CanStoreData()
public async Task MemoryTarget_ThrowsNotFoundExceptionWhenDataIsNotAvailable()
{
var content = Encoding.UTF8.GetBytes("Hello World");

string hash = CalculateDigest(content);
var descriptor = new Descriptor
{
Expand All @@ -68,9 +69,9 @@ public async Task MemoryTarget_ThrowsNotFoundExceptionWhenDataIsNotAvailable()
var contentExists = await memoryTarget.ExistsAsync(descriptor, cancellationToken);
Assert.False(contentExists);
await Assert.ThrowsAsync<NotFoundException>(async () =>
{
await memoryTarget.FetchAsync(descriptor, cancellationToken);
});
{
await memoryTarget.FetchAsync(descriptor, cancellationToken);
});
}

/// <summary>
Expand Down Expand Up @@ -202,7 +203,6 @@ public async Task MemoryTarget_ShouldReturnPredecessorsOfNodes()
generateIndex(descs.GetRange(4, 2)); // blob 7
generateIndex(new() { descs[6] }); // blob 8


for (var i = 0; i < blobs.Count; i++)
{
await memoryTarget.PushAsync(descs[i], new MemoryStream(blobs[i]), cancellationToken);
Expand Down
45 changes: 37 additions & 8 deletions Oras/Content/Content.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
using Oras.Constants;
using Oras.Exceptions;
using Oras.Interfaces;
using Oras.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Index = Oras.Models.Index;
using Oras.Exceptions;
using System.IO;
using System.Security.Cryptography;

namespace Oras.Content
{
public static class Content
{
/// <summary>
/// Retrieves the successors of a node
/// </summary>
/// <param name="fetcher"></param>
/// <param name="node"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<IList<Descriptor>> SuccessorsAsync(IFetcher fetcher, Descriptor node, CancellationToken cancellationToken)
{
switch (node.MediaType)
Expand All @@ -41,21 +47,44 @@ public static async Task<IList<Descriptor>> SuccessorsAsync(IFetcher fetcher, De
return default;
}

/// <summary>
/// Fetches all the content for a given descriptor.
/// Currently only sha256 is supported but we would supports others hash algorithms in the future.
/// </summary>
/// <param name="fetcher"></param>
/// <param name="desc"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<Byte[]> FetchAllAsync(IFetcher fetcher, Descriptor desc, CancellationToken cancellationToken)
{
var stream = await fetcher.FetchAsync(desc, cancellationToken);
return await ReadAllAsync(stream, desc);
}

public static string CalculateDigest(byte[] content)
/// <summary>
/// Calculates the digest of the content
/// Currently only sha256 is supported but we would supports others hash algorithms in the future.
/// </summary>
/// <param name="content"></param>
/// <returns></returns>
internal static string CalculateDigest(byte[] content)
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(content);
var output = $"{nameof(SHA256)}:{BitConverter.ToString(hash).Replace("-", "")}";
return output;
return output.ToLower();
}

public static async Task<byte[]> ReadAllAsync(Stream stream, Descriptor descriptor)
/// <summary>
/// Reads and verifies the content from a stream
/// </summary>
/// <param name="stream"></param>
/// <param name="descriptor"></param>
/// <returns></returns>
/// <exception cref="InvalidDescriptorSizeException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="MismatchedDigestException"></exception>
internal static async Task<byte[]> ReadAllAsync(Stream stream, Descriptor descriptor)
{
if (descriptor.Size < 0)
{
Expand Down
68 changes: 68 additions & 0 deletions Oras/Copy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Oras.Interfaces;
using Oras.Models;
using System;
using System.Threading;
using System.Threading.Tasks;
using static Oras.Content.Content;

namespace Oras
{
public class Copy
{

/// <summary>
/// Copy copies a rooted directed acyclic graph (DAG) with the tagged root node
/// in the source Target to the destination Target.
/// The destination reference will be the same as the source reference if the
/// destination reference is left blank.
/// Returns the descriptor of the root node on successful copy.
/// </summary>
/// <param name="src"></param>
/// <param name="srcRef"></param>
/// <param name="dst"></param>
/// <param name="dstRef"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<Descriptor> CopyAsync(ITarget src, string srcRef, ITarget dst, string dstRef, CancellationToken cancellationToken)
{
if (src is null)
{
throw new Exception("null source target");
}
if (dst is null)
{
throw new Exception("null destination target");
}
if (dstRef == string.Empty)
{
dstRef = srcRef;
}
var root = await src.ResolveAsync(srcRef, cancellationToken);
await CopyGraphAsync(src, dst, root, cancellationToken);
await dst.TagAsync(root, dstRef, cancellationToken);
return root;
}

public static async Task CopyGraphAsync(ITarget src, ITarget dst, Descriptor node, CancellationToken cancellationToken)
{
// check if node exists in target
if (!await dst.ExistsAsync(node, cancellationToken))
{
// retrieve successors
var successors = await SuccessorsAsync(src, node, cancellationToken);
// obtain data stream
var dataStream = await src.FetchAsync(node, cancellationToken);
// check if the node has successors
if (successors != null)
{
foreach (var childNode in successors)
{
await CopyGraphAsync(src, dst, childNode, cancellationToken);
}
}
await dst.PushAsync(node, dataStream, cancellationToken);
}
}
}
}
4 changes: 2 additions & 2 deletions Oras/Memory/MemoryGraph.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using static Oras.Content.Content;
using Oras.Interfaces;
using Oras.Interfaces;
using Oras.Models;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using static Oras.Content.Content;

namespace Oras.Memory
{
Expand Down
4 changes: 2 additions & 2 deletions Oras/Memory/MemoryStorage.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using static Oras.Content.Content;
using Oras.Exceptions;
using Oras.Exceptions;
using Oras.Interfaces;
using Oras.Models;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using static Oras.Content.Content;

namespace Oras.Memory
{
Expand Down
18 changes: 11 additions & 7 deletions Oras/Oras.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="7.0.2" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Oras.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="7.0.2" />
</ItemGroup>
</Project>