Skip to content

Commit

Permalink
Add support to get raw file content as byte[]
Browse files Browse the repository at this point in the history
This allows to spare a little over 100ms in my tests on small files vs using the `GetAllContentsByRef` API.

Fixes #1651
  • Loading branch information
0xced committed Mar 17, 2020
1 parent f6a9a47 commit e6955cd
Show file tree
Hide file tree
Showing 13 changed files with 354 additions and 5 deletions.
23 changes: 23 additions & 0 deletions Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ public interface IObservableRepositoryContentsClient
/// <param name="name">The name of the repository</param>
IObservable<RepositoryContent> GetAllContents(string owner, string name);

/// <summary>
/// Returns the raw content of the file at the given <paramref name="path"/> or <c>null</c> if the path is a directory.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/repos/contents/#get-contents">API documentation</a> for more information.
/// </remarks>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="path">The content path</param>
IObservable<byte[]> GetRawContent(string owner, string name, string path);

/// <summary>
/// Returns the contents of the root directory in a repository.
/// </summary>
Expand All @@ -156,6 +167,18 @@ public interface IObservableRepositoryContentsClient
/// <param name="path">The content path</param>
IObservable<RepositoryContent> GetAllContentsByRef(string owner, string name, string reference, string path);

/// <summary>
/// Returns the raw content of the file at the given <paramref name="path"/> or <c>null</c> if the path is a directory.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/repos/contents/#get-contents">API documentation</a> for more information.
/// </remarks>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="path">The content path</param>
/// <param name="reference">The name of the commit/branch/tag.</param>
IObservable<byte[]> GetRawContentByRef(string owner, string name, string path, string reference);

/// <summary>
/// Returns the contents of a file or directory in a repository.
/// </summary>
Expand Down
47 changes: 47 additions & 0 deletions Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using Octokit.Reactive.Internal;

Expand Down Expand Up @@ -238,6 +239,28 @@ public IObservable<RepositoryContent> GetAllContents(string owner, string name)
.GetAndFlattenAllPages<RepositoryContent>(ApiUrls.RepositoryContent(owner, name, string.Empty));
}

/// <summary>
/// Returns the raw content of the file at the given <paramref name="path"/> or <c>null</c> if the path is a directory.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/repos/contents/#get-contents">API documentation</a> for more information.
/// </remarks>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="path">The content path</param>
public IObservable<byte[]> GetRawContent(string owner, string name, string path)
{
Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner));
Ensure.ArgumentNotNullOrEmptyString(name, nameof(name));
Ensure.ArgumentNotNullOrEmptyString(path, nameof(path));

return _client
.Connection
.GetRaw(ApiUrls.RepositoryContent(owner, name, path), null)
.ToObservable()
.Select(e => e.Body);
}

/// <summary>
/// Returns the contents of the root directory in a repository.
/// </summary>
Expand Down Expand Up @@ -270,6 +293,30 @@ public IObservable<RepositoryContent> GetAllContentsByRef(string owner, string n
return _client.Connection.GetAndFlattenAllPages<RepositoryContent>(ApiUrls.RepositoryContent(owner, name, path, reference));
}

/// <summary>
/// Returns the raw content of the file at the given <paramref name="path"/> or <c>null</c> if the path is a directory.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/repos/contents/#get-contents">API documentation</a> for more information.
/// </remarks>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="path">The content path</param>
/// <param name="reference">The name of the commit/branch/tag.</param>
public IObservable<byte[]> GetRawContentByRef(string owner, string name, string path, string reference)
{
Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner));
Ensure.ArgumentNotNullOrEmptyString(name, nameof(name));
Ensure.ArgumentNotNullOrEmptyString(reference, nameof(reference));
Ensure.ArgumentNotNullOrEmptyString(path, nameof(path));

return _client
.Connection
.GetRaw(ApiUrls.RepositoryContent(owner, name, path, reference), null)
.ToObservable()
.Select(e => e.Body);
}

