Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pkg/cmd/run/download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type DownloadOptions struct {
type platform interface {
List(runID string) ([]shared.Artifact, error)
Download(url string, dir safepaths.Absolute) error
DownloadWithName(url string, name string, dir safepaths.Absolute) error
}

type iprompter interface {
Expand Down Expand Up @@ -187,7 +188,7 @@ func runDownload(opts *DownloadOptions) error {
}
}

err := opts.Platform.Download(a.DownloadURL, destDir)
err := opts.Platform.DownloadWithName(a.DownloadURL, a.Name, destDir)
if err != nil {
return fmt.Errorf("error downloading %s: %w", a.Name, err)
}
Expand Down
70 changes: 48 additions & 22 deletions pkg/cmd/run/download/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"net/http"
"os"
"strings"

"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
Expand All @@ -24,16 +25,18 @@ func (p *apiPlatform) List(runID string) ([]shared.Artifact, error) {
}

func (p *apiPlatform) Download(url string, dir safepaths.Absolute) error {
return downloadArtifact(p.client, url, dir)
return downloadArtifact(p.client, url, dir, "")
}

func downloadArtifact(httpClient *http.Client, url string, destDir safepaths.Absolute) error {
func (p *apiPlatform) DownloadWithName(url string, name string, dir safepaths.Absolute) error {
return downloadArtifact(p.client, url, dir, name)
}

func downloadArtifact(httpClient *http.Client, url string, destDir safepaths.Absolute, name string) error {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
// The server rejects this :(
//req.Header.Set("Accept", "application/zip")

resp, err := httpClient.Do(req)
if err != nil {
Expand All @@ -45,26 +48,49 @@ func downloadArtifact(httpClient *http.Client, url string, destDir safepaths.Abs
return api.HandleHTTPError(resp)
}

tmpfile, err := os.CreateTemp("", "gh-artifact.*.zip")
if err != nil {
return fmt.Errorf("error initializing temporary file: %w", err)
}
defer func() {
_ = tmpfile.Close()
_ = os.Remove(tmpfile.Name())
}()
// Non-zipped artifacts (uploaded with archive: false) are returned
// with Content-Type application/octet-stream. Only attempt zip
// extraction when the response is actually a zip archive.
if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/zip") {
tmpfile, err := os.CreateTemp("", "gh-artifact.*.zip")
if err != nil {
return fmt.Errorf("error initializing temporary file: %w", err)
}
defer func() {
_ = tmpfile.Close()
_ = os.Remove(tmpfile.Name())
}()

size, err := io.Copy(tmpfile, resp.Body)
if err != nil {
return fmt.Errorf("error writing zip archive: %w", err)
}
size, err := io.Copy(tmpfile, resp.Body)
if err != nil {
return fmt.Errorf("error writing zip archive: %w", err)
}

zipfile, err := zip.NewReader(tmpfile, size)
if err != nil {
return fmt.Errorf("error extracting zip archive: %w", err)
}
if err := ghzip.ExtractZip(zipfile, destDir); err != nil {
return fmt.Errorf("error extracting zip archive: %w", err)
zipfile, err := zip.NewReader(tmpfile, size)
if err != nil {
return fmt.Errorf("error extracting zip archive: %w", err)
}
if err := ghzip.ExtractZip(zipfile, destDir); err != nil {
return fmt.Errorf("error extracting zip archive: %w", err)
}
} else {
// Non-zipped artifact: save the raw file directly.
fileName := name
if fileName == "" {
fileName = "artifact"
}
filePath, err := destDir.Join(fileName)
if err != nil {
return fmt.Errorf("error creating file path: %w", err)
}
f, err := os.Create(filePath.String())
if err != nil {
return fmt.Errorf("error creating file: %w", err)
}
defer f.Close()
if _, err := io.Copy(f, resp.Body); err != nil {
return fmt.Errorf("error writing file: %w", err)
}
}

return nil
Expand Down
28 changes: 28 additions & 0 deletions pkg/cmd/run/download/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,31 @@ func Test_Download(t *testing.T) {
filepath.Join("artifact", "src", "util.go"),
}, paths)
}

func Test_Download_NonZip(t *testing.T) {
tmpDir := t.TempDir()
destDir, err := safepaths.ParseAbsolute(filepath.Join(tmpDir, "artifact"))
require.NoError(t, err)

reg := &httpmock.Registry{}
defer reg.Verify(t)

reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO/actions/artifacts/12345/zip"),
func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Header: http.Header{"Content-Type": []string{"application/octet-stream"}},
Body: http.NoBody,
}, nil
})

api := &apiPlatform{
client: &http.Client{Transport: reg},
}
require.NoError(t, api.DownloadWithName("https://api.github.com/repos/OWNER/REPO/actions/artifacts/12345/zip", "my-artifact.txt", destDir))

// Verify the file was saved directly (not extracted as zip)
_, err = os.Stat(filepath.Join(destDir.String(), "my-artifact.txt"))
require.NoError(t, err)
}