github.com/google/go-github/v49@v49.1.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 // GitHub API docs: https://docs.github.com/en/rest/repos/contents#get-repository-content 196 func (s *RepositoriesService) GetContents(ctx context.Context, owner, repo, path string, opts *RepositoryContentGetOptions) (fileContent *RepositoryContent, directoryContent []*RepositoryContent, resp *Response, err error) { 197 escapedPath := (&url.URL{Path: strings.TrimSuffix(path, "/")}).String() 198 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, escapedPath) 199 u, err = addOptions(u, opts) 200 if err != nil { 201 return nil, nil, nil, err 202 } 203 204 req, err := s.client.NewRequest("GET", u, nil) 205 if err != nil { 206 return nil, nil, nil, err 207 } 208 209 var rawJSON json.RawMessage 210 resp, err = s.client.Do(ctx, req, &rawJSON) 211 if err != nil { 212 return nil, nil, resp, err 213 } 214 215 fileUnmarshalError := json.Unmarshal(rawJSON, &fileContent) 216 if fileUnmarshalError == nil { 217 return fileContent, nil, resp, nil 218 } 219 220 directoryUnmarshalError := json.Unmarshal(rawJSON, &directoryContent) 221 if directoryUnmarshalError == nil { 222 return nil, directoryContent, resp, nil 223 } 224 225 return nil, nil, resp, fmt.Errorf("unmarshalling failed for both file and directory content: %s and %s", fileUnmarshalError, directoryUnmarshalError) 226 } 227 228 // CreateFile creates a new file in a repository at the given path and returns 229 // the commit and file metadata. 230 // 231 // GitHub API docs: https://docs.github.com/en/rest/repos/contents#create-or-update-file-contents 232 func (s *RepositoriesService) CreateFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 233 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 234 req, err := s.client.NewRequest("PUT", u, opts) 235 if err != nil { 236 return nil, nil, err 237 } 238 239 createResponse := new(RepositoryContentResponse) 240 resp, err := s.client.Do(ctx, req, createResponse) 241 if err != nil { 242 return nil, resp, err 243 } 244 245 return createResponse, resp, nil 246 } 247 248 // UpdateFile updates a file in a repository at the given path and returns the 249 // commit and file metadata. Requires the blob SHA of the file being updated. 250 // 251 // GitHub API docs: https://docs.github.com/en/rest/repos/contents#create-or-update-file-contents 252 func (s *RepositoriesService) UpdateFile(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("PUT", u, opts) 255 if err != nil { 256 return nil, nil, err 257 } 258 259 updateResponse := new(RepositoryContentResponse) 260 resp, err := s.client.Do(ctx, req, updateResponse) 261 if err != nil { 262 return nil, resp, err 263 } 264 265 return updateResponse, resp, nil 266 } 267 268 // DeleteFile deletes a file from a repository and returns the commit. 269 // Requires the blob SHA of the file to be deleted. 270 // 271 // GitHub API docs: https://docs.github.com/en/rest/repos/contents#delete-a-file 272 func (s *RepositoriesService) DeleteFile(ctx context.Context, owner, repo, path string, opts *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) { 273 u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path) 274 req, err := s.client.NewRequest("DELETE", u, opts) 275 if err != nil { 276 return nil, nil, err 277 } 278 279 deleteResponse := new(RepositoryContentResponse) 280 resp, err := s.client.Do(ctx, req, deleteResponse) 281 if err != nil { 282 return nil, resp, err 283 } 284 285 return deleteResponse, resp, nil 286 } 287 288 // ArchiveFormat is used to define the archive type when calling GetArchiveLink. 289 type ArchiveFormat string 290 291 const ( 292 // Tarball specifies an archive in gzipped tar format. 293 Tarball ArchiveFormat = "tarball" 294 295 // Zipball specifies an archive in zip format. 296 Zipball ArchiveFormat = "zipball" 297 ) 298 299 // GetArchiveLink returns an URL to download a tarball or zipball archive for a 300 // repository. The archiveFormat can be specified by either the github.Tarball 301 // or github.Zipball constant. 302 // 303 // GitHub API docs: https://docs.github.com/en/rest/repos/contents/#get-archive-link 304 func (s *RepositoriesService) GetArchiveLink(ctx context.Context, owner, repo string, archiveformat ArchiveFormat, opts *RepositoryContentGetOptions, followRedirects bool) (*url.URL, *Response, error) { 305 u := fmt.Sprintf("repos/%s/%s/%s", owner, repo, archiveformat) 306 if opts != nil && opts.Ref != "" { 307 u += fmt.Sprintf("/%s", opts.Ref) 308 } 309 resp, err := s.client.roundTripWithOptionalFollowRedirect(ctx, u, followRedirects) 310 if err != nil { 311 return nil, nil, err 312 } 313 defer resp.Body.Close() 314 315 if resp.StatusCode != http.StatusFound { 316 return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status) 317 } 318 319 parsedURL, err := url.Parse(resp.Header.Get("Location")) 320 if err != nil { 321 return nil, newResponse(resp), err 322 } 323 324 return parsedURL, newResponse(resp), nil 325 }