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