Skip to content

Commit 6b6e681

Browse files
authored
feat: Refactor repositories download contents (#4153)
1 parent fee8865 commit 6b6e681

File tree

4 files changed

+204
-160
lines changed

4 files changed

+204
-160
lines changed

example/contents/main.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2026 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
// The contents command utilizes go-github as a CLI tool for
7+
// downloading the contents of a file in a repository.
8+
// It takes an inputs of the repository owner, repository name, path to the
9+
// file in the repository, reference (branch, tag or commit SHA), and output
10+
// path for the downloaded file. It then uses the Repositories.DownloadContents
11+
// method to download the file and saves it to the specified output path.
12+
package main
13+
14+
import (
15+
"bufio"
16+
"context"
17+
"fmt"
18+
"io"
19+
"os"
20+
"path/filepath"
21+
"strings"
22+
23+
"github.com/google/go-github/v84/github"
24+
)
25+
26+
func main() {
27+
fmt.Println("This example will download the contents of a file from a GitHub repository.")
28+
29+
r := bufio.NewReader(os.Stdin)
30+
31+
fmt.Print("Repository Owner: ")
32+
owner, _ := r.ReadString('\n')
33+
owner = strings.TrimSpace(owner)
34+
35+
fmt.Print("Repository Name: ")
36+
repo, _ := r.ReadString('\n')
37+
repo = strings.TrimSpace(repo)
38+
39+
fmt.Print("Repository Path: ")
40+
repoPath, _ := r.ReadString('\n')
41+
repoPath = strings.TrimSpace(repoPath)
42+
43+
fmt.Print("Reference (branch, tag or commit SHA): ")
44+
ref, _ := r.ReadString('\n')
45+
ref = strings.TrimSpace(ref)
46+
47+
fmt.Print("Output Path: ")
48+
outputPath, _ := r.ReadString('\n')
49+
outputPath = filepath.Clean(strings.TrimSpace(outputPath))
50+
51+
fmt.Printf("\nDownloading %v/%v/%v at ref %v to %v...\n", owner, repo, repoPath, ref, outputPath)
52+
53+
client := github.NewClient(nil)
54+
55+
rc, _, err := client.Repositories.DownloadContents(context.Background(), owner, repo, repoPath, &github.RepositoryContentGetOptions{Ref: ref})
56+
if err != nil {
57+
fmt.Printf("Error: %v\n", err)
58+
os.Exit(1)
59+
}
60+
defer rc.Close()
61+
62+
f, err := os.Create(outputPath) //#nosec G703 -- path is validated above
63+
if err != nil {
64+
fmt.Printf("Error: %v\n", err)
65+
os.Exit(1)
66+
}
67+
defer f.Close()
68+
69+
if _, err := io.Copy(f, rc); err != nil {
70+
fmt.Printf("Error: %v\n", err)
71+
os.Exit(1)
72+
}
73+
74+
fmt.Println("Download completed.")
75+
}

github/repos_contents.go

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"io"
1818
"net/http"
1919
"net/url"
20-
"path"
2120
"strings"
2221
)
2322

@@ -137,40 +136,8 @@ func (s *RepositoriesService) GetReadme(ctx context.Context, owner, repo string,
137136
//
138137
//meta:operation GET /repos/{owner}/{repo}/contents/{path}
139138
func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *Response, error) {
140-
dir := path.Dir(filepath)
141-
filename := path.Base(filepath)
142-
fileContent, _, resp, err := s.GetContents(ctx, owner, repo, filepath, opts)
143-
if err == nil && fileContent != nil {
144-
content, err := fileContent.GetContent()
145-
if err == nil && content != "" {
146-
return io.NopCloser(strings.NewReader(content)), resp, nil
147-
}
148-
}
149-
150-
_, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts)
151-
if err != nil {
152-
return nil, resp, err
153-
}
154-
155-
for _, contents := range dirContents {
156-
if contents.GetName() == filename {
157-
if contents.GetDownloadURL() == "" {
158-
return nil, resp, fmt.Errorf("no download link found for %v", filepath)
159-
}
160-
dlReq, err := http.NewRequestWithContext(ctx, "GET", *contents.DownloadURL, nil)
161-
if err != nil {
162-
return nil, resp, err
163-
}
164-
dlResp, err := s.client.client.Do(dlReq)
165-
if err != nil {
166-
return nil, &Response{Response: dlResp}, err
167-
}
168-
169-
return dlResp.Body, &Response{Response: dlResp}, nil
170-
}
171-
}
172-
173-
return nil, resp, fmt.Errorf("no file named %v found in %v", filename, dir)
139+
rc, _, resp, err := s.DownloadContentsWithMeta(ctx, owner, repo, filepath, opts)
140+
return rc, resp, err
174141
}
175142

176143
// DownloadContentsWithMeta is identical to DownloadContents but additionally
@@ -186,40 +153,36 @@ func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo,
186153
//
187154
//meta:operation GET /repos/{owner}/{repo}/contents/{path}
188155
func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *RepositoryContent, *Response, error) {
189-
dir := path.Dir(filepath)
190-
filename := path.Base(filepath)
191156
fileContent, _, resp, err := s.GetContents(ctx, owner, repo, filepath, opts)
192-
if err == nil && fileContent != nil {
193-
content, err := fileContent.GetContent()
194-
if err == nil && content != "" {
195-
return io.NopCloser(strings.NewReader(content)), fileContent, resp, nil
196-
}
157+
if err != nil {
158+
return nil, nil, resp, err
159+
}
160+
161+
if fileContent == nil {
162+
return nil, nil, resp, errors.New("no file content found")
163+
}
164+
165+
content, err := fileContent.GetContent()
166+
if err == nil && content != "" {
167+
return io.NopCloser(strings.NewReader(content)), fileContent, resp, nil
197168
}
198169

199-
_, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts)
170+
downloadURL := fileContent.GetDownloadURL()
171+
if downloadURL == "" {
172+
return nil, fileContent, resp, errors.New("download url is empty")
173+
}
174+
175+
dlReq, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
200176
if err != nil {
201-
return nil, nil, resp, err
177+
return nil, fileContent, resp, err
202178
}
203179

204-
for _, contents := range dirContents {
205-
if contents.GetName() == filename {
206-
if contents.GetDownloadURL() == "" {
207-
return nil, contents, resp, fmt.Errorf("no download link found for %v", filepath)
208-
}
209-
dlReq, err := http.NewRequestWithContext(ctx, "GET", *contents.DownloadURL, nil)
210-
if err != nil {
211-
return nil, contents, resp, err
212-
}
213-
dlResp, err := s.client.client.Do(dlReq)
214-
if err != nil {
215-
return nil, contents, &Response{Response: dlResp}, err
216-
}
217-
218-
return dlResp.Body, contents, &Response{Response: dlResp}, nil
219-
}
180+
dlResp, err := s.client.client.Do(dlReq)
181+
if err != nil {
182+
return nil, fileContent, &Response{Response: dlResp}, err
220183
}
221184

222-
return nil, nil, resp, fmt.Errorf("no file named %v found in %v", filename, dir)
185+
return dlResp.Body, fileContent, &Response{Response: dlResp}, nil
223186
}
224187

225188
// GetContents can return either the metadata and content of a single file

0 commit comments

Comments
 (0)