Repository: google/go-github
Pull request #1381 (2020-01-16 12:54:55)

Allow DownloadReleaseAsset to follow redirects
Adds an extra parameter to DownloadReleaseAsset `followRedirects`. This will tell the client to follow one level of redirects when downloading the asset.

Solves #1378 

andersjanmyr: @gmlewis It's working now. I had to use a new `http.Client`. I also added a new integration test to verify that it's working. It's getting an asset from one of my repos `andersjanmyr/goose`. I don't know if you want this external dependency, but `go-github` doesn't contain any assets to test with.

Patch:
diff --git a/github/apps_installation.go b/github/apps_installation.go
index 09d4043f..c2e3b4ca 100644
--- a/github/apps_installation.go
+++ b/github/apps_installation.go
@@ -101,3 +101,17 @@ func (s *AppsService) RemoveRepository(ctx context.Context, instID, repoID int64
 
 	return s.client.Do(ctx, req, nil)
 }
+
+// RevokeInstallationToken revokes an installation token.
+//
+// GitHub docs: https://developer.github.com/v3/apps/installations/#revoke-an-installation-token
+func (s *AppsService) RevokeInstallationToken(ctx context.Context) (*Response, error) {
+	u := "installation/token"
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Accept", mediaTypeRevokeTokenPreview)
+
+	return s.client.Do(ctx, req, nil)
+}
diff --git a/github/github.go b/github/github.go
index b752c37b..70136105 100644
--- a/github/github.go
+++ b/github/github.go
@@ -46,6 +46,9 @@ const (
 
 	// Media Type values to access preview APIs
 
+	// https://developer.github.com/changes/2020-01-10-revoke-installation-token/
+	mediaTypeRevokeTokenPreview = "application/vnd.github.gambit-preview+json"
+
 	// https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/
 	mediaTypeStarringPreview = "application/vnd.github.v3.star+json"
 
diff --git a/github/repos_releases.go b/github/repos_releases.go
index 5c0a1cea..0a434572 100644
--- a/github/repos_releases.go
+++ b/github/repos_releases.go
@@ -266,8 +266,13 @@ func (s *RepositoriesService) GetReleaseAsset(ctx context.Context, owner, repo s
 // If a redirect is returned, the redirect URL will be returned as a string instead
 // of the io.ReadCloser. Exactly one of rc and redirectURL will be zero.
 //
+// followRedirectsClient can be passed to download the asset from a redirected
+// location. Passing http.DefaultClient is recommended unless special circumstances
+// exist, but it's possible to pass any http.Client. If nil is passed the
+// redirectURL will be returned instead.
+//
 // GitHub API docs: https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
-func (s *RepositoriesService) DownloadReleaseAsset(ctx context.Context, owner, repo string, id int64) (rc io.ReadCloser, redirectURL string, err error) {
+func (s *RepositoriesService) DownloadReleaseAsset(ctx context.Context, owner, repo string, id int64, followRedirectsClient *http.Client) (rc io.ReadCloser, redirectURL string, err error) {
 	u := fmt.Sprintf("repos/%s/%s/releases/assets/%d", owner, repo, id)
 
 	req, err := s.client.NewRequest("GET", u, nil)
@@ -293,6 +298,10 @@ func (s *RepositoriesService) DownloadReleaseAsset(ctx context.Context, owner, r
 		if !strings.Contains(err.Error(), "disable redirect") {
 			return nil, "", err
 		}
+		if followRedirectsClient != nil {
+			rc, err := s.downloadReleaseAssetFromURL(ctx, followRedirectsClient, loc)
+			return rc, "", err
+		}
 		return nil, loc, nil // Intentionally return no error with valid redirect URL.
 	}
 
@@ -304,6 +313,24 @@ func (s *RepositoriesService) DownloadReleaseAsset(ctx context.Context, owner, r
 	return resp.Body, "", nil
 }
 
+func (s *RepositoriesService) downloadReleaseAssetFromURL(ctx context.Context, followRedirectsClient *http.Client, url string) (rc io.ReadCloser, err error) {
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req = withContext(ctx, req)
+	req.Header.Set("Accept", "*/*")
+	resp, err := followRedirectsClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	if err := CheckResponse(resp); err != nil {
+		resp.Body.Close()
+		return nil, err
+	}
+	return resp.Body, nil
+}
+
 // EditReleaseAsset edits a repository release asset.
 //
 // GitHub API docs: https://developer.github.com/v3/repos/releases/#edit-a-release-asset


Test patch:
diff --git a/github/apps_installation_test.go b/github/apps_installation_test.go
index 745e1e5d..10955bf5 100644
--- a/github/apps_installation_test.go
+++ b/github/apps_installation_test.go
@@ -101,3 +101,19 @@ func TestAppsService_RemoveRepository(t *testing.T) {
 		t.Errorf("Apps.RemoveRepository returned error: %v", err)
 	}
 }
+
+func TestAppsService_RevokeInstallationToken(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/installation/token", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		testHeader(t, r, "Accept", mediaTypeRevokeTokenPreview)
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	_, err := client.Apps.RevokeInstallationToken(context.Background())
+	if err != nil {
+		t.Errorf("Apps.RevokeInstallationToken returned error: %v", err)
+	}
+}
diff --git a/github/repos_releases_test.go b/github/repos_releases_test.go
index 9e38d2d6..9ccf9ac4 100644
--- a/github/repos_releases_test.go
+++ b/github/repos_releases_test.go
@@ -252,7 +252,7 @@ func TestRepositoriesService_DownloadReleaseAsset_Stream(t *testing.T) {
 		fmt.Fprint(w, "Hello World")
 	})
 
-	reader, _, err := client.Repositories.DownloadReleaseAsset(context.Background(), "o", "r", 1)
+	reader, _, err := client.Repositories.DownloadReleaseAsset(context.Background(), "o", "r", 1, nil)
 	if err != nil {
 		t.Errorf("Repositories.DownloadReleaseAsset returned error: %v", err)
 	}
@@ -276,7 +276,7 @@ func TestRepositoriesService_DownloadReleaseAsset_Redirect(t *testing.T) {
 		http.Redirect(w, r, "/yo", http.StatusFound)
 	})
 
-	_, got, err := client.Repositories.DownloadReleaseAsset(context.Background(), "o", "r", 1)
+	_, got, err := client.Repositories.DownloadReleaseAsset(context.Background(), "o", "r", 1, nil)
 	if err != nil {
 		t.Errorf("Repositories.DownloadReleaseAsset returned error: %v", err)
 	}
@@ -286,6 +286,36 @@ func TestRepositoriesService_DownloadReleaseAsset_Redirect(t *testing.T) {
 	}
 }
 
