github.com/google/go-github/v65@v65.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 dlReq, err := http.NewRequestWithContext(ctx, http.MethodGet, *contents.DownloadURL, nil) 154 if err != nil { 155 return nil, resp, err 156 } 157 dlResp, err := s.client.client.Do(dlReq) 158 if err != nil { 159 return nil, &Response{Response: dlResp}, err 160 } 161 162 return dlResp.Body, &Response{Response: dlResp}, nil 163 } 164 } 165 166 return nil, resp, fmt.Errorf("no file named %s found in %s", filename, dir) 167 } 168 169 // DownloadContentsWithMeta is identical to DownloadContents but additionally 170 // returns the RepositoryContent of the requested file. This additional data 171 // is useful for future operations involving the requested file. For merely 172 // reading the content of a file, DownloadContents is perfectly adequate. 173 // 174 // It is possible for the download to result in a failed response when the 175 // returned error is nil. Callers should check the returned Response status 176 // code to verify the content is from a successful response. 177 // 178 // GitHub API docs: https://docs.github.com/rest/repos/contents#get-repository-content 179 // 180 //meta:operation GET /repos/{owner}/{repo}/contents/{path} 181 func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *RepositoryContent, *Response, error) { 182 dir := path.Dir(filepath) 183 filename := path.Base(filepath) 184 _, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts) 185 if err != nil { 186 return nil, nil, resp, err 187 } 188 189 for _, contents := range dirContents { 190 if *contents.Name == filename { 191 if contents.DownloadURL == nil || *contents.DownloadURL == "" { 192 return nil, contents, resp, fmt.Errorf("no download link found for %s", filepath) 193 } 194 195 dlReq, err := http.NewRequestWithContext(ctx, http.MethodGet, *contents.DownloadURL, nil) 196 if err != nil { 197 return nil, contents, resp, err 198 } 199 dlResp, err := s.client.client.Do(dlReq) 200 if err != nil { 201 return nil, contents, &Response{Response: dlResp}, err 202 } 203 204 return dlResp.Body, contents, &Response{Response: dlResp}, nil 205 } 206 } 207 208 return nil, nil, resp, fmt.Errorf("no file named %s found in %s", filename, dir) 209 } 210 211 // GetContents can return either the metadata and content of a single file 212 // (when path references a file) or the metadata of all the files and/or 213 // subdirectories of a directory (when path references a directory). To make it 214 // easy to distinguish between both result types and to mimic the API as much 215 // as possible, both result types will be returned but only one will contain a 216 // value and the other will be nil. 217 // 218 // Due to an auth vulnerability issue in the GitHub v3 API, ".." is not allowed 219 // to appear anywhere in the "path" or this method will return an error. 220 // 221 // GitHub API docs: https://docs.github.com/rest/repos/contents#get-repository-content 222 // 223 //meta:operation GET /repos/{owner}/{repo}/contents/{path} 224 func (s *RepositoriesService) GetContents(ctx context.Context, owner, repo, path string, opts *RepositoryContentGetOptions) (fileContent *RepositoryContent, directoryContent []*RepositoryContent, resp *Response, err error) { 225 if strings.Contains(path, "..") { 226 return nil, nil, nil, ErrPathForbidden 227 } 228 229 escapedPath := (&url.URL{Path: strings.TrimSuffix(path, "/")}).String() 230 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, escapedPath) 231 u, err = addOptions(u, opts) 232 if err != nil { 233 return nil, nil, nil, err 234 } 235 236 req, err := s.client.NewRequest("GET", u, nil) 237 if err != nil { 238 return nil, nil, nil, err 239 } 240 241 var rawJSON json.RawMessage 242 resp, err = s.client.Do(ctx, req, &rawJSON) 243 if err != nil { 244 return nil, nil, resp, err 245 } 246 247 fileUnmarshalError := json.Unmarshal(rawJSON, &fileContent) 248 if fileUnmarshalError == nil { 249 return fileContent, nil, resp, nil 250 } 251 252 directoryUnmarshalError := json.Unmarshal(rawJSON, &directoryContent) 253 if directoryUnmarshalError == nil { 254 return nil, directoryContent, resp, nil 255 } 256 257 return nil, nil, resp, fmt.Errorf("unmarshalling failed for both file and directory content: %s and %s", fileUnmarshalError, directoryUnmarshalError) 258 } 259 260 // CreateFile creates a new file in a repository at the given path and returns 261 // the commit and file metadata. 262 // 263 // GitHub API docs: https://docs.github.com/rest/repos/contents#create-or-update-file-contents 264 // 265 //meta:operation PUT /repos/{owner}/{repo}/contents/{path} 266 func (s *RepositoriesService) CreateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 267 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 268 req, err := s.client.NewRequest("PUT", u, opts) 269 if err != nil { 270 return nil, nil, err 271 } 272 273 createResponse := new(RepositoryContentResponse) 274 resp, err := s.client.Do(ctx, req, createResponse) 275 if err != nil { 276 return nil, resp, err 277 } 278 279 return createResponse, resp, nil 280 } 281 282 // UpdateFile updates a file in a repository at the given path and returns the 283 // commit and file metadata. Requires the blob SHA of the file being updated. 284 // 285 // GitHub API docs: https://docs.github.com/rest/repos/contents#create-or-update-file-contents 286 // 287 //meta:operation PUT /repos/{owner}/{repo}/contents/{path} 288 func (s *RepositoriesService) UpdateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 289 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 290 req, err := s.client.NewRequest("PUT", u, opts) 291 if err != nil { 292 return nil, nil, err 293 } 294 295 updateResponse := new(RepositoryContentResponse) 296 resp, err := s.client.Do(ctx, req, updateResponse) 297 if err != nil { 298 return nil, resp, err 299 } 300 301 return updateResponse, resp, nil 302 } 303 304 // DeleteFile deletes a file from a repository and returns the commit. 305 // Requires the blob SHA of the file to be deleted. 306 // 307 // GitHub API docs: https://docs.github.com/rest/repos/contents#delete-a-file 308 // 309 //meta:operation DELETE /repos/{owner}/{repo}/contents/{path} 310 func (s *RepositoriesService) DeleteFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 311 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 312 req, err := s.client.NewRequest("DELETE", u, opts) 313 if err != nil { 314 return nil, nil, err 315 } 316 317 deleteResponse := new(RepositoryContentResponse) 318 resp, err := s.client.Do(ctx, req, deleteResponse) 319 if err != nil { 320 return nil, resp, err 321 } 322 323 return deleteResponse, resp, nil 324 } 325 326 // ArchiveFormat is used to define the archive type when calling GetArchiveLink. 327 type ArchiveFormat string 328 329 const ( 330 // Tarball specifies an archive in gzipped tar format. 331 Tarball ArchiveFormat = "tarball" 332 333 // Zipball specifies an archive in zip format. 334 Zipball ArchiveFormat = "zipball" 335 ) 336 337 // GetArchiveLink returns an URL to download a tarball or zipball archive for a 338 // repository. The archiveFormat can be specified by either the github.Tarball 339 // or github.Zipball constant. 340 // 341 // GitHub API docs: https://docs.github.com/rest/repos/contents#download-a-repository-archive-tar 342 // GitHub API docs: https://docs.github.com/rest/repos/contents#download-a-repository-archive-zip 343 // 344 //meta:operation GET /repos/{owner}/{repo}/tarball/{ref} 345 //meta:operation GET /repos/{owner}/{repo}/zipball/{ref} 346 func (s *RepositoriesService) GetArchiveLink(ctx context.Context, owner, repo string, archiveformat ArchiveFormat, opts *RepositoryContentGetOptions, maxRedirects int) (*url.URL, *Response, error) { 347 u := fmt.Sprintf("repos/%s/%s/%s", owner, repo, archiveformat) 348 if opts != nil && opts.Ref != "" { 349 u += fmt.Sprintf("/%s", opts.Ref) 350 } 351 resp, err := s.client.roundTripWithOptionalFollowRedirect(ctx, u, maxRedirects) 352 if err != nil { 353 return nil, nil, err 354 } 355 defer resp.Body.Close() 356 357 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { 358 return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status) 359 } 360 361 parsedURL, err := url.Parse(resp.Header.Get("Location")) 362 if err != nil { 363 return nil, newResponse(resp), err 364 } 365 366 return parsedURL, newResponse(resp), nil 367 }