/// <summary>
/// Returns the contents of a file or directory in a repository.
/// </summary>
Expand Down
68 changes: 68 additions & 0 deletions Octokit.Tests/Clients/RepositoryContentsClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,39 @@ public async Task EnsuresNonNullArguments()
}
}

public class TheGetRawContentMethod
{
[Fact]
public async Task ReturnsRawContent()
{
var result = new byte[] {1, 2, 3};

var connection = Substitute.For<IApiConnection>();
connection.GetRaw(Args.Uri, default).Returns(result);
var contentsClient = new RepositoryContentsClient(connection);

var rawContent = await contentsClient.GetRawContent("fake", "repo", "path/to/file.txt");

connection.Received().GetRaw(Arg.Is<Uri>(u => u.ToString() == "repos/fake/repo/contents/path/to/file.txt"), null);
Assert.Same(result, rawContent);
}

[Fact]
public async Task EnsuresNonNullArguments()
{
var connection = Substitute.For<IApiConnection>();
var client = new RepositoryContentsClient(connection);

await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetRawContent(null, "name", "path"));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetRawContent("owner", null, "path"));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetRawContent("owner", "name", null));

await Assert.ThrowsAsync<ArgumentException>(() => client.GetRawContent("", "name", "path"));
await Assert.ThrowsAsync<ArgumentException>(() => client.GetRawContent("owner", "", "path"));
await Assert.ThrowsAsync<ArgumentException>(() => client.GetRawContent("owner", "name", ""));
}
}

public class TheGetContentsByRefMethod
{
[Fact]
Expand Down Expand Up @@ -311,6 +344,41 @@ public async Task EnsuresNonNullArguments()
}
}

public class TheGetRawContentByRefMethod
{
[Fact]
public async Task ReturnsRawContent()
{
var result = new byte[] {1, 2, 3};

var connection = Substitute.For<IApiConnection>();
connection.GetRaw(Args.Uri, default).Returns(result);
var contentsClient = new RepositoryContentsClient(connection);

var rawContent = await contentsClient.GetRawContentByRef("fake", "repo", "path/to/file.txt", "reference");

connection.Received().GetRaw(Arg.Is<Uri>(u => u.ToString() == "repos/fake/repo/contents/path/to/file.txt?ref=reference"), null);
Assert.Same(result, rawContent);
}

[Fact]
public async Task EnsuresNonNullArguments()
{
var connection = Substitute.For<IApiConnection>();
var client = new RepositoryContentsClient(connection);

await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetRawContentByRef(null, "name", "path", "reference"));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetRawContentByRef("owner", null, "path", "reference"));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetRawContentByRef("owner", "name", null, "reference"));
await Assert.ThrowsAsync<ArgumentNullException>(() => client.GetRawContentByRef("owner", "name", "path", null));

await Assert.ThrowsAsync<ArgumentException>(() => client.GetRawContentByRef("", "name", "path", "reference"));
await Assert.ThrowsAsync<ArgumentException>(() => client.GetRawContentByRef("owner", "", "path", "reference"));
await Assert.ThrowsAsync<ArgumentException>(() => client.GetRawContentByRef("owner", "name", "", "reference"));
await Assert.ThrowsAsync<ArgumentException>(() => client.GetRawContentByRef("owner", "name", "path", ""));
}
}

public class TheCreateFileMethod
{
[Fact]
Expand Down
80 changes: 80 additions & 0 deletions Octokit.Tests/Reactive/ObservableRepositoryContentsClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,45 @@ public void EnsuresNonNullArguments()
}
}

