github.com/google/go-github/v33@v33.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 // GetContents can return either the metadata and content of a single file 148 // (when path references a file) or the metadata of all the files and/or 149 // subdirectories of a directory (when path references a directory). To make it 150 // easy to distinguish between both result types and to mimic the API as much 151 // as possible, both result types will be returned but only one will contain a 152 // value and the other will be nil. 153 // 154 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#get-repository-content 155 func (s *RepositoriesService) GetContents(ctx context.Context, owner, repo, path string, opts *RepositoryContentGetOptions) (fileContent *RepositoryContent, directoryContent []*RepositoryContent, resp *Response, err error) { 156 escapedPath := (&url.URL{Path: strings.TrimSuffix(path, "/")}).String() 157 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, escapedPath) 158 u, err = addOptions(u, opts) 159 if err != nil { 160 return nil, nil, nil, err 161 } 162 req, err := s.client.NewRequest("GET", u, nil) 163 if err != nil { 164 return nil, nil, nil, err 165 } 166 var rawJSON json.RawMessage 167 resp, err = s.client.Do(ctx, req, &rawJSON) 168 if err != nil { 169 return nil, nil, resp, err 170 } 171 fileUnmarshalError := json.Unmarshal(rawJSON, &fileContent) 172 if fileUnmarshalError == nil { 173 return fileContent, nil, resp, nil 174 } 175 directoryUnmarshalError := json.Unmarshal(rawJSON, &directoryContent) 176 if directoryUnmarshalError == nil { 177 return nil, directoryContent, resp, nil 178 } 179 return nil, nil, resp, fmt.Errorf("unmarshalling failed for both file and directory content: %s and %s", fileUnmarshalError, directoryUnmarshalError) 180 } 181 182 // CreateFile creates a new file in a repository at the given path and returns 183 // the commit and file metadata. 184 // 185 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#create-or-update-file-contents 186 func (s *RepositoriesService) CreateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 187 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 188 req, err := s.client.NewRequest("PUT", u, opts) 189 if err != nil { 190 return nil, nil, err 191 } 192 createResponse := new(RepositoryContentResponse) 193 resp, err := s.client.Do(ctx, req, createResponse) 194 if err != nil { 195 return nil, resp, err 196 } 197 return createResponse, resp, nil 198 } 199 200 // UpdateFile updates a file in a repository at the given path and returns the 201 // commit and file metadata. Requires the blob SHA of the file being updated. 202 // 203 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#create-or-update-file-contents 204 func (s *RepositoriesService) UpdateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 205 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 206 req, err := s.client.NewRequest("PUT", u, opts) 207 if err != nil { 208 return nil, nil, err 209 } 210 updateResponse := new(RepositoryContentResponse) 211 resp, err := s.client.Do(ctx, req, updateResponse) 212 if err != nil { 213 return nil, resp, err 214 } 215 return updateResponse, resp, nil 216 } 217 218 // DeleteFile deletes a file from a repository and returns the commit. 219 // Requires the blob SHA of the file to be deleted. 220 // 221 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#delete-a-file 222 func (s *RepositoriesService) DeleteFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 223 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 224 req, err := s.client.NewRequest("DELETE", u, opts) 225 if err != nil { 226 return nil, nil, err 227 } 228 deleteResponse := new(RepositoryContentResponse) 229 resp, err := s.client.Do(ctx, req, deleteResponse) 230 if err != nil { 231 return nil, resp, err 232 } 233 return deleteResponse, resp, nil 234 } 235 236 // ArchiveFormat is used to define the archive type when calling GetArchiveLink. 237 type ArchiveFormat string 238 239 const ( 240 // Tarball specifies an archive in gzipped tar format. 241 Tarball ArchiveFormat = "tarball" 242 243 // Zipball specifies an archive in zip format. 244 Zipball ArchiveFormat = "zipball" 245 ) 246 247 // GetArchiveLink returns an URL to download a tarball or zipball archive for a 248 // repository. The archiveFormat can be specified by either the github.Tarball 249 // or github.Zipball constant. 250 // 251 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/contents/#get-archive-link 252 func (s *RepositoriesService) GetArchiveLink(ctx context.Context, owner, repo string, archiveformat ArchiveFormat, opts *RepositoryContentGetOptions, followRedirects bool) (*url.URL, *Response, error) { 253 u := fmt.Sprintf("repos/%s/%s/%s", owner, repo, archiveformat) 254 if opts != nil && opts.Ref != "" { 255 u += fmt.Sprintf("/%s", opts.Ref) 256 } 257 resp, err := s.getArchiveLinkFromURL(ctx, u, followRedirects) 258 if err != nil { 259 return nil, nil, err 260 } 261 if resp.StatusCode != http.StatusFound { 262 return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status) 263 } 264 parsedURL, err := url.Parse(resp.Header.Get("Location")) 265 return parsedURL, newResponse(resp), err 266 } 267 268 func (s *RepositoriesService) getArchiveLinkFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) { 269 req, err := s.client.NewRequest("GET", u, nil) 270 if err != nil { 271 return nil, err 272 } 273 274 var resp *http.Response 275 // Use http.DefaultTransport if no custom Transport is configured 276 req = withContext(ctx, req) 277 if s.client.client.Transport == nil { 278 resp, err = http.DefaultTransport.RoundTrip(req) 279 } else { 280 resp, err = s.client.client.Transport.RoundTrip(req) 281 } 282 if err != nil { 283 return nil, err 284 } 285 resp.Body.Close() 286 287 // If redirect response is returned, follow it 288 if followRedirects && resp.StatusCode == http.StatusMovedPermanently { 289 u = resp.Header.Get("Location") 290 resp, err = s.getArchiveLinkFromURL(ctx, u, false) 291 } 292 return resp, err 293 }