diff --git a/example/contents/main.go b/example/contents/main.go new file mode 100644 index 00000000000..7749f094edf --- /dev/null +++ b/example/contents/main.go @@ -0,0 +1,75 @@ +// Copyright 2026 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The contents command utilizes go-github as a CLI tool for +// downloading the contents of a file in a repository. +// It takes an inputs of the repository owner, repository name, path to the +// file in the repository, reference (branch, tag or commit SHA), and output +// path for the downloaded file. It then uses the Repositories.DownloadContents +// method to download the file and saves it to the specified output path. +package main + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/google/go-github/v84/github" +) + +func main() { + fmt.Println("This example will download the contents of a file from a GitHub repository.") + + r := bufio.NewReader(os.Stdin) + + fmt.Print("Repository Owner: ") + owner, _ := r.ReadString('\n') + owner = strings.TrimSpace(owner) + + fmt.Print("Repository Name: ") + repo, _ := r.ReadString('\n') + repo = strings.TrimSpace(repo) + + fmt.Print("Repository Path: ") + repoPath, _ := r.ReadString('\n') + repoPath = strings.TrimSpace(repoPath) + + fmt.Print("Reference (branch, tag or commit SHA): ") + ref, _ := r.ReadString('\n') + ref = strings.TrimSpace(ref) + + fmt.Print("Output Path: ") + outputPath, _ := r.ReadString('\n') + outputPath = filepath.Clean(strings.TrimSpace(outputPath)) + + fmt.Printf("\nDownloading %v/%v/%v at ref %v to %v...\n", owner, repo, repoPath, ref, outputPath) + + client := github.NewClient(nil) + + rc, _, err := client.Repositories.DownloadContents(context.Background(), owner, repo, repoPath, &github.RepositoryContentGetOptions{Ref: ref}) + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + defer rc.Close() + + f, err := os.Create(outputPath) //#nosec G703 -- path is validated above + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + defer f.Close() + + if _, err := io.Copy(f, rc); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + + fmt.Println("Download completed.") +} diff --git a/github/repos_contents.go b/github/repos_contents.go index 0c2ac3bf25f..53313ea22da 100644 --- a/github/repos_contents.go +++ b/github/repos_contents.go @@ -17,7 +17,6 @@ import ( "io" "net/http" "net/url" - "path" "strings" ) @@ -137,40 +136,8 @@ func (s *RepositoriesService) GetReadme(ctx context.Context, owner, repo string, // //meta:operation GET /repos/{owner}/{repo}/contents/{path} func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *Response, error) { - dir := path.Dir(filepath) - filename := path.Base(filepath) - fileContent, _, resp, err := s.GetContents(ctx, owner, repo, filepath, opts) - if err == nil && fileContent != nil { - content, err := fileContent.GetContent() - if err == nil && content != "" { - return io.NopCloser(strings.NewReader(content)), resp, nil - } - } - - _, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts) - if err != nil { - return nil, resp, err - } - - for _, contents := range dirContents { - if contents.GetName() == filename { - if contents.GetDownloadURL() == "" { - return nil, resp, fmt.Errorf("no download link found for %v", filepath) - } - dlReq, err := http.NewRequestWithContext(ctx, "GET", *contents.DownloadURL, nil) - if err != nil { - return nil, resp, err - } - dlResp, err := s.client.client.Do(dlReq) - if err != nil { - return nil, &Response{Response: dlResp}, err - } - - return dlResp.Body, &Response{Response: dlResp}, nil - } - } - - return nil, resp, fmt.Errorf("no file named %v found in %v", filename, dir) + rc, _, resp, err := s.DownloadContentsWithMeta(ctx, owner, repo, filepath, opts) + return rc, resp, err } // DownloadContentsWithMeta is identical to DownloadContents but additionally @@ -186,40 +153,36 @@ func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo, // //meta:operation GET /repos/{owner}/{repo}/contents/{path} func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *RepositoryContent, *Response, error) { - dir := path.Dir(filepath) - filename := path.Base(filepath) fileContent, _, resp, err := s.GetContents(ctx, owner, repo, filepath, opts) - if err == nil && fileContent != nil { - content, err := fileContent.GetContent() - if err == nil && content != "" { - return io.NopCloser(strings.NewReader(content)), fileContent, resp, nil - } + if err != nil { + return nil, nil, resp, err + } + + if fileContent == nil { + return nil, nil, resp, errors.New("no file content found") + } + + content, err := fileContent.GetContent() + if err == nil && content != "" { + return io.NopCloser(strings.NewReader(content)), fileContent, resp, nil } - _, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts) + downloadURL := fileContent.GetDownloadURL() + if downloadURL == "" { + return nil, fileContent, resp, errors.New("download url is empty") + } + + dlReq, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) if err != nil { - return nil, nil, resp, err + return nil, fileContent, resp, err } - for _, contents := range dirContents { - if contents.GetName() == filename { - if contents.GetDownloadURL() == "" { - return nil, contents, resp, fmt.Errorf("no download link found for %v", filepath) - } - dlReq, err := http.NewRequestWithContext(ctx, "GET", *contents.DownloadURL, nil) - if err != nil { - return nil, contents, resp, err - } - dlResp, err := s.client.client.Do(dlReq) - if err != nil { - return nil, contents, &Response{Response: dlResp}, err - } - - return dlResp.Body, contents, &Response{Response: dlResp}, nil - } + dlResp, err := s.client.client.Do(dlReq) + if err != nil { + return nil, fileContent, &Response{Response: dlResp}, err } - return nil, nil, resp, fmt.Errorf("no file named %v found in %v", filename, dir) + return dlResp.Body, fileContent, &Response{Response: dlResp}, nil } // GetContents can return either the metadata and content of a single file diff --git a/github/repos_contents_test.go b/github/repos_contents_test.go index 113aad6ebc4..bd0f1562779 100644 --- a/github/repos_contents_test.go +++ b/github/repos_contents_test.go @@ -126,18 +126,18 @@ func TestRepositoriesService_GetReadme(t *testing.T) { }) } -func TestRepositoriesService_DownloadContents_SuccessForFile(t *testing.T) { +func TestRepositoriesService_DownloadContents_SuccessWithContent(t *testing.T) { t.Parallel() client, mux, serverURL := setup(t) mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "content": "foo", - "download_url": "`+serverURL+baseURLPath+`/download/f" - }`) + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "content": "foo", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) ctx := t.Context() @@ -175,25 +175,20 @@ func TestRepositoriesService_DownloadContents_SuccessForFile(t *testing.T) { }) } -func TestRepositoriesService_DownloadContents_SuccessForDirectory(t *testing.T) { +func TestRepositoriesService_DownloadContents_SuccessByDownload(t *testing.T) { t.Parallel() client, mux, serverURL := setup(t) mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f" - }`) - }) - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `[{ - "type": "file", - "name": "f", - "download_url": "`+serverURL+baseURLPath+`/download/f" - }]`) + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "content": "", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) + mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, "foo") @@ -240,18 +235,12 @@ func TestRepositoriesService_DownloadContents_FailedResponse(t *testing.T) { mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f" - }`) - }) - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `[{ - "type": "file", - "name": "f", - "download_url": "`+serverURL+baseURLPath+`/download/f" - }]`) + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "content": "", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") @@ -287,18 +276,10 @@ func TestRepositoriesService_DownloadContents_NoDownloadURL(t *testing.T) { mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "content": "" - }`) - }) - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `[{ - "type": "file", - "name": "f", - "content": "" - }]`) + "type": "file", + "name": "f", + "content": "" +}`) }) ctx := t.Context() @@ -322,20 +303,39 @@ func TestRepositoriesService_DownloadContents_NoFile(t *testing.T) { mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "content": "" - }`) + w.WriteHeader(http.StatusNotFound) }) + ctx := t.Context() + reader, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d/f", nil) + if err == nil { + t.Error("Repositories.DownloadContents did not return expected error") + } + + if resp == nil { + t.Error("Repositories.DownloadContents did not return expected response") + } + + if reader != nil { + t.Error("Repositories.DownloadContents did not return expected reader") + } +} + +func TestRepositoriesService_DownloadContents_NotFile(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `[]`) + fmt.Fprint(w, `[{ + "type": "file", + "name": "f", + "content": "" +}]`) }) ctx := t.Context() - reader, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d/f", nil) + reader, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d", nil) if err == nil { t.Error("Repositories.DownloadContents did not return expected error") } @@ -349,18 +349,18 @@ func TestRepositoriesService_DownloadContents_NoFile(t *testing.T) { } } -func TestRepositoriesService_DownloadContentsWithMeta_SuccessForFile(t *testing.T) { +func TestRepositoriesService_DownloadContentsWithMeta_SuccessWithContent(t *testing.T) { t.Parallel() client, mux, serverURL := setup(t) mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "download_url": "`+serverURL+baseURLPath+`/download/f", - "content": "foo" - }`) + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "content": "foo", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) ctx := t.Context() @@ -409,18 +409,19 @@ func TestRepositoriesService_DownloadContentsWithMeta_SuccessForFile(t *testing. }) } -func TestRepositoriesService_DownloadContentsWithMeta_SuccessForDirectory(t *testing.T) { +func TestRepositoriesService_DownloadContentsWithMeta_SuccessByDownload(t *testing.T) { t.Parallel() client, mux, serverURL := setup(t) - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `[{ - "type": "file", - "name": "f", - "download_url": "`+serverURL+baseURLPath+`/download/f" - }]`) + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) + mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, "foo") @@ -459,29 +460,19 @@ func TestRepositoriesService_DownloadContentsWithMeta_FailedResponse(t *testing. t.Parallel() client, mux, serverURL := setup(t) - downloadURL := fmt.Sprintf("%v%v/download/f", serverURL, baseURLPath) - mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "download_url": "`+downloadURL+`" - }`) + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, "foo error") }) - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `[{ - "type": "file", - "name": "f", - "download_url": "`+downloadURL+`" - }]`) - }) ctx := t.Context() r, c, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil) @@ -519,17 +510,10 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoDownloadURL(t *testing.T mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "type": "file", - "name": "f", - }`) - }) - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `[{ - "type": "file", - "name": "f", - "content": "" - }]`) + "type": "file", + "name": "f", + "content": "" +}`) }) ctx := t.Context() @@ -555,9 +539,9 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoFile(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `[]`) + w.WriteHeader(http.StatusNotFound) }) ctx := t.Context() @@ -571,6 +555,30 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoFile(t *testing.T) { } } +func TestRepositoriesService_DownloadContentsWithMeta_NotFile(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{ + "type": "file", + "name": "f", + "content": "" +}]`) + }) + + ctx := t.Context() + _, _, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d", nil) + if err == nil { + t.Error("Repositories.DownloadContentsWithMeta did not return expected error") + } + + if resp == nil { + t.Error("Repositories.DownloadContentsWithMeta did not return expected response") + } +} + func TestRepositoriesService_GetContents_File(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/tools/metadata/main_test.go b/tools/metadata/main_test.go index 703c2d1d7cb..e9b656190a6 100644 --- a/tools/metadata/main_test.go +++ b/tools/metadata/main_test.go @@ -363,12 +363,10 @@ func newTestServer(t *testing.T, ref string, files map[string]any) *httptest.Ser Name: github.Ptr(path.Base(path.Dir(name))), }) mux.HandleFunc( - path.Join(repoPath, "contents/descriptions", path.Dir(name)), - jsonHandler(refQuery, []*github.RepositoryContent{ - { - Name: github.Ptr(path.Base(name)), - DownloadURL: github.Ptr(server.URL + "/dl/" + name), - }, + path.Join(repoPath, "contents/descriptions", name), + jsonHandler(refQuery, &github.RepositoryContent{ + Name: github.Ptr(path.Base(name)), + DownloadURL: github.Ptr(server.URL + "/dl/" + name), }), ) mux.HandleFunc(