git-lfs/lfs/client.go

552 lines
13 KiB
Go
Raw Normal View History

2015-03-19 19:30:55 +00:00
package lfs
2013-10-04 17:09:03 +00:00
import (
2015-01-23 22:11:10 +00:00
"bytes"
"encoding/base64"
"encoding/json"
"errors"
2013-10-04 17:09:03 +00:00
"fmt"
"github.com/cheggaaa/pb"
2015-01-23 22:11:10 +00:00
"github.com/rubyist/tracerx"
"io"
"io/ioutil"
2014-04-16 14:30:13 +00:00
"mime"
2013-10-04 17:09:03 +00:00
"net/http"
"os"
"path/filepath"
"strings"
2013-10-04 17:09:03 +00:00
)
const (
2015-02-26 01:05:11 +00:00
// Legacy type
gitMediaType = "application/vnd.git-media"
// The main type, sub type, and suffix. Use this when ensuring the type from
// an HTTP response is correct.
gitMediaMetaTypePrefix = gitMediaType + "+json"
2015-02-26 01:05:11 +00:00
// Adds the extra mime params. Use this when sending the type in an HTTP
// request.
gitMediaMetaType = gitMediaMetaTypePrefix + "; charset=utf-8"
)
func Download(oidPath string) (io.ReadCloser, int64, *WrappedError) {
oid := filepath.Base(oidPath)
req, creds, err := request("GET", oid)
if err != nil {
return nil, 0, Error(err)
}
req.Header.Set("Accept", gitMediaType)
res, wErr := doHTTPWithCreds(req, creds)
if wErr != nil {
return nil, 0, wErr
}
contentType := res.Header.Get("Content-Type")
if contentType == "" {
wErr = Error(errors.New("Empty Content-Type"))
setErrorResponseContext(wErr, res)
return nil, 0, wErr
}
if strings.HasPrefix(contentType, gitMediaMetaTypePrefix) {
obj := &objectResource{}
err := json.NewDecoder(res.Body).Decode(obj)
res.Body.Close()
if err != nil {
wErr := Error(err)
setErrorResponseContext(wErr, res)
return nil, 0, wErr
}
dlReq, err := obj.NewRequest("download", "GET")
if err != nil {
wErr := Error(err)
setErrorResponseContext(wErr, res)
return nil, 0, wErr
}
dlCreds, err := setRequestHeaders(dlReq)
if err != nil {
return nil, 0, Errorf(err, "Error attempting to GET %s", oidPath)
}
dlRes, err := DoHTTP(Config, dlReq)
if err != nil {
wErr := Error(err)
setErrorResponseContext(wErr, res)
return nil, 0, wErr
}
saveCredentials(dlCreds, dlRes)
contentType := dlRes.Header.Get("Content-Type")
if contentType == "" {
wErr = Error(errors.New("Empty Content-Type"))
setErrorResponseContext(wErr, res)
return nil, 0, wErr
}
res = dlRes
}
ok, headerSize, wErr := validateMediaHeader(contentType, res.Body)
if !ok {
setErrorResponseContext(wErr, res)
return nil, 0, wErr
}
return res.Body, res.ContentLength - int64(headerSize), nil
}
func Upload(oidPath, filename string, cb CopyCallback) *WrappedError {
linkMeta, status, err := callPost(oidPath, filename)
if err != nil && status != 302 {
return Errorf(err, "Error starting file upload.")
}
oid := filepath.Base(oidPath)
switch status {
2015-02-13 20:37:19 +00:00
case 200: // object exists on the server
case 405, 302:
// Do the old style OPTIONS + PUT
2015-02-17 18:46:08 +00:00
status, wErr := callOptions(oidPath)
if wErr != nil {
return wErr
}
if status != 200 {
err = callPut(oidPath, filename, cb)
if err != nil {
return Errorf(err, "Error uploading file %s (%s)", filename, oid)
}
}
2015-02-13 20:37:19 +00:00
case 202:
// the server responded with hypermedia links to upload and verify the object.
err = callExternalPut(oidPath, filename, linkMeta, cb)
if err != nil {
return Errorf(err, "Error uploading file %s (%s)", filename, oid)
}
default:
return Errorf(err, "Unexpected HTTP response: %d", status)
}
return nil
}
2015-02-26 01:09:53 +00:00
type objectResource struct {
Oid string `json:"oid,omitempty"`
Size int64 `json:"size,omitempty"`
Links map[string]*linkRelation `json:"_links,omitempty"`
}
var objectRelationDoesNotExist = errors.New("relation does not exist")
func (o *objectResource) NewRequest(relation, method string) (*http.Request, error) {
rel, ok := o.Rel(relation)
if !ok {
return nil, objectRelationDoesNotExist
}
req, err := http.NewRequest(method, rel.Href, nil)
if err != nil {
return nil, err
}
for h, v := range rel.Header {
req.Header.Set(h, v)
}
return req, nil
}
2015-02-26 01:09:53 +00:00
func (o *objectResource) Rel(name string) (*linkRelation, bool) {
if o.Links == nil {
return nil, false
}
rel, ok := o.Links[name]
return rel, ok
}
type linkRelation struct {
Href string `json:"href"`
Header map[string]string `json:"header,omitempty"`
}
2015-02-17 18:46:08 +00:00
func callOptions(filehash string) (int, *WrappedError) {
oid := filepath.Base(filehash)
_, err := os.Stat(filehash)
if err != nil {
2015-02-17 18:46:08 +00:00
return 0, Errorf(err, "Internal object does not exist: %s", filehash)
}
2015-01-23 22:11:10 +00:00
tracerx.Printf("api_options: %s", oid)
req, creds, err := request("OPTIONS", oid)
if err != nil {
2015-02-17 18:46:08 +00:00
return 0, Errorf(err, "Unable to build OPTIONS request for %s", oid)
}
res, wErr := doHTTPWithCreds(req, creds)
if wErr != nil {
return 0, wErr
}
return res.StatusCode, nil
}
2015-02-17 18:46:08 +00:00
func callPut(filehash, filename string, cb CopyCallback) *WrappedError {
2014-03-12 14:55:01 +00:00
if filename == "" {
filename = filehash
}
oid := filepath.Base(filehash)
file, err := os.Open(filehash)
2013-10-31 19:22:33 +00:00
if err != nil {
2015-02-17 18:46:08 +00:00
return Errorf(err, "Internal object does not exist: %s", filehash)
2013-10-31 19:22:33 +00:00
}
defer file.Close()
2013-10-31 19:22:33 +00:00
stat, err := file.Stat()
2013-10-04 17:09:03 +00:00
if err != nil {
2015-02-17 18:46:08 +00:00
return Errorf(err, "Internal object does not exist: %s", filehash)
2013-10-04 17:09:03 +00:00
}
req, creds, err := request("PUT", oid)
2013-10-04 17:09:03 +00:00
if err != nil {
2015-02-17 18:46:08 +00:00
return Errorf(err, "Unable to build PUT request for %s", oid)
2013-10-04 17:09:03 +00:00
}
fileSize := stat.Size()
reader := &CallbackReader{
C: cb,
TotalSize: fileSize,
Reader: file,
}
bar := pb.StartNew(int(fileSize))
bar.SetUnits(pb.U_BYTES)
bar.Start()
req.Header.Set("Content-Type", gitMediaType)
req.Header.Set("Accept", gitMediaMetaType)
req.Body = ioutil.NopCloser(bar.NewProxyReader(reader))
req.ContentLength = fileSize
2013-10-04 17:09:03 +00:00
fmt.Printf("Sending %s\n", filename)
2015-01-23 22:11:10 +00:00
tracerx.Printf("api_put: %s %s", oid, filename)
_, wErr := doHTTPWithCreds(req, creds)
2013-10-04 17:09:03 +00:00
2015-02-17 18:46:08 +00:00
return wErr
2013-10-04 17:09:03 +00:00
}
2015-02-26 01:09:53 +00:00
func callExternalPut(filehash, filename string, obj *objectResource, cb CopyCallback) *WrappedError {
if obj == nil {
2015-02-17 18:46:08 +00:00
return Errorf(errors.New("No hypermedia links provided"),
"Error attempting to PUT %s", filename)
2015-02-13 20:37:19 +00:00
}
req, err := obj.NewRequest("upload", "PUT")
if err == objectRelationDoesNotExist {
2015-02-17 18:46:08 +00:00
return Errorf(errors.New("No upload link provided"),
"Error attempting to PUT %s", filename)
2015-01-23 22:11:10 +00:00
}
if err != nil {
return Errorf(err, "Error attempting to PUT %s", filename)
}
2015-01-23 22:11:10 +00:00
file, err := os.Open(filehash)
if err != nil {
2015-02-17 18:46:08 +00:00
return Errorf(err, "Error attempting to PUT %s", filename)
2015-01-23 22:11:10 +00:00
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
2015-02-17 18:46:08 +00:00
return Errorf(err, "Error attempting to PUT %s", filename)
2015-01-23 22:11:10 +00:00
}
fileSize := stat.Size()
reader := &CallbackReader{
2015-01-23 22:11:10 +00:00
C: cb,
TotalSize: fileSize,
Reader: file,
}
2015-02-17 19:19:12 +00:00
creds, err := setRequestHeaders(req)
if err != nil {
return Errorf(err, "Error attempting to PUT %s", filename)
}
2015-01-23 22:11:10 +00:00
bar := pb.StartNew(int(fileSize))
bar.SetUnits(pb.U_BYTES)
bar.Start()
req.Body = ioutil.NopCloser(bar.NewProxyReader(reader))
req.ContentLength = fileSize
tracerx.Printf("external_put: %s %s", filepath.Base(filehash), req.URL)
res, err := DoHTTP(Config, req)
2015-01-23 22:11:10 +00:00
if err != nil {
2015-02-17 18:46:08 +00:00
return Errorf(err, "Error attempting to PUT %s", filename)
2015-01-23 22:11:10 +00:00
}
saveCredentials(creds, res)
2015-01-23 22:11:10 +00:00
2015-02-13 20:37:19 +00:00
// Run the verify callback
verifyReq, err := obj.NewRequest("verify", "POST")
if err == objectRelationDoesNotExist {
return nil
}
if err != nil {
return Errorf(err, "Error attempting to verify %s", filename)
}
verifyCreds, err := setRequestHeaders(verifyReq)
if err != nil {
return Errorf(err, "Error attempting to verify %s", filename)
}
2015-01-23 22:11:10 +00:00
oid := filepath.Base(filehash)
d := fmt.Sprintf(`{"oid":"%s", "size":%d}`, oid, fileSize)
verifyReq.Body = ioutil.NopCloser(bytes.NewBufferString(d))
2015-01-23 22:11:10 +00:00
tracerx.Printf("verify: %s %s", oid, verifyReq.URL.String())
verifyRes, err := DoHTTP(Config, verifyReq)
if err != nil {
return Errorf(err, "Error attempting to verify %s", filename)
2015-01-23 22:11:10 +00:00
}
saveCredentials(verifyCreds, verifyRes)
2015-01-23 22:11:10 +00:00
return nil
}
2015-02-26 01:09:53 +00:00
func callPost(filehash, filename string) (*objectResource, int, *WrappedError) {
2015-01-23 22:11:10 +00:00
oid := filepath.Base(filehash)
req, creds, err := request("POST", "")
2015-01-23 22:11:10 +00:00
if err != nil {
2015-02-17 18:46:08 +00:00
return nil, 0, Errorf(err, "Error attempting to POST %s", filename)
2015-01-23 22:11:10 +00:00
}
file, err := os.Open(filehash)
if err != nil {
2015-02-17 18:46:08 +00:00
return nil, 0, Errorf(err, "Error attempting to POST %s", filename)
2015-01-23 22:11:10 +00:00
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
2015-02-17 18:46:08 +00:00
return nil, 0, Errorf(err, "Error attempting to POST %s", filename)
2015-01-23 22:11:10 +00:00
}
fileSize := stat.Size()
d := fmt.Sprintf(`{"oid":"%s", "size":%d}`, oid, fileSize)
req.Body = ioutil.NopCloser(bytes.NewBufferString(d))
req.Header.Set("Accept", gitMediaMetaType)
tracerx.Printf("api_post: %s %s", oid, filename)
res, wErr := doHTTPWithCreds(req, creds)
2015-01-23 22:11:10 +00:00
if wErr != nil {
return nil, 0, wErr
}
2015-02-13 20:37:19 +00:00
if res.StatusCode == 202 {
2015-02-26 01:09:53 +00:00
obj := &objectResource{}
err := json.NewDecoder(res.Body).Decode(obj)
2015-01-23 22:11:10 +00:00
if err != nil {
return nil, res.StatusCode, Errorf(err, "Error decoding JSON from %s %s.", req.Method, req.URL)
2015-01-23 22:11:10 +00:00
}
2015-02-26 01:09:53 +00:00
return obj, res.StatusCode, nil
2015-01-23 22:11:10 +00:00
}
return nil, res.StatusCode, nil
}
func validateMediaHeader(contentType string, reader io.Reader) (bool, int, *WrappedError) {
mediaType, params, err := mime.ParseMediaType(contentType)
var headerSize int
if err != nil {
return false, headerSize, Errorf(err, "Invalid Media Type: %s", contentType)
}
2015-01-23 22:11:10 +00:00
if mediaType == gitMediaType {
2015-01-23 22:11:10 +00:00
givenHeader, ok := params["header"]
if !ok {
return false, headerSize, Error(fmt.Errorf("Missing Git Media header in %s", contentType))
2015-01-23 22:11:10 +00:00
}
2015-01-23 22:11:10 +00:00
fullGivenHeader := "--" + givenHeader + "\n"
headerSize = len(fullGivenHeader)
header := make([]byte, headerSize)
2015-01-23 22:11:10 +00:00
_, err = io.ReadAtLeast(reader, header, len(fullGivenHeader))
if err != nil {
return false, headerSize, Errorf(err, "Error reading response body.")
2015-01-23 22:11:10 +00:00
}
2015-01-23 22:11:10 +00:00
if string(header) != fullGivenHeader {
return false, headerSize, Error(fmt.Errorf("Invalid header: %s expected, got %s", fullGivenHeader, header))
2015-01-23 22:11:10 +00:00
}
}
return true, headerSize, nil
}
// Wraps DoHTTP(), and saves or removes credentials from the git credential
// store based on the response.
func doHTTPWithCreds(req *http.Request, creds Creds) (*http.Response, *WrappedError) {
res, err := DoHTTP(Config, req)
var wErr *WrappedError
2014-08-08 17:31:33 +00:00
if err == RedirectError {
err = nil
}
2013-11-02 00:23:37 +00:00
if err == nil {
2015-02-17 18:46:08 +00:00
if creds != nil {
saveCredentials(creds, res)
2013-11-02 00:23:37 +00:00
}
2015-02-17 18:46:08 +00:00
wErr = handleResponseError(res)
} else if res.StatusCode != 302 { // hack for pre-release
wErr = Errorf(err, "Error sending HTTP request to %s", req.URL.String())
2014-08-08 17:31:33 +00:00
}
2014-08-08 17:31:33 +00:00
if wErr != nil {
if res != nil {
setErrorResponseContext(wErr, res)
} else {
setErrorRequestContext(wErr, req)
}
2013-11-02 00:23:37 +00:00
}
2014-08-08 17:31:33 +00:00
return res, wErr
}
2015-02-17 18:46:08 +00:00
func handleResponseError(res *http.Response) *WrappedError {
if res.StatusCode < 400 || res.StatusCode == 405 {
return nil
}
var wErr *WrappedError
apiErr := &ClientError{}
dec := json.NewDecoder(res.Body)
if err := dec.Decode(apiErr); err != nil {
wErr = Errorf(err, "Error decoding JSON from response")
} else {
var msg string
switch res.StatusCode {
case 401, 403:
msg = fmt.Sprintf("Authorization error: %s\nCheck that you have proper access to the repository.", res.Request.URL)
case 404:
msg = fmt.Sprintf("Repository not found: %s\nCheck that it exists and that you have proper access to it.", res.Request.URL)
default:
msg = fmt.Sprintf("Invalid response: %d", res.StatusCode)
}
wErr = Errorf(apiErr, msg)
}
if res.StatusCode < 500 {
wErr.Panic = false
}
return wErr
}
func saveCredentials(creds Creds, res *http.Response) {
if creds == nil {
return
}
2015-02-17 18:46:08 +00:00
if res.StatusCode < 300 {
execCreds(creds, "approve")
return
}
2015-03-19 17:13:58 +00:00
if res.StatusCode < 404 {
2015-02-17 18:46:08 +00:00
execCreds(creds, "reject")
}
}
2014-08-08 20:02:44 +00:00
var hiddenHeaders = map[string]bool{
"Authorization": true,
}
func setErrorRequestContext(err *WrappedError, req *http.Request) {
err.Set("Endpoint", Config.Endpoint())
2014-08-08 20:02:44 +00:00
err.Set("URL", fmt.Sprintf("%s %s", req.Method, req.URL.String()))
setErrorHeaderContext(err, "Response", req.Header)
2014-08-08 17:31:33 +00:00
}
func setErrorResponseContext(err *WrappedError, res *http.Response) {
2014-08-08 17:31:33 +00:00
err.Set("Status", res.Status)
2014-08-08 20:02:44 +00:00
setErrorHeaderContext(err, "Request", res.Header)
2014-08-08 17:31:33 +00:00
setErrorRequestContext(err, res.Request)
2013-11-02 00:23:37 +00:00
}
func setErrorHeaderContext(err *WrappedError, prefix string, head http.Header) {
2014-08-08 20:02:44 +00:00
for key, _ := range head {
contextKey := fmt.Sprintf("%s:%s", prefix, key)
if _, skip := hiddenHeaders[key]; skip {
err.Set(contextKey, "--")
} else {
err.Set(contextKey, head.Get(key))
}
}
}
func request(method, oid string) (*http.Request, Creds, error) {
u := Config.ObjectUrl(oid)
req, err := http.NewRequest(method, u.String(), nil)
2015-02-17 19:16:59 +00:00
if err != nil {
return req, nil, err
}
2015-02-17 19:19:12 +00:00
creds, err := setRequestHeaders(req)
2015-02-17 19:16:59 +00:00
return req, creds, err
2013-10-31 21:33:57 +00:00
}
2015-02-17 19:19:12 +00:00
func setRequestHeaders(req *http.Request) (Creds, error) {
req.Header.Set("User-Agent", UserAgent)
if _, ok := req.Header["Authorization"]; ok {
return nil, nil
}
creds, err := credentials(req.URL)
if err != nil {
return nil, err
}
token := fmt.Sprintf("%s:%s", creds["username"], creds["password"])
auth := "Basic " + base64.URLEncoding.EncodeToString([]byte(token))
req.Header.Set("Authorization", auth)
return creds, nil
}
type ClientError struct {
Message string `json:"message"`
DocumentationUrl string `json:"documentation_url,omitempty"`
RequestId string `json:"request_id,omitempty"`
}
func (e *ClientError) Error() string {
msg := e.Message
if len(e.DocumentationUrl) > 0 {
msg += "\nDocs: " + e.DocumentationUrl
}
if len(e.RequestId) > 0 {
msg += "\nRequest ID: " + e.RequestId
}
return msg
}