diff --git a/Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs b/Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs index e08c7128c6..b55afc633c 100644 --- a/Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs +++ b/Octokit.Reactive/Clients/IObservableRepositoryContentsClient.cs @@ -137,6 +137,17 @@ public interface IObservableRepositoryContentsClient /// The name of the repository IObservable GetAllContents(string owner, string name); + /// + /// Returns the raw content of the file at the given or null if the path is a directory. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The content path + IObservable GetRawContent(string owner, string name, string path); + /// /// Returns the contents of the root directory in a repository. /// @@ -156,6 +167,18 @@ public interface IObservableRepositoryContentsClient /// The content path IObservable GetAllContentsByRef(string owner, string name, string reference, string path); + /// + /// Returns the raw content of the file at the given or null if the path is a directory. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The content path + /// The name of the commit/branch/tag. + IObservable GetRawContentByRef(string owner, string name, string path, string reference); + /// /// Returns the contents of a file or directory in a repository. /// diff --git a/Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs b/Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs index f70804c824..a044e09096 100644 --- a/Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs +++ b/Octokit.Reactive/Clients/ObservableRepositoryContentsClient.cs @@ -1,5 +1,6 @@ using System; using System.Reactive; +using System.Reactive.Linq; using System.Reactive.Threading.Tasks; using Octokit.Reactive.Internal; @@ -238,6 +239,28 @@ public IObservable GetAllContents(string owner, string name) .GetAndFlattenAllPages(ApiUrls.RepositoryContent(owner, name, string.Empty)); } + /// + /// Returns the raw content of the file at the given or null if the path is a directory. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The content path + public IObservable 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); + } + /// /// Returns the contents of the root directory in a repository. /// @@ -270,6 +293,30 @@ public IObservable GetAllContentsByRef(string owner, string n return _client.Connection.GetAndFlattenAllPages(ApiUrls.RepositoryContent(owner, name, path, reference)); } + /// + /// Returns the raw content of the file at the given or null if the path is a directory. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The content path + /// The name of the commit/branch/tag. + public IObservable 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); + } + /// /// Returns the contents of a file or directory in a repository. /// diff --git a/Octokit.Tests/Clients/RepositoryContentsClientTests.cs b/Octokit.Tests/Clients/RepositoryContentsClientTests.cs index 58a04b4ec9..4f4ad4429f 100644 --- a/Octokit.Tests/Clients/RepositoryContentsClientTests.cs +++ b/Octokit.Tests/Clients/RepositoryContentsClientTests.cs @@ -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(); + 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(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(); + var client = new RepositoryContentsClient(connection); + + await Assert.ThrowsAsync(() => client.GetRawContent(null, "name", "path")); + await Assert.ThrowsAsync(() => client.GetRawContent("owner", null, "path")); + await Assert.ThrowsAsync(() => client.GetRawContent("owner", "name", null)); + + await Assert.ThrowsAsync(() => client.GetRawContent("", "name", "path")); + await Assert.ThrowsAsync(() => client.GetRawContent("owner", "", "path")); + await Assert.ThrowsAsync(() => client.GetRawContent("owner", "name", "")); + } + } + public class TheGetContentsByRefMethod { [Fact] @@ -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(); + 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(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(); + var client = new RepositoryContentsClient(connection); + + await Assert.ThrowsAsync(() => client.GetRawContentByRef(null, "name", "path", "reference")); + await Assert.ThrowsAsync(() => client.GetRawContentByRef("owner", null, "path", "reference")); + await Assert.ThrowsAsync(() => client.GetRawContentByRef("owner", "name", null, "reference")); + await Assert.ThrowsAsync(() => client.GetRawContentByRef("owner", "name", "path", null)); + + await Assert.ThrowsAsync(() => client.GetRawContentByRef("", "name", "path", "reference")); + await Assert.ThrowsAsync(() => client.GetRawContentByRef("owner", "", "path", "reference")); + await Assert.ThrowsAsync(() => client.GetRawContentByRef("owner", "name", "", "reference")); + await Assert.ThrowsAsync(() => client.GetRawContentByRef("owner", "name", "path", "")); + } + } + public class TheCreateFileMethod { [Fact] diff --git a/Octokit.Tests/Reactive/ObservableRepositoryContentsClientTests.cs b/Octokit.Tests/Reactive/ObservableRepositoryContentsClientTests.cs index 2d52c22d43..565034b493 100644 --- a/Octokit.Tests/Reactive/ObservableRepositoryContentsClientTests.cs +++ b/Octokit.Tests/Reactive/ObservableRepositoryContentsClientTests.cs @@ -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(); + var gitHubClient = new GitHubClient(connection); + var contentsClient = new ObservableRepositoryContentsClient(gitHubClient); + IApiResponse response = new ApiResponse + ( + new Response { ApiInfo = new ApiInfo(new Dictionary(), new List(), new List(), "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(u => u.ToString() == "repos/fake/repo/contents/path/to/file.txt"), null); + Assert.Same(result, rawContent); + } + + [Fact] + public void EnsuresNonNullArguments() + { + var gitHubClient = Substitute.For(); + var client = new ObservableRepositoryContentsClient(gitHubClient); + + Assert.Throws(() => client.GetRawContent(null, "name", "path")); + Assert.Throws(() => client.GetRawContent("owner", null, "path")); + Assert.Throws(() => client.GetRawContent("owner", "name", null)); + + Assert.Throws(() => client.GetRawContent("", "name", "path")); + Assert.Throws(() => client.GetRawContent("owner", "", "path")); + Assert.Throws(() => client.GetRawContent("owner", "name", "")); + } + } + public class TheGetContentsByRefMethod { [Fact] @@ -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(); + var gitHubClient = new GitHubClient(connection); + var contentsClient = new ObservableRepositoryContentsClient(gitHubClient); + IApiResponse response = new ApiResponse + ( + new Response { ApiInfo = new ApiInfo(new Dictionary(), new List(), new List(), "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(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(); + var client = new ObservableRepositoryContentsClient(gitHubClient); + + Assert.Throws(() => client.GetRawContentByRef(null, "name", "path", "reference")); + Assert.Throws(() => client.GetRawContentByRef("owner", null, "path", "reference")); + Assert.Throws(() => client.GetRawContentByRef("owner", "name", null, "reference")); + Assert.Throws(() => client.GetRawContentByRef("owner", "name", "path", null)); + + Assert.Throws(() => client.GetRawContentByRef("", "name", "path", "reference")); + Assert.Throws(() => client.GetRawContentByRef("owner", "", "path", "reference")); + Assert.Throws(() => client.GetRawContentByRef("owner", "name", "", "reference")); + Assert.Throws(() => client.GetRawContentByRef("owner", "name", "path", "")); + } + } + public class TheCreateFileMethod { [Fact] diff --git a/Octokit/Clients/IRepositoryContentsClient.cs b/Octokit/Clients/IRepositoryContentsClient.cs index 08f986b1a8..698a44277c 100644 --- a/Octokit/Clients/IRepositoryContentsClient.cs +++ b/Octokit/Clients/IRepositoryContentsClient.cs @@ -25,6 +25,17 @@ public interface IRepositoryContentsClient [ExcludeFromPaginationApiOptionsConventionTest("Pagination not supported by GitHub API (tested 29/08/2017)")] Task> GetAllContents(string owner, string name, string path); + /// + /// Returns the raw content of the file at the given or null if the path is a directory. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The content path + Task GetRawContent(string owner, string name, string path); + /// /// Returns the contents of a file or directory in a repository. /// @@ -70,6 +81,18 @@ public interface IRepositoryContentsClient [ExcludeFromPaginationApiOptionsConventionTest("Pagination not supported by GitHub API (tested 29/08/2017)")] Task> GetAllContentsByRef(string owner, string name, string path, string reference); + /// + /// Returns the raw content of the file at the given or null if the path is a directory. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The content path + /// The name of the commit/branch/tag. + Task GetRawContentByRef(string owner, string name, string path, string reference); + /// /// Returns the contents of a file or directory in a repository. /// diff --git a/Octokit/Clients/RepositoryContentsClient.cs b/Octokit/Clients/RepositoryContentsClient.cs index 8f8f6bf0bc..e5c7a6661f 100644 --- a/Octokit/Clients/RepositoryContentsClient.cs +++ b/Octokit/Clients/RepositoryContentsClient.cs @@ -41,6 +41,27 @@ public Task> GetAllContents(string owner, strin return ApiConnection.GetAll(url); } + /// + /// Returns the raw content of the file at the given or null if the path is a directory. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The content path + [ManualRoute("GET", "repos/{owner}/{repo}/contents/{path}")] + public Task 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); + } + /// /// Returns the contents of a file or directory in a repository. /// @@ -112,13 +133,32 @@ public Task> 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(url); } + /// + /// Returns the raw content of the file at the given or null if the path is a directory. + /// + /// + /// See the API documentation for more information. + /// + /// The owner of the repository + /// The name of the repository + /// The content path + /// The name of the commit/branch/tag. + [ManualRoute("GET", "repos/{owner}/{repo}/contents/{path}?ref={ref}")] + public Task 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); + } + /// /// Returns the contents of a file or directory in a repository. /// diff --git a/Octokit/Helpers/AcceptHeaders.cs b/Octokit/Helpers/AcceptHeaders.cs index 6f869f6d80..9d0848cee8 100644 --- a/Octokit/Helpers/AcceptHeaders.cs +++ b/Octokit/Helpers/AcceptHeaders.cs @@ -13,6 +13,12 @@ public static class AcceptHeaders public const string CommitReferenceSha1MediaType = "application/vnd.github.v3.sha"; + /// + /// Support for retrieving raw file content with the method. + /// + /// https://developer.github.com/v3/repos/contents/#custom-media-types + public const string RawContentMediaType = "application/vnd.github.v3.raw"; + public const string StarCreationTimestamps = "application/vnd.github.v3.star+json"; public const string MigrationsApiPreview = "application/vnd.github.wyandotte-preview+json"; diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index d10687061e..9b95f9c192 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -2363,7 +2363,7 @@ public static Uri RepositoryArchiveLink(string owner, string name, ArchiveFormat /// The for getting the contents of the specified repository and path public static Uri RepositoryContent(string owner, string name, string path, string reference) { - return "repos/{0}/{1}/contents/{2}?ref={3}".FormatUri(owner, name, path, reference); + return "repos/{0}/{1}/contents/{2}?ref={3}".FormatUri(owner, name, path == "/" ? "" : path, reference); } /// diff --git a/Octokit/Http/ApiConnection.cs b/Octokit/Http/ApiConnection.cs index aa8c514c09..dd3cc04102 100644 --- a/Octokit/Http/ApiConnection.cs +++ b/Octokit/Http/ApiConnection.cs @@ -106,6 +106,21 @@ public async Task GetHtml(Uri uri, IDictionary parameter return response.Body; } + /// + /// Gets the raw content of the API resource at the specified URI. + /// + /// URI of the API resource to get + /// Parameters to add to the API request + /// The API resource's raw content or null if the points to a directory. + /// Thrown when an API error occurs. + public async Task GetRaw(Uri uri, IDictionary parameters) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + var response = await Connection.GetRaw(uri, parameters).ConfigureAwait(false); + return response.Body; + } + /// /// Gets all API resources in the list at the specified URI. /// diff --git a/Octokit/Http/Connection.cs b/Octokit/Http/Connection.cs index 106cac7066..6c2c7560cd 100644 --- a/Octokit/Http/Connection.cs +++ b/Octokit/Http/Connection.cs @@ -211,6 +211,25 @@ public Task> GetHtml(Uri uri, IDictionary p }); } + /// + /// Performs an asynchronous HTTP GET request that expects a containing raw data. + /// + /// URI endpoint to send request to + /// Querystring parameters for the request + /// representing the received HTTP response + /// The property will be null if the points to a directory instead of a file + public Task> GetRaw(Uri uri, IDictionary parameters) + { + Ensure.ArgumentNotNull(uri, nameof(uri)); + + return GetRaw(new Request + { + Method = HttpMethod.Get, + BaseAddress = BaseAddress, + Endpoint = uri.ApplyParameters(parameters) + }); + } + public Task> Patch(Uri uri, object body) { Ensure.ArgumentNotNull(uri, nameof(uri)); @@ -620,6 +639,13 @@ async Task> GetHtml(IRequest request) return new ApiResponse(response, response.Body as string); } + async Task> GetRaw(IRequest request) + { + request.Headers.Add("Accept", AcceptHeaders.RawContentMediaType); + var response = await RunRequest(request, CancellationToken.None).ConfigureAwait(false); + return new ApiResponse(response, response.Body as byte[]); + } + async Task> Run(IRequest request, CancellationToken cancellationToken) { _jsonPipeline.SerializeRequest(request); diff --git a/Octokit/Http/HttpClientAdapter.cs b/Octokit/Http/HttpClientAdapter.cs index 8b997a50ab..2c780342f5 100644 --- a/Octokit/Http/HttpClientAdapter.cs +++ b/Octokit/Http/HttpClientAdapter.cs @@ -100,6 +100,7 @@ protected virtual async Task BuildResponse(HttpResponseMessage respon // We added support for downloading images,zip-files and application/octet-stream. // Let's constrain this appropriately. var binaryContentTypes = new[] { + AcceptHeaders.RawContentMediaType, "application/zip" , "application/x-gzip" , "application/octet-stream"}; diff --git a/Octokit/Http/IApiConnection.cs b/Octokit/Http/IApiConnection.cs index 616dbfead0..976d6c9b61 100644 --- a/Octokit/Http/IApiConnection.cs +++ b/Octokit/Http/IApiConnection.cs @@ -62,6 +62,15 @@ public interface IApiConnection /// Thrown when an API error occurs. Task GetHtml(Uri uri, IDictionary parameters); + /// + /// Gets the raw content of the API resource at the specified URI. + /// + /// URI of the API resource to get + /// Parameters to add to the API request + /// The API resource's raw content or null if the points to a directory. + /// Thrown when an API error occurs. + Task GetRaw(Uri uri, IDictionary parameters); + /// /// Gets all API resources in the list at the specified URI. /// diff --git a/Octokit/Http/IConnection.cs b/Octokit/Http/IConnection.cs index 08755cb0ee..920b871d9b 100644 --- a/Octokit/Http/IConnection.cs +++ b/Octokit/Http/IConnection.cs @@ -20,6 +20,15 @@ public interface IConnection : IApiInfoProvider /// representing the received HTTP response Task> GetHtml(Uri uri, IDictionary parameters); + /// + /// Performs an asynchronous HTTP GET request that expects a containing raw data. + /// + /// URI endpoint to send request to + /// Querystring parameters for the request + /// representing the received HTTP response + /// The property will be null if the points to a directory instead of a file + Task> GetRaw(Uri uri, IDictionary parameters); + /// /// Performs an asynchronous HTTP GET request. /// Attempts to map the response to an object of type