public class TheGetRawContentMethod
{
[Fact]
public async Task ReturnsRawContent()
{
var result = new byte[] {1, 2, 3};

var connection = Substitute.For<IConnection>();
var gitHubClient = new GitHubClient(connection);
var contentsClient = new ObservableRepositoryContentsClient(gitHubClient);
IApiResponse<byte[]> response = new ApiResponse<byte[]>
(
new Response { ApiInfo = new ApiInfo(new Dictionary<string, Uri>(), new List<string>(), new List<string>(), "etag", new RateLimit()) },
result
);
connection.GetRaw(Args.Uri, default).Returns(response);

var rawContent = await contentsClient.GetRawContent("fake", "repo", "path/to/file.txt");

connection.Received().GetRaw(Arg.Is<Uri>(u => u.ToString() == "repos/fake/repo/contents/path/to/file.txt"), null);
Assert.Same(result, rawContent);
}

[Fact]
public void EnsuresNonNullArguments()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableRepositoryContentsClient(gitHubClient);

Assert.Throws<ArgumentNullException>(() => client.GetRawContent(null, "name", "path"));
Assert.Throws<ArgumentNullException>(() => client.GetRawContent("owner", null, "path"));
Assert.Throws<ArgumentNullException>(() => client.GetRawContent("owner", "name", null));

Assert.Throws<ArgumentException>(() => client.GetRawContent("", "name", "path"));
Assert.Throws<ArgumentException>(() => client.GetRawContent("owner", "", "path"));
Assert.Throws<ArgumentException>(() => client.GetRawContent("owner", "name", ""));
}
}

public class TheGetContentsByRefMethod
{
[Fact]
Expand Down Expand Up @@ -396,6 +435,47 @@ public void EnsuresNonNullArguments()
}
}

public class TheGetRawContentByRefMethod
{
[Fact]
public async Task ReturnsRawContent()
{
var result = new byte[] {1, 2, 3};

var connection = Substitute.For<IConnection>();
var gitHubClient = new GitHubClient(connection);
var contentsClient = new ObservableRepositoryContentsClient(gitHubClient);
IApiResponse<byte[]> response = new ApiResponse<byte[]>
(
new Response { ApiInfo = new ApiInfo(new Dictionary<string, Uri>(), new List<string>(), new List<string>(), "etag", new RateLimit()) },
result
);
connection.GetRaw(Args.Uri, default).Returns(response);

var rawContent = await contentsClient.GetRawContentByRef("fake", "repo", "path/to/file.txt", "reference");

connection.Received().GetRaw(Arg.Is<Uri>(u => u.ToString() == "repos/fake/repo/contents/path/to/file.txt?ref=reference"), null);
Assert.Same(result, rawContent);
}

[Fact]
public void EnsuresNonNullArguments()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableRepositoryContentsClient(gitHubClient);

Assert.Throws<ArgumentNullException>(() => client.GetRawContentByRef(null, "name", "path", "reference"));
Assert.Throws<ArgumentNullException>(() => client.GetRawContentByRef("owner", null, "path", "reference"));
Assert.Throws<ArgumentNullException>(() => client.GetRawContentByRef("owner", "name", null, "reference"));
Assert.Throws<ArgumentNullException>(() => client.GetRawContentByRef("owner", "name", "path", null));

Assert.Throws<ArgumentException>(() => client.GetRawContentByRef("", "name", "path", "reference"));
Assert.Throws<ArgumentException>(() => client.GetRawContentByRef("owner", "", "path", "reference"));
Assert.Throws<ArgumentException>(() => client.GetRawContentByRef("owner", "name", "", "reference"));
Assert.Throws<ArgumentException>(() => client.GetRawContentByRef("owner", "name", "path", ""));
}
}

