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 }