+func TestRepositoriesService_DownloadReleaseAsset_FollowRedirect(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/releases/assets/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", defaultMediaType)
+		// /yo, below will be served as baseURLPath/yo
+		http.Redirect(w, r, baseURLPath+"/yo", http.StatusFound)
+	})
+	mux.HandleFunc("/yo", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", "*/*")
+		w.Header().Set("Content-Type", "application/octet-stream")
+		w.Header().Set("Content-Disposition", "attachment; filename=hello-world.txt")
+		fmt.Fprint(w, "Hello World")
+	})
+
+	reader, _, err := client.Repositories.DownloadReleaseAsset(context.Background(), "o", "r", 1, http.DefaultClient)
+	content, err := ioutil.ReadAll(reader)
+	if err != nil {
+		t.Errorf("Repositories.DownloadReleaseAsset returned error: %v", err)
+	}
+	reader.Close()
+	want := []byte("Hello World")
+	if !bytes.Equal(want, content) {
+		t.Errorf("Repositories.DownloadReleaseAsset returned %+v, want %+v", content, want)
+	}
+}
+
 func TestRepositoriesService_DownloadReleaseAsset_APIError(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()
@@ -297,7 +327,7 @@ func TestRepositoriesService_DownloadReleaseAsset_APIError(t *testing.T) {
 		fmt.Fprint(w, `{"message":"Not Found","documentation_url":"https://developer.github.com/v3"}`)
 	})
 
-	resp, loc, err := client.Repositories.DownloadReleaseAsset(context.Background(), "o", "r", 1)
+	resp, loc, err := client.Repositories.DownloadReleaseAsset(context.Background(), "o", "r", 1, nil)
 	if err == nil {
 		t.Error("Repositories.DownloadReleaseAsset did not return an error")
 	}
diff --git a/test/integration/repos_test.go b/test/integration/repos_test.go
index e4a09bfb..0cd2a87a 100644
--- a/test/integration/repos_test.go
+++ b/test/integration/repos_test.go
@@ -9,6 +9,8 @@ package integration
 
 import (
 	"context"
+	"io"
+	"io/ioutil"
 	"net/http"
 	"reflect"
 	"testing"
@@ -179,3 +181,19 @@ func TestRepositories_List(t *testing.T) {
 		}
 	}
 }
+
+func TestRepositories_DownloadReleaseAsset(t *testing.T) {
+	if !checkAuth("TestRepositories_DownloadReleaseAsset") {
+		return
+	}
+
+	rc, _, err := client.Repositories.DownloadReleaseAsset(context.Background(), "andersjanmyr", "goose", 484892, http.DefaultClient)
+	if err != nil {
+		t.Fatalf("Repositories.DownloadReleaseAsset(andersjanmyr, goose, 484892, true) returned error: %v", err)
+	}
+	defer func() { _ = rc.Close() }()
+	_, err = io.Copy(ioutil.Discard, rc)
+	if err != nil {
+		t.Fatalf("Repositories.DownloadReleaseAsset(andersjanmyr, goose, 484892, true) returned error: %v", err)
+	}
+}