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

Add support for downloading zip and tar archives of repositories. #967

Merged
merged 10 commits into from
Feb 26, 2021
56 changes: 45 additions & 11 deletions src/main/java/org/kohsuke/github/GHRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.lang3.StringUtils;
import org.kohsuke.github.function.InputStreamConsumer;

import java.io.FileNotFoundException;
import java.io.IOException;
Expand All @@ -49,21 +50,15 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.WeakHashMap;

import javax.annotation.Nonnull;

import static java.util.Arrays.*;
import static org.kohsuke.github.internal.Previews.ANTIOPE;
import static org.kohsuke.github.internal.Previews.ANT_MAN;
import static org.kohsuke.github.internal.Previews.BAPTISTE;
import static org.kohsuke.github.internal.Previews.FLASH;
import static org.kohsuke.github.internal.Previews.INERTIA;
import static org.kohsuke.github.internal.Previews.MERCY;
import static org.kohsuke.github.internal.Previews.SHADOW_CAT;
import static java.util.Objects.requireNonNull;
import static org.kohsuke.github.internal.Previews.*;

/**
* A repository on GitHub.
Expand Down Expand Up @@ -1788,7 +1783,7 @@ public InputStream readBlob(String blobSha) throws IOException {
return root.createRequest()
.withHeader("Accept", "application/vnd.github.v3.raw")
.withUrlPath(target)
.fetchStream();
.fetchStream(Requester::copyInputStream);
}

/**
Expand Down Expand Up @@ -2815,7 +2810,7 @@ public Reader renderMarkdown(String text, MarkdownMode mode) throws IOException
.with("mode", mode == null ? null : mode.toString())
.with("context", getFullName())
.withUrlPath("/markdown")
.fetchStream(),
.fetchStream(Requester::copyInputStream),
"UTF-8");
}

Expand Down Expand Up @@ -2969,6 +2964,45 @@ public GHTagObject createTag(String tag, String message, String object, String t
.wrap(this);
}

/**
* Streams a zip archive of the repository, optionally at a given <code>ref</code>.
*
* @param sink
* The {@link InputStreamConsumer} that will consume the stream
* @param ref
* if <code>null</code> the repository's default branch, usually <code>master</code>,
* @throws IOException
* The IO exception.
*/
public void readZip(InputStreamConsumer sink, String ref) throws IOException {
downloadArchive("zip", ref, sink);
}

/**
* Streams a tar archive of the repository, optionally at a given <code>ref</code>.
*
* @param sink
* The {@link InputStreamConsumer} that will consume the stream
* @param ref
* if <code>null</code> the repository's default branch, usually <code>master</code>,
* @throws IOException
* The IO exception.
*/
public void readTar(InputStreamConsumer sink, String ref) throws IOException {
downloadArchive("tar", ref, sink);
}

private void downloadArchive(@Nonnull String type, @CheckForNull String ref, @Nonnull InputStreamConsumer sink)
throws IOException {
requireNonNull(sink, "Sink must not be null");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use spotbugs annotation for this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the annotation doesn't actually enforce it at run time though as far as I know?
they provide hints for IDE and static analysis tools

String tailUrl = getApiTailUrl(type + "ball");
if (ref != null) {
tailUrl += "/" + ref;
}
final Requester builder = root.createRequest().method("GET").withUrlPath(tailUrl);
builder.fetchStream(sink);
}

