github.com/google/go-github/v53@v53.2.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/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 // 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"` // 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/rest/repos/contents#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 106 req, err := s.client.NewRequest("GET", u, nil) 107 if err != nil { 108 return nil, nil, err 109 } 110 111 readme := new(RepositoryContent) 112 resp, err := s.client.Do(ctx, req, readme) 113 if err != nil { 114 return nil, resp, err 115 } 116 117 return readme, resp, nil 118 } 119 120 // DownloadContents returns an io.ReadCloser that reads the contents of the 121 // specified file. This function will work with files of any size, as opposed 122 // to GetContents which is limited to 1 Mb files. It is the caller's 123 // responsibility to close the ReadCloser. 124 // 125 // It is possible for the download to result in a failed response when the 126 // returned error is nil. Callers should check the returned Response status 127 // code to verify the content is from a successful response. 128 func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *Response, error) { 129 dir := path.Dir(filepath) 130 filename := path.Base(filepath) 131 _, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts) 132 if err != nil { 133 return nil, resp, err 134 } 135 136 for _, contents := range dirContents { 137 if *contents.Name == filename { 138 if contents.DownloadURL == nil || *contents.DownloadURL == "" { 139 return nil, resp, fmt.Errorf("no download link found for %s", filepath) 140 } 141 142 dlResp, err := s.client.client.Get(*contents.DownloadURL) 143 if err != nil { 144 return nil, &Response{Response: dlResp}, err 145 } 146 147 return dlResp.Body, &Response{Response: dlResp}, nil 148 } 149 } 150 151 return nil, resp, fmt.Errorf("no file named %s found in %s", filename, dir) 152 } 153 154 // DownloadContentsWithMeta is identical to DownloadContents but additionally 155 // returns the RepositoryContent of the requested file. This additional data 156 // is useful for future operations involving the requested file. For merely 157 // reading the content of a file, DownloadContents is perfectly adequate. 158 // 159 // It is possible for the download to result in a failed response when the 160 // returned error is nil. Callers should check the returned Response status 161 // code to verify the content is from a successful response. 162 func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *RepositoryContent, *Response, error) { 163 dir := path.Dir(filepath) 164 filename := path.Base(filepath) 165 _, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts) 166 if err != nil { 167 return nil, nil, resp, err 168 } 169 170 for _, contents := range dirContents { 171 if *contents.Name == filename { 172 if contents.DownloadURL == nil || *contents.DownloadURL == "" { 173 return nil, contents, resp, fmt.Errorf("no download link found for %s", filepath) 174 } 175 176 dlResp, err := s.client.client.Get(*contents.DownloadURL) 177 if err != nil { 178 return nil, contents, &Response{Response: dlResp}, err 179 } 180 181 return dlResp.Body, contents, &Response{Response: dlResp}, nil 182 } 183 } 184 185 return nil, nil, resp, fmt.Errorf("no file named %s found in %s", filename, dir) 186 } 187 188 // GetContents can return either the metadata and content of a single file 189 // (when path references a file) or the metadata of all the files and/or 190 // subdirectories of a directory (when path references a directory). To make it 191 // easy to distinguish between both result types and to mimic the API as much 192 // as possible, both result types will be returned but only one will contain a 193 // value and the other will be nil. 194 // 195 // Due to an auth vulnerability issue in the GitHub v3 API, ".." is not allowed 196 // to appear anywhere in the "path" or this method will return an error. 197 // 198 // GitHub API docs: https://docs.github.com/en/rest/repos/contents#get-repository-content 199 func (s *RepositoriesService) GetContents(ctx context.Context, owner, repo, path string, opts *RepositoryContentGetOptions) (fileContent *RepositoryContent, directoryContent []*RepositoryContent, resp *Response, err error) { 200 if strings.Contains(path, "..") { 201 return nil, nil, nil, errors.New("path must not contain '..' due to auth vulnerability issue") 202 } 203 204 escapedPath := (&url.URL{Path: strings.TrimSuffix(path, "/")}).String() 205 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, escapedPath) 206 u, err = addOptions(u, opts) 207 if err != nil { 208 return nil, nil, nil, err 209 } 210 211 req, err := s.client.NewRequest("GET", u, nil) 212 if err != nil { 213 return nil, nil, nil, err 214 } 215 216 var rawJSON json.RawMessage 217 resp, err = s.client.Do(ctx, req, &rawJSON) 218 if err != nil { 219 return nil, nil, resp, err 220 } 221 222 fileUnmarshalError := json.Unmarshal(rawJSON, &fileContent) 223 if fileUnmarshalError == nil { 224 return fileContent, nil, resp, nil 225 } 226 227 directoryUnmarshalError := json.Unmarshal(rawJSON, &directoryContent) 228 if directoryUnmarshalError == nil { 229 return nil, directoryContent, resp, nil 230 } 231 232 return nil, nil, resp, fmt.Errorf("unmarshalling failed for both file and directory content: %s and %s", fileUnmarshalError, directoryUnmarshalError) 233 } 234 235 // CreateFile creates a new file in a repository at the given path and returns 236 // the commit and file metadata. 237 // 238 // GitHub API docs: https://docs.github.com/en/rest/repos/contents#create-or-update-file-contents 239 func (s *RepositoriesService) CreateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 240 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 241 req, err := s.client.NewRequest("PUT", u, opts) 242 if err != nil { 243 return nil, nil, err 244 } 245 246 createResponse := new(RepositoryContentResponse) 247 resp, err := s.client.Do(ctx, req, createResponse) 248 if err != nil { 249 return nil, resp, err 250 } 251 252 return createResponse, resp, nil 253 } 254 255 // UpdateFile updates a file in a repository at the given path and returns the 256 // commit and file metadata. Requires the blob SHA of the file being updated. 257 // 258 // GitHub API docs: https://docs.github.com/en/rest/repos/contents#create-or-update-file-contents 259 func (s *RepositoriesService) UpdateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 260 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 261 req, err := s.client.NewRequest("PUT", u, opts) 262 if err != nil { 263 return nil, nil, err 264 } 265 266 updateResponse := new(RepositoryContentResponse) 267 resp, err := s.client.Do(ctx, req, updateResponse) 268 if err != nil { 269 return nil, resp, err 270 } 271 272 return updateResponse, resp, nil 273 } 274 275 // DeleteFile deletes a file from a repository and returns the commit. 276 // Requires the blob SHA of the file to be deleted. 277 // 278 // GitHub API docs: https://docs.github.com/en/rest/repos/contents#delete-a-file 279 func (s *RepositoriesService) DeleteFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 280 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 281 req, err := s.client.NewRequest("DELETE", u, opts) 282 if err != nil { 283 return nil, nil, err 284 } 285 286 deleteResponse := new(RepositoryContentResponse) 287 resp, err := s.client.Do(ctx, req, deleteResponse) 288 if err != nil { 289 return nil, resp, err 290 } 291 292 return deleteResponse, resp, nil 293 } 294 295 // ArchiveFormat is used to define the archive type when calling GetArchiveLink. 296 type ArchiveFormat string 297 298 const ( 299 // Tarball specifies an archive in gzipped tar format. 300 Tarball ArchiveFormat = "tarball" 301 302 // Zipball specifies an archive in zip format. 303 Zipball ArchiveFormat = "zipball" 304 ) 305 306 // GetArchiveLink returns an URL to download a tarball or zipball archive for a 307 // repository. The archiveFormat can be specified by either the github.Tarball 308 // or github.Zipball constant. 309 // 310 // GitHub API docs: https://docs.github.com/en/rest/repos/contents/#get-archive-link 311 func (s *RepositoriesService) GetArchiveLink(ctx context.Context, owner, repo string, archiveformat ArchiveFormat, opts *RepositoryContentGetOptions, followRedirects bool) (*url.URL, *Response, error) { 312 u := fmt.Sprintf("repos/%s/%s/%s", owner, repo, archiveformat) 313 if opts != nil && opts.Ref != "" { 314 u += fmt.Sprintf("/%s", opts.Ref) 315 } 316 resp, err := s.client.roundTripWithOptionalFollowRedirect(ctx, u, followRedirects) 317 if err != nil { 318 return nil, nil, err 319 } 320 defer resp.Body.Close() 321 322 if resp.StatusCode != http.StatusFound { 323 return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status) 324 } 325 326 parsedURL, err := url.Parse(resp.Header.Get("Location")) 327 if err != nil { 328 return nil, newResponse(resp), err 329 } 330 331 return parsedURL, newResponse(resp), nil 332 }