Skip to content

Commit d4cc3af

Browse files
authored
Merge pull request #967 from chids/download-repository-archives
Add support for downloading zip and tar archives of repositories.
2 parents 453f475 + 936ab49 commit d4cc3af

22 files changed

+882
-43
lines changed

src/main/java/org/kohsuke/github/GHRepository.java

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import edu.umd.cs.findbugs.annotations.NonNull;
3131
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3232
import org.apache.commons.lang3.StringUtils;
33+
import org.kohsuke.github.function.InputStreamFunction;
3334

3435
import java.io.FileNotFoundException;
3536
import java.io.IOException;
@@ -49,21 +50,15 @@
4950
import java.util.LinkedHashSet;
5051
import java.util.List;
5152
import java.util.Map;
52-
import java.util.Objects;
5353
import java.util.Set;
5454
import java.util.TreeMap;
5555
import java.util.WeakHashMap;
5656

5757
import javax.annotation.Nonnull;
5858

5959
import static java.util.Arrays.*;
60-
import static org.kohsuke.github.internal.Previews.ANTIOPE;
61-
import static org.kohsuke.github.internal.Previews.ANT_MAN;
62-
import static org.kohsuke.github.internal.Previews.BAPTISTE;
63-
import static org.kohsuke.github.internal.Previews.FLASH;
64-
import static org.kohsuke.github.internal.Previews.INERTIA;
65-
import static org.kohsuke.github.internal.Previews.MERCY;
66-
import static org.kohsuke.github.internal.Previews.SHADOW_CAT;
60+
import static java.util.Objects.requireNonNull;
61+
import static org.kohsuke.github.internal.Previews.*;
6762