/**
* Populate this object.
*
Expand All @@ -2980,7 +3014,7 @@ void populate() throws IOException {
return; // can't populate if the root is offline
}

final URL url = Objects.requireNonNull(getUrl(), "Missing instance URL!");
final URL url = requireNonNull(getUrl(), "Missing instance URL!");

try {
// IMPORTANT: the url for repository records does not reliably point to the API url.
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/kohsuke/github/GitHub.java
Original file line number Diff line number Diff line change
Expand Up @@ -1278,7 +1278,7 @@ public Reader renderMarkdown(String text) throws IOException {
.with(new ByteArrayInputStream(text.getBytes("UTF-8")))
.contentType("text/plain;charset=UTF-8")
.withUrlPath("/markdown/raw")
.fetchStream(),
.fetchStream(Requester::copyInputStream),
"UTF-8");
}

Expand Down
16 changes: 2 additions & 14 deletions src/main/java/org/kohsuke/github/GitHubResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.function.FunctionThrows;

import java.io.Closeable;
import java.io.IOException;
Expand Down Expand Up @@ -194,24 +195,11 @@ public T body() {
/**
* Represents a supplier of results that can throw.
*
* <p>
* This is a <a href="package-summary.html">functional interface</a> whose functional method is
* {@link #apply(ResponseInfo)}.
*
* @param <T>
* the type of results supplied by this supplier
*/
@FunctionalInterface
interface BodyHandler<T> {

/**
* Gets a result.
*
* @return a result
* @throws IOException
* if an I/O Exception occurs.
*/
T apply(ResponseInfo input) throws IOException;
interface BodyHandler<T> extends FunctionThrows<ResponseInfo, T, IOException> {
}

/**
Expand Down
45 changes: 39 additions & 6 deletions src/main/java/org/kohsuke/github/Requester.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
*/
package org.kohsuke.github;

import edu.umd.cs.findbugs.annotations.NonNull;
import org.apache.commons.io.IOUtils;
import org.kohsuke.github.function.InputStreamConsumer;
import org.kohsuke.github.function.InputStreamFunction;

import java.io.ByteArrayInputStream;
import java.io.IOException;
Expand Down Expand Up @@ -106,15 +109,45 @@ public int fetchHttpStatusCode() throws IOException {
* Response input stream. There are scenarios where direct stream reading is needed, however it is better to use
* {@link #fetch(Class)} where possible.
*
* @return the input stream
* @throws IOException
* the io exception
*/
public InputStream fetchStream() throws IOException {
return client
.sendRequest(this,
(responseInfo) -> new ByteArrayInputStream(IOUtils.toByteArray(responseInfo.bodyStream())))
.body();
public void fetchStream(@Nonnull InputStreamConsumer consumer) throws IOException {
fetchStream((inputStream) -> {
consumer.accept(inputStream);
return null;
});
}

/**
* Response input stream. There are scenarios where direct stream reading is needed, however it is better to use
* {@link #fetch(Class)} where possible.
*
* @throws IOException
* the io exception
*/
public <T> T fetchStream(@Nonnull InputStreamFunction<T> handler) throws IOException {
return client.sendRequest(this, (responseInfo) -> handler.apply(responseInfo.bodyStream())).body();
}

/**
* Helper function to make it easy to pull streams.
*
* Copies an input stream to an in-memory input stream. The performance on this is not great but
* {@link GitHubResponse.ResponseInfo#bodyStream()} is closed at the end of every call to
* {@link GitHubClient#sendRequest(GitHubRequest, GitHubResponse.BodyHandler)}, so any reads to the original input
* stream must be completed before then. There are a number of deprecated methods that return {@link InputStream}.
* This method keeps all of them using the same code path.
*
* @param inputStream
* the input stream to be copied
* @return an in-memory copy of the passed input stream
* @throws IOException
* if an error occurs while copying the stream
*/
@NonNull
public static InputStream copyInputStream(InputStream inputStream) throws IOException {
return new ByteArrayInputStream(IOUtils.toByteArray(inputStream));
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/kohsuke/github/function/ConsumerThrows.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.kohsuke.github.function;

/**
* A functional interface, equivalent to {@link java.util.function.Consumer} but that allows throwing {@link Throwable}
*/
@FunctionalInterface
public interface ConsumerThrows<T, E extends Throwable> {
void accept(T input) throws E;
}
9 changes: 9 additions & 0 deletions src/main/java/org/kohsuke/github/function/FunctionThrows.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.kohsuke.github.function;

/**
* A functional interface, equivalent to {@link java.util.function.Function} but that allows throwing {@link Throwable}
*/
@FunctionalInterface
public interface FunctionThrows<T, R, E extends Throwable> {
R apply(T input) throws E;
}
12 changes: 12 additions & 0 deletions src/main/java/org/kohsuke/github/function/InputStreamConsumer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.kohsuke.github.function;

import java.io.IOException;
import java.io.InputStream;

/**
* A functional interface, equivalent to {@link java.util.function.Consumer} but that takes an {@link InputStream} and
* can throw an {@link IOException}
*/
@FunctionalInterface
public interface InputStreamConsumer extends ConsumerThrows<InputStream, IOException> {
}
12 changes: 12 additions & 0 deletions src/main/java/org/kohsuke/github/function/InputStreamFunction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.kohsuke.github.function;

import java.io.IOException;
import java.io.InputStream;

/**
* A functional interface, equivalent to {@link java.util.function.Function} but that allows throwing {@link Throwable}
*
*/
@FunctionalInterface
public interface InputStreamFunction<R> extends FunctionThrows<InputStream, R, IOException> {
}
16 changes: 16 additions & 0 deletions src/test/java/org/kohsuke/github/GHRepositoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import org.apache.commons.io.IOUtils;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
Expand All @@ -29,6 +31,20 @@ private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization("hub4j-test-org").getRepository("github-api");
}

@Test
public void testZipball() throws IOException {
getTempRepository().readZip((InputStream inputstream) -> {
InputStream i = new ByteArrayInputStream(IOUtils.toByteArray(inputstream));
}, null);
}

@Test
public void testTarball() throws IOException {
getTempRepository().readTar((InputStream inputstream) -> {
InputStream i = new ByteArrayInputStream(IOUtils.toByteArray(inputstream));
}, null);
}

@Test
public void testGetters() throws IOException {
GHRepository r = getTempRepository();
Expand Down
Loading