public class TheCreateFileMethod
{
[Fact]
Expand Down
25 changes: 25 additions & 0 deletions Octokit/Clients/IRepositoryContentsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ public interface IRepositoryContentsClient
[ExcludeFromPaginationApiOptionsConventionTest("Pagination not supported by GitHub API (tested 29/08/2017)")]
Task<IReadOnlyList<RepositoryContent>> GetAllContents(string owner, string name, string path);

/// <summary>
/// Returns the raw content of the file at the given <paramref name="path"/> or <c>null</c> if the path is a directory.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/repos/contents/#get-contents">API documentation</a> for more information.
/// </remarks>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="path">The content path</param>
[ExcludeFromPaginationApiOptionsConventionTest("Pagination not supported by GitHub API (tested 29/08/2017)")]
Task<byte[]> GetRawContent(string owner, string name, string path);

/// <summary>
/// Returns the contents of a file or directory in a repository.
/// </summary>
Expand Down Expand Up @@ -70,6 +82,19 @@ public interface IRepositoryContentsClient
[ExcludeFromPaginationApiOptionsConventionTest("Pagination not supported by GitHub API (tested 29/08/2017)")]
Task<IReadOnlyList<RepositoryContent>> GetAllContentsByRef(string owner, string name, string path, string reference);

/// <summary>
/// Returns the raw content of the file at the given <paramref name="path"/> or <c>null</c> if the path is a directory.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/repos/contents/#get-contents">API documentation</a> for more information.
/// </remarks>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="path">The content path</param>
/// <param name="reference">The name of the commit/branch/tag.</param>
[ExcludeFromPaginationApiOptionsConventionTest("Pagination not supported by GitHub API (tested 29/08/2017)")]
Task<byte[]> GetRawContentByRef(string owner, string name, string path, string reference);

/// <summary>
/// Returns the contents of a file or directory in a repository.
/// </summary>
Expand Down
48 changes: 44 additions & 4 deletions Octokit/Clients/RepositoryContentsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ public Task<IReadOnlyList<RepositoryContent>> GetAllContents(string owner, strin
return ApiConnection.GetAll<RepositoryContent>(url);
}

/// <summary>
/// Returns the raw content of the file at the given <paramref name="path"/> or <c>null</c> if the path is a directory.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/repos/contents/#get-contents">API documentation</a> for more information.
/// </remarks>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="path">The content path</param>
[ManualRoute("GET", "repos/{owner}/{repo}/contents/{path}")]
public Task<byte[]> GetRawContent(string owner, string name, string path)
{
Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner));
Ensure.ArgumentNotNullOrEmptyString(name, nameof(name));
Ensure.ArgumentNotNullOrEmptyString(path, nameof(path));

var url = ApiUrls.RepositoryContent(owner, name, path);

return ApiConnection.GetRaw(url, null);
}

/// <summary>
/// Returns the contents of a file or directory in a repository.
/// </summary>
Expand Down Expand Up @@ -112,13 +133,32 @@ public Task<IReadOnlyList<RepositoryContent>> GetAllContentsByRef(string owner,
Ensure.ArgumentNotNullOrEmptyString(path, nameof(path));
Ensure.ArgumentNotNullOrEmptyString(reference, nameof(reference));

var url = (path == "/")
? ApiUrls.RepositoryContent(owner, name, "", reference)
: ApiUrls.RepositoryContent(owner, name, path, reference);

var url = ApiUrls.RepositoryContent(owner, name, path, reference);
return ApiConnection.GetAll<RepositoryContent>(url);
}

/// <summary>
/// Returns the raw content of the file at the given <paramref name="path"/> or <c>null</c> if the path is a directory.
/// </summary>
/// <remarks>
/// See the <a href="https://developer.github.com/v3/repos/contents/#get-contents">API documentation</a> for more information.
/// </remarks>
/// <param name="owner">The owner of the repository</param>
/// <param name="name">The name of the repository</param>
/// <param name="path">The content path</param>
/// <param name="reference">The name of the commit/branch/tag.</param>
[ManualRoute("GET", "repos/{owner}/{repo}/contents/{path}?ref={ref}")]
public Task<byte[]> GetRawContentByRef(string owner, string name, string path, string reference)
{
Ensure.ArgumentNotNullOrEmptyString(owner, nameof(owner));
Ensure.ArgumentNotNullOrEmptyString(name, nameof(name));
Ensure.ArgumentNotNullOrEmptyString(path, nameof(path));
Ensure.ArgumentNotNullOrEmptyString(reference, nameof(reference));

var url = ApiUrls.RepositoryContent(owner, name, path, reference);
return ApiConnection.GetRaw(url, null);
}

/// <summary>
/// Returns the contents of a file or directory in a repository.
/// </summary>
Expand Down
Loading

0 comments on commit e6955cd

Please sign in to comment.