6863
/**
6964
* A repository on GitHub.
@@ -1788,7 +1783,7 @@ public InputStream readBlob(String blobSha) throws IOException {
17881783
return root.createRequest()
17891784
.withHeader("Accept", "application/vnd.github.v3.raw")
17901785
.withUrlPath(target)
1791-
.fetchStream();
1786+
.fetchStream(Requester::copyInputStream);
17921787
}
17931788

17941789
/**
@@ -2815,7 +2810,7 @@ public Reader renderMarkdown(String text, MarkdownMode mode) throws IOException
28152810
.with("mode", mode == null ? null : mode.toString())
28162811
.with("context", getFullName())
28172812
.withUrlPath("/markdown")
2818-
.fetchStream(),
2813+
.fetchStream(Requester::copyInputStream),
28192814
"UTF-8");
28202815
}
28212816

@@ -2969,6 +2964,52 @@ public GHTagObject createTag(String tag, String message, String object, String t
29692964
.wrap(this);
29702965
}
29712966

2967+
/**
2968+
* Streams a zip archive of the repository, optionally at a given <code>ref</code>.
2969+
*
2970+
* @param <T>
2971+
* the type of result
2972+
* @param streamFunction
2973+
* The {@link InputStreamFunction} that will process the stream
2974+
* @param ref
2975+
* if <code>null</code> the repository's default branch, usually <code>master</code>,
2976+
* @throws IOException
2977+
* The IO exception.
2978+
* @return the result of reading the stream.
2979+
*/
2980+
public <T> T readZip(InputStreamFunction<T> streamFunction, String ref) throws IOException {
2981+
return downloadArchive("zip", ref, streamFunction);
2982+
}
2983+
2984+
/**
2985+
* Streams a tar archive of the repository, optionally at a given <code>ref</code>.
2986+
*
2987+
* @param <T>
2988+
* the type of result
2989+
* @param streamFunction
2990+
* The {@link InputStreamFunction} that will process the stream
2991+
* @param ref
2992+
* if <code>null</code> the repository's default branch, usually <code>master</code>,
2993+
* @throws IOException
2994+
* The IO exception.
2995+
* @return the result of reading the stream.
2996+
*/
2997+
public <T> T readTar(InputStreamFunction<T> streamFunction, String ref) throws IOException {
2998+
return downloadArchive("tar", ref, streamFunction);
2999+
}
3000+
3001+
private <T> T downloadArchive(@Nonnull String type,
3002+
@CheckForNull String ref,
3003+
@Nonnull InputStreamFunction<T> streamFunction) throws IOException {
3004+
requireNonNull(streamFunction, "Sink must not be null");
3005+
String tailUrl = getApiTailUrl(type + "ball");
3006+
if (ref != null) {
3007+
tailUrl += "/" + ref;
3008+
}
3009+
final Requester builder = root.createRequest().method("GET").withUrlPath(tailUrl);
3010+
return builder.fetchStream(streamFunction);
3011+
}
3012+
29723013
/**
29733014
* Populate this object.
29743015
*
@@ -2980,7 +3021,7 @@ void populate() throws IOException {
29803021
return; // can't populate if the root is offline
29813022
}
29823023

2983-
final URL url = Objects.requireNonNull(getUrl(), "Missing instance URL!");
3024+
final URL url = requireNonNull(getUrl(), "Missing instance URL!");
29843025

29853026
try {
29863027
// IMPORTANT: the url for repository records does not reliably point to the API url.

src/main/java/org/kohsuke/github/GitHub.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1278,7 +1278,7 @@ public Reader renderMarkdown(String text) throws IOException {
12781278
.with(new ByteArrayInputStream(text.getBytes("UTF-8")))
12791279
.contentType("text/plain;charset=UTF-8")
12801280
.withUrlPath("/markdown/raw")
1281-
.fetchStream(),
1281+
.fetchStream(Requester::copyInputStream),
12821282
"UTF-8");
12831283
}
12841284

src/main/java/org/kohsuke/github/GitHubResponse.java

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.fasterxml.jackson.databind.InjectableValues;
55
import com.fasterxml.jackson.databind.JsonMappingException;
66
import org.apache.commons.io.IOUtils;
7+
import org.kohsuke.github.function.FunctionThrows;
78

89
import java.io.Closeable;
910
import java.io.IOException;
@@ -194,24 +195,11 @@ public T body() {
194195
/**
195196
* Represents a supplier of results that can throw.
196197
*
197-
* <p>
198-
* This is a <a href="package-summary.html">functional interface</a> whose functional method is
199-
* {@link #apply(ResponseInfo)}.
200-
*
201198
* @param <T>
202199
* the type of results supplied by this supplier
203200
*/
204201
@FunctionalInterface
205-
interface BodyHandler<T> {
206-
207-
/**
208-
* Gets a result.
209-
*
210-
* @return a result
211-
* @throws IOException
212-
* if an I/O Exception occurs.
213-
*/
214-
T apply(ResponseInfo input) throws IOException;
202+
interface BodyHandler<T> extends FunctionThrows<ResponseInfo, T, IOException> {
215203
}
216204

217205
/**

src/main/java/org/kohsuke/github/Requester.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
*/
2424
package org.kohsuke.github;
2525

26+
import edu.umd.cs.findbugs.annotations.NonNull;
2627
import org.apache.commons.io.IOUtils;
28+
import org.kohsuke.github.function.InputStreamFunction;
2729

2830
import java.io.ByteArrayInputStream;
2931
import java.io.IOException;
@@ -106,15 +108,31 @@ public int fetchHttpStatusCode() throws IOException {
106108
* Response input stream. There are scenarios where direct stream reading is needed, however it is better to use
107109
* {@link #fetch(Class)} where possible.
108110
*
109-
* @return the input stream
110111
* @throws IOException
111112
* the io exception
112113
*/
113-
public InputStream fetchStream() throws IOException {
114-
return client
115-
.sendRequest(this,
116-
(responseInfo) -> new ByteArrayInputStream(IOUtils.toByteArray(responseInfo.bodyStream())))
117-
.body();
114+
public <T> T fetchStream(@Nonnull InputStreamFunction<T> handler) throws IOException {
115+
return client.sendRequest(this, (responseInfo) -> handler.apply(responseInfo.bodyStream())).body();
116+
}
117+
118+
/**
119+
* Helper function to make it easy to pull streams.
120+
*
121+
* Copies an input stream to an in-memory input stream. The performance on this is not great but
122+
* {@link GitHubResponse.ResponseInfo#bodyStream()} is closed at the end of every call to
123+
* {@link GitHubClient#sendRequest(GitHubRequest, GitHubResponse.BodyHandler)}, so any reads to the original input
124+
* stream must be completed before then. There are a number of deprecated methods that return {@link InputStream}.
125+
* This method keeps all of them using the same code path.
126+
*
127+
* @param inputStream
128+
* the input stream to be copied
129+
* @return an in-memory copy of the passed input stream
130+
* @throws IOException
131+
* if an error occurs while copying the stream
132+
*/
133+
@NonNull
134+
public static InputStream copyInputStream(InputStream inputStream) throws IOException {
135+
return new ByteArrayInputStream(IOUtils.toByteArray(inputStream));
118136
}
119137

120138
/**
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.kohsuke.github.function;
2+
3+
/**
4+
* A functional interface, equivalent to {@link java.util.function.Function} but that allows throwing {@link Throwable}
5+
*
6+
* @param <T>
7+
* the type of input
8+
* @param <R>
9+
* the type of output
10+
* @param <E>
11+
* the type of error
12+
*/
13+
@FunctionalInterface
14+
public interface FunctionThrows<T, R, E extends Throwable> {
15+
/**
16+
* Apply r.
17+
*
18+
* @param input
19+
* the input
20+
* @return the r
21+
* @throws E
22+
* the e
23+
*/
24+
R apply(T input) throws E;
25+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.kohsuke.github.function;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
6+
/**
7+
* A functional interface, equivalent to {@link java.util.function.Function} but that allows throwing {@link Throwable}
8+
*
9+
* @param <R>
10+
* the type to of object to be returned
11+
*/
12+
@FunctionalInterface
13+
public interface InputStreamFunction<R> extends FunctionThrows<InputStream, R, IOException> {
14+
}

src/test/java/org/kohsuke/github/GHRepositoryTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import org.apache.commons.io.IOUtils;
55
import org.junit.Test;
66

7+
import java.io.ByteArrayInputStream;
78
import java.io.FileNotFoundException;
89
import java.io.IOException;
10+
import java.io.InputStream;
911
import java.net.URL;
1012
import java.util.ArrayList;
1113
import java.util.Date;
@@ -29,6 +31,20 @@ private GHRepository getRepository(GitHub gitHub) throws IOException {
2931
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
3032
}
3133

34+
@Test
35+
public void testZipball() throws IOException {
36+
getTempRepository().readZip((InputStream inputstream) -> {
37+
return new ByteArrayInputStream(IOUtils.toByteArray(inputstream));
38+
}, null);
39+
}
40+
41+
@Test
42+
public void testTarball() throws IOException {
43+
getTempRepository().readTar((InputStream inputstream) -> {
44+
return new ByteArrayInputStream(IOUtils.toByteArray(inputstream));
45+
}, null);
46+
}
47+
3248
@Test
3349
public void testGetters() throws IOException {
3450
GHRepository r = getTempRepository();

0 commit comments

Comments
 (0)