github.com/google/go-github/v42@v42.0.0/github/repos_contents.go (about)

     1  // Copyright 2013 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  // Repository contents API methods.
     7  // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/contents/
     8  
     9  package github
    10  
    11  import (
    12  	"context"
    13  	"encoding/base64"
    14  	"encoding/json"
    15  	"errors"
    16  	"fmt"
    17  	"io"
    18  	"net/http"
    19  	"net/url"
    20  	"path"
    21  	"strings"
    22  )
    23  
    24  // RepositoryContent represents a file or directory in a github repository.
    25  type RepositoryContent struct {
    26  	Type *string `json:"type,omitempty"`
    27  	// Target is only set if the type is "symlink" and the target is not a normal file.
    28  	// If Target is set, Path will be the symlink path.
    29  	Target   *string `json:"target,omitempty"`
    30  	Encoding *string `json:"encoding,omitempty"`
    31  	Size     *int    `json:"size,omitempty"`
    32  	Name     *string `json:"name,omitempty"`
    33  	Path     *string `json:"path,omitempty"`
    34  	// Content contains the actual file content, which may be encoded.
    35  	// Callers should call GetContent which will decode the content if
    36  	// necessary.
    37  	Content     *string `json:"content,omitempty"`
    38  	SHA         *string `json:"sha,omitempty"`
    39  	URL         *string `json:"url,omitempty"`
    40  	GitURL      *string `json:"git_url,omitempty"`
    41  	HTMLURL     *string `json:"html_url,omitempty"`
    42  	DownloadURL *string `json:"download_url,omitempty"`
    43  }
    44  
    45  // RepositoryContentResponse holds the parsed response from CreateFile, UpdateFile, and DeleteFile.
    46  type RepositoryContentResponse struct {
    47  	Content *RepositoryContent `json:"content,omitempty"`
    48  	Commit  `json:"commit,omitempty"`
    49  }
    50  
    51  // RepositoryContentFileOptions specifies optional parameters for CreateFile, UpdateFile, and DeleteFile.
    52  type RepositoryContentFileOptions struct {
    53  	Message   *string       `json:"message,omitempty"`
    54  	Content   []byte        `json:"content,omitempty"` // unencoded
    55  	SHA       *string       `json:"sha,omitempty"`
    56  	Branch    *string       `json:"branch,omitempty"`
    57  	Author    *CommitAuthor `json:"author,omitempty"`
    58  	Committer *CommitAuthor `json:"committer,omitempty"`
    59  }
    60  
    61  // RepositoryContentGetOptions represents an optional ref parameter, which can be a SHA,
    62  // branch, or tag
    63  type RepositoryContentGetOptions struct {
    64  	Ref string `url:"ref,omitempty"`
    65  }
    66  
    67  // String converts RepositoryContent to a string. It's primarily for testing.
    68  func (r RepositoryContent) String() string {
    69  	return Stringify(r)
    70  }
    71  
    72  // GetContent returns the content of r, decoding it if necessary.
    73  func (r *RepositoryContent) GetContent() (string, error) {
    74  	var encoding string
    75  	if r.Encoding != nil {
    76  		encoding = *r.Encoding
    77  	}
    78  
    79  	switch encoding {
    80  	case "base64":
    81  		if r.Content == nil {
    82  			return "", errors.New("malformed response: base64 encoding of null content")
    83  		}
    84  		c, err := base64.StdEncoding.DecodeString(*r.Content)
    85  		return string(c), err
    86  	case "":
    87  		if r.Content == nil {
    88  			return "", nil
    89  		}
    90  		return *r.Content, nil
    91  	default:
    92  		return "", fmt.Errorf("unsupported content encoding: %v", encoding)
    93  	}
    94  }
    95  
    96  // GetReadme gets the Readme file for the repository.
    97  //
    98  // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#get-a-repository-readme
    99  func (s *RepositoriesService) GetReadme(ctx context.Context, owner, repo string, opts *RepositoryContentGetOptions) (*RepositoryContent, *Response, error) {
   100  	u := fmt.Sprintf("repos/%v/%v/readme", owner, repo)
   101  	u, err := addOptions(u, opts)
   102  	if err != nil {
   103  		return nil, nil, err
   104  	}
   105  	req, err := s.client.NewRequest("GET", u, nil)
   106  	if err != nil {
   107  		return nil, nil, err
   108  	}
   109  	readme := new(RepositoryContent)
   110  	resp, err := s.client.Do(ctx, req, readme)
   111  	if err != nil {
   112  		return nil, resp, err
   113  	}
   114  	return readme, resp, nil
   115  }
   116  
   117  // DownloadContents returns an io.ReadCloser that reads the contents of the
   118  // specified file. This function will work with files of any size, as opposed
   119  // to GetContents which is limited to 1 Mb files. It is the caller's
   120  // responsibility to close the ReadCloser.
   121  //
   122  // It is possible for the download to result in a failed response when the
   123  // returned error is nil. Callers should check the returned Response status
   124  // code to verify the content is from a successful response.
   125  func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *Response, error) {
   126  	dir := path.Dir(filepath)
   127  	filename := path.Base(filepath)
   128  	_, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts)
   129  	if err != nil {
   130  		return nil, resp, err
   131  	}
   132  	for _, contents := range dirContents {
   133  		if *contents.Name == filename {
   134  			if contents.DownloadURL == nil || *contents.DownloadURL == "" {
   135  				return nil, resp, fmt.Errorf("no download link found for %s", filepath)
   136  			}
   137  			dlResp, err := s.client.client.Get(*contents.DownloadURL)
   138  			if err != nil {
   139  				return nil, &Response{Response: dlResp}, err
   140  			}
   141  			return dlResp.Body, &Response{Response: dlResp}, nil
   142  		}
   143  	}
   144  	return nil, resp, fmt.Errorf("no file named %s found in %s", filename, dir)
   145  }
   146  
   147  // DownloadContentsWithMeta is identical to DownloadContents but additionally
   148  // returns the RepositoryContent of the requested file. This additional data
   149  // is useful for future operations involving the requested file. For merely
   150  // reading the content of a file, DownloadContents is perfectly adequate.
   151  //
   152  // It is possible for the download to result in a failed response when the
   153  // returned error is nil. Callers should check the returned Response status
   154  // code to verify the content is from a successful response.
   155  func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *RepositoryContent, *Response, error) {
   156  	dir := path.Dir(filepath)
   157  	filename := path.Base(filepath)
   158  	_, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts)
   159  	if err != nil {
   160  		return nil, nil, resp, err
   161  	}
   162  	for _, contents := range dirContents {
   163  		if *contents.Name == filename {
   164  			if contents.DownloadURL == nil || *contents.DownloadURL == "" {
   165  				return nil, contents, resp, fmt.Errorf("no download link found for %s", filepath)
   166  			}
   167  			dlResp, err := s.client.client.Get(*contents.DownloadURL)
   168  			if err != nil {
   169  				return nil, contents, &Response{Response: dlResp}, err
   170  			}
   171  			return dlResp.Body, contents, &Response{Response: dlResp}, nil
   172  		}
   173  	}
   174  	return nil, nil, resp, fmt.Errorf("no file named %s found in %s", filename, dir)
   175  }
   176  
   177  // GetContents can return either the metadata and content of a single file
   178  // (when path references a file) or the metadata of all the files and/or
   179  // subdirectories of a directory (when path references a directory). To make it
   180  // easy to distinguish between both result types and to mimic the API as much
   181  // as possible, both result types will be returned but only one will contain a
   182  // value and the other will be nil.
   183  //
   184  // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#get-repository-content
   185  func (s *RepositoriesService) GetContents(ctx context.Context, owner, repo, path string, opts *RepositoryContentGetOptions) (fileContent *RepositoryContent, directoryContent []*RepositoryContent, resp *Response, err error) {
   186  	escapedPath := (&url.URL{Path: strings.TrimSuffix(path, "/")}).String()
   187  	u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, escapedPath)
   188  	u, err = addOptions(u, opts)
   189  	if err != nil {
   190  		return nil, nil, nil, err
   191  	}
   192  	req, err := s.client.NewRequest("GET", u, nil)
   193  	if err != nil {
   194  		return nil, nil, nil, err
   195  	}
   196  	var rawJSON json.RawMessage
   197  	resp, err = s.client.Do(ctx, req, &rawJSON)
   198  	if err != nil {
   199  		return nil, nil, resp, err
   200  	}
   201  	fileUnmarshalError := json.Unmarshal(rawJSON, &fileContent)
   202  	if fileUnmarshalError == nil {
   203  		return fileContent, nil, resp, nil
   204  	}
   205  	directoryUnmarshalError := json.Unmarshal(rawJSON, &directoryContent)
   206  	if directoryUnmarshalError == nil {
   207  		return nil, directoryContent, resp, nil
   208  	}
   209  	return nil, nil, resp, fmt.Errorf("unmarshalling failed for both file and directory content: %s and %s", fileUnmarshalError, directoryUnmarshalError)
   210  }
   211  
   212  // CreateFile creates a new file in a repository at the given path and returns
   213  // the commit and file metadata.
   214  //
   215  // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#create-or-update-file-contents
   216  func (s *RepositoriesService) CreateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
   217  	u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
   218  	req, err := s.client.NewRequest("PUT", u, opts)
   219  	if err != nil {
   220  		return nil, nil, err
   221  	}
   222  	createResponse := new(RepositoryContentResponse)
   223  	resp, err := s.client.Do(ctx, req, createResponse)
   224  	if err != nil {
   225  		return nil, resp, err
   226  	}
   227  	return createResponse, resp, nil
   228  }
   229  
   230  // UpdateFile updates a file in a repository at the given path and returns the
   231  // commit and file metadata. Requires the blob SHA of the file being updated.
   232  //
   233  // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#create-or-update-file-contents
   234  func (s *RepositoriesService) UpdateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
   235  	u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
   236  	req, err := s.client.NewRequest("PUT", u, opts)
   237  	if err != nil {
   238  		return nil, nil, err
   239  	}
   240  	updateResponse := new(RepositoryContentResponse)
   241  	resp, err := s.client.Do(ctx, req, updateResponse)
   242  	if err != nil {
   243  		return nil, resp, err
   244  	}
   245  	return updateResponse, resp, nil
   246  }
   247  
   248  // DeleteFile deletes a file from a repository and returns the commit.
   249  // Requires the blob SHA of the file to be deleted.
   250  //
   251  // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#delete-a-file
   252  func (s *RepositoriesService) DeleteFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
   253  	u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
   254  	req, err := s.client.NewRequest("DELETE", u, opts)
   255  	if err != nil {
   256  		return nil, nil, err
   257  	}
   258  	deleteResponse := new(RepositoryContentResponse)
   259  	resp, err := s.client.Do(ctx, req, deleteResponse)
   260  	if err != nil {
   261  		return nil, resp, err
   262  	}
   263  	return deleteResponse, resp, nil
   264  }
   265  
   266  // ArchiveFormat is used to define the archive type when calling GetArchiveLink.
   267  type ArchiveFormat string
   268  
   269  const (
   270  	// Tarball specifies an archive in gzipped tar format.
   271  	Tarball ArchiveFormat = "tarball"
   272  
   273  	// Zipball specifies an archive in zip format.
   274  	Zipball ArchiveFormat = "zipball"
   275  )
   276  
   277  // GetArchiveLink returns an URL to download a tarball or zipball archive for a
   278  // repository. The archiveFormat can be specified by either the github.Tarball
   279  // or github.Zipball constant.
   280  //
   281  // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/contents/#get-archive-link
   282  func (s *RepositoriesService) GetArchiveLink(ctx context.Context, owner, repo string, archiveformat ArchiveFormat, opts *RepositoryContentGetOptions, followRedirects bool) (*url.URL, *Response, error) {
   283  	u := fmt.Sprintf("repos/%s/%s/%s", owner, repo, archiveformat)
   284  	if opts != nil && opts.Ref != "" {
   285  		u += fmt.Sprintf("/%s", opts.Ref)
   286  	}
   287  	resp, err := s.getArchiveLinkFromURL(ctx, u, followRedirects)
   288  	if err != nil {
   289  		return nil, nil, err
   290  	}
   291  	if resp.StatusCode != http.StatusFound {
   292  		return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status)
   293  	}
   294  	parsedURL, err := url.Parse(resp.Header.Get("Location"))
   295  	return parsedURL, newResponse(resp), err
   296  }
   297  
   298  func (s *RepositoriesService) getArchiveLinkFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
   299  	req, err := s.client.NewRequest("GET", u, nil)
   300  	if err != nil {
   301  		return nil, err
   302  	}
   303  
   304  	var resp *http.Response
   305  	// Use http.DefaultTransport if no custom Transport is configured
   306  	req = withContext(ctx, req)
   307  	if s.client.client.Transport == nil {
   308  		resp, err = http.DefaultTransport.RoundTrip(req)
   309  	} else {
   310  		resp, err = s.client.client.Transport.RoundTrip(req)
   311  	}
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  	resp.Body.Close()
   316  
   317  	// If redirect response is returned, follow it
   318  	if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
   319  		u = resp.Header.Get("Location")
   320  		resp, err = s.getArchiveLinkFromURL(ctx, u, false)
   321  	}
   322  	return resp, err
   323  }