git-lfs/lfs/client.go

485 lines
11 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-03-20 01:55:40 +00:00
"bytes"
"encoding/base64"
2015-03-19 23:20:39 +00:00
"encoding/json"
"errors"
2013-10-04 17:09:03 +00:00
"fmt"
2015-03-20 17:55:24 +00:00
"github.com/rubyist/tracerx"
"io"
2015-03-19 23:20:39 +00:00
"io/ioutil"
2013-10-04 17:09:03 +00:00
"net/http"
2015-03-20 01:55:40 +00:00
"os"
"path/filepath"
2015-03-19 23:20:39 +00:00
"regexp"
2015-03-20 01:55:40 +00:00
"strconv"
2013-10-04 17:09:03 +00:00
)
const (
2015-03-19 21:16:52 +00:00
mediaType = "application/vnd.git-lfs+json; charset-utf-8"
)
// The apiEvent* statuses (and the apiEvent channel) are used by
// UploadQueue to know when it is OK to process uploads concurrently.
const (
apiEventFail = iota
apiEventSuccess
)
2015-03-19 21:16:52 +00:00
var (
2015-03-20 17:10:38 +00:00
lfsMediaTypeRE = regexp.MustCompile(`\Aapplication/vnd\.git\-lfs\+json(;|\z)`)
2015-03-22 18:32:22 +00:00
jsonMediaTypeRE = regexp.MustCompile(`\Aapplication/json(;|\z)`)
2015-03-19 21:16:52 +00:00
objectRelationDoesNotExist = errors.New("relation does not exist")
hiddenHeaders = map[string]bool{
"Authorization": true,
}
2015-03-19 23:20:39 +00:00
defaultErrors = map[int]string{
400: "Client error: %s",
401: "Authorization error: %s\nCheck that you have proper access to the repository",
2015-03-27 15:53:02 +00:00
403: "Authorization error: %s\nCheck that you have proper access to the repository",
2015-03-19 23:20:39 +00:00
404: "Repository or object not found: %s\nCheck that it exists and that you have proper access to it",
500: "Server error: %s",
}
apiEvent = make(chan int)
2015-03-19 21:16:52 +00:00
)
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"`
}
2015-03-20 01:55:40 +00:00
func (o *objectResource) NewRequest(relation, method string) (*http.Request, Creds, error) {
rel, ok := o.Rel(relation)
if !ok {
2015-03-20 01:55:40 +00:00
return nil, nil, objectRelationDoesNotExist
}
2015-03-20 01:55:40 +00:00
req, creds, err := newClientRequest(method, rel.Href)
if err != nil {
2015-03-20 01:55:40 +00:00
return nil, nil, err
}
for h, v := range rel.Header {
req.Header.Set(h, v)
}
2015-03-20 01:55:40 +00:00
return req, creds, 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-03-19 21:16:52 +00:00
type ClientError struct {
Message string `json:"message"`
DocumentationUrl string `json:"documentation_url,omitempty"`
RequestId string `json:"request_id,omitempty"`
}
2015-03-19 21:16:52 +00:00
func (e *ClientError) Error() string {
msg := e.Message
if len(e.DocumentationUrl) > 0 {
msg += "\nDocs: " + e.DocumentationUrl
2013-10-04 17:09:03 +00:00
}
2015-03-19 21:16:52 +00:00
if len(e.RequestId) > 0 {
msg += "\nRequest ID: " + e.RequestId
}
2015-03-19 21:16:52 +00:00
return msg
2013-10-04 17:09:03 +00:00
}
2015-03-19 21:16:52 +00:00
func Download(oid string) (io.ReadCloser, int64, *WrappedError) {
2015-03-20 01:55:40 +00:00
req, creds, err := newApiRequest("GET", oid)
if err != nil {
return nil, 0, Error(err)
}
res, obj, wErr := doApiRequest(req, creds)
if wErr != nil {
return nil, 0, wErr
}
req, creds, err = obj.NewRequest("download", "GET")
if err != nil {
return nil, 0, Error(err)
}
res, wErr = doHttpRequest(req, creds)
if wErr != nil {
return nil, 0, wErr
}
return res.Body, res.ContentLength, nil
2015-03-19 21:16:52 +00:00
}
2015-01-23 22:11:10 +00:00
2015-03-26 18:46:33 +00:00
type byteCloser struct {
*bytes.Reader
}
func (b *byteCloser) Close() error {
return nil
}
2015-03-20 01:55:40 +00:00
func Upload(oidPath, filename string, cb CopyCallback) *WrappedError {
oid := filepath.Base(oidPath)
file, err := os.Open(oidPath)
if err != nil {
return Error(err)
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return Error(err)
}
reqObj := &objectResource{
Oid: oid,
Size: stat.Size(),
}
by, err := json.Marshal(reqObj)
if err != nil {
return Error(err)
}
req, creds, err := newApiRequest("POST", oid)
2015-03-20 01:55:40 +00:00
if err != nil {
return Error(err)
}
req.Header.Set("Content-Type", mediaType)
req.Header.Set("Content-Length", strconv.Itoa(len(by)))
req.ContentLength = int64(len(by))
2015-03-26 18:46:33 +00:00
req.Body = &byteCloser{bytes.NewReader(by)}
2015-03-20 01:55:40 +00:00
2015-03-20 17:55:24 +00:00
tracerx.Printf("api: uploading %s (%s)", filename, oid)
2015-03-20 01:55:40 +00:00
res, obj, wErr := doApiRequest(req, creds)
if wErr != nil {
sendApiEvent(apiEventFail)
2015-03-20 01:55:40 +00:00
return wErr
}
sendApiEvent(apiEventSuccess)
reader := &CallbackReader{
C: cb,
TotalSize: reqObj.Size,
Reader: file,
}
2015-03-21 17:28:33 +00:00
if res.StatusCode == 200 {
// Drain the reader to update any progress bars
io.Copy(ioutil.Discard, reader)
return nil
}
2015-03-20 01:55:40 +00:00
req, creds, err = obj.NewRequest("upload", "PUT")
if err != nil {
return Error(err)
}
if len(req.Header.Get("Content-Type")) == 0 {
req.Header.Set("Content-Type", "application/octet-stream")
}
req.Header.Set("Content-Length", strconv.FormatInt(reqObj.Size, 10))
2015-03-21 17:28:33 +00:00
req.ContentLength = reqObj.Size
2015-03-20 18:01:43 +00:00
req.Body = ioutil.NopCloser(reader)
2015-03-20 01:55:40 +00:00
res, wErr = doHttpRequest(req, creds)
if wErr != nil {
return wErr
}
if res.StatusCode > 299 {
return Errorf(nil, "Invalid status for %s %s: %d", req.Method, req.URL, res.StatusCode)
}
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
2015-03-20 01:55:40 +00:00
req, creds, err = obj.NewRequest("verify", "POST")
if err == objectRelationDoesNotExist {
return nil
} else if err != nil {
return Error(err)
}
req.Header.Set("Content-Type", mediaType)
req.Header.Set("Content-Length", strconv.Itoa(len(by)))
req.ContentLength = int64(len(by))
2015-03-20 01:55:40 +00:00
req.Body = ioutil.NopCloser(bytes.NewReader(by))
res, wErr = doHttpRequest(req, creds)
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
2015-03-20 01:55:40 +00:00
return wErr
2015-01-23 22:11:10 +00:00
}
2015-03-20 01:55:40 +00:00
func doHttpRequest(req *http.Request, creds Creds) (*http.Response, *WrappedError) {
2015-03-19 23:20:39 +00:00
res, err := DoHTTP(Config, req)
var wErr *WrappedError
if err != nil {
wErr = Errorf(err, "Error for %s %s", res.Request.Method, res.Request.URL)
} else {
if creds != nil {
saveCredentials(creds, res)
}
wErr = handleResponse(res)
}
if wErr != nil {
if res != nil {
setErrorResponseContext(wErr, res)
} else {
setErrorRequestContext(wErr, req)
}
}
return res, wErr
}
2015-03-26 18:46:33 +00:00
func doApiRequestWithRedirects(req *http.Request, creds Creds, via []*http.Request) (*http.Response, *WrappedError) {
2015-03-20 01:55:40 +00:00
res, wErr := doHttpRequest(req, creds)
2015-03-26 18:46:33 +00:00
if wErr != nil {
2015-05-11 14:42:23 +00:00
return nil, wErr
2015-03-26 18:46:33 +00:00
}
if res.StatusCode == 307 {
redirectedReq, redirectedCreds, err := newClientRequest(req.Method, res.Header.Get("Location"))
if err != nil {
2015-05-11 14:42:23 +00:00
return nil, Errorf(err, err.Error())
2015-03-26 18:46:33 +00:00
}
via = append(via, req)
2015-05-11 14:42:31 +00:00
seeker, ok := req.Body.(io.Seeker)
if !ok {
2015-05-11 14:42:23 +00:00
return nil, Errorf(nil, "Request body needs to be an io.Seeker to handle redirects.")
2015-03-26 18:46:33 +00:00
}
2015-05-11 14:42:31 +00:00
if _, err := seeker.Seek(0, 0); err != nil {
return nil, Error(err)
}
redirectedReq.Body = req.Body
redirectedReq.ContentLength = req.ContentLength
2015-03-26 18:46:33 +00:00
if err = checkRedirect(redirectedReq, via); err != nil {
2015-05-11 14:42:23 +00:00
return nil, Errorf(err, err.Error())
2015-03-26 18:46:33 +00:00
}
return doApiRequestWithRedirects(redirectedReq, redirectedCreds, via)
}
2015-05-11 14:42:23 +00:00
return res, nil
2015-03-26 18:46:33 +00:00
}
func doApiRequest(req *http.Request, creds Creds) (*http.Response, *objectResource, *WrappedError) {
via := make([]*http.Request, 0, 4)
res, wErr := doApiRequestWithRedirects(req, creds, via)
2015-03-20 01:55:40 +00:00
if wErr != nil {
2015-05-11 14:41:24 +00:00
return nil, nil, wErr
2015-03-20 01:55:40 +00:00
}
obj := &objectResource{}
wErr = decodeApiResponse(res, obj)
if wErr != nil {
setErrorResponseContext(wErr, res)
2015-05-11 14:41:24 +00:00
return nil, nil, wErr
2015-03-20 01:55:40 +00:00
}
2015-05-11 14:41:24 +00:00
return res, obj, nil
2015-03-20 01:55:40 +00:00
}
2015-03-19 23:20:39 +00:00
func handleResponse(res *http.Response) *WrappedError {
if res.StatusCode < 400 {
return nil
}
defer func() {
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
}()
2015-03-20 01:55:40 +00:00
cliErr := &ClientError{}
wErr := decodeApiResponse(res, cliErr)
if wErr == nil {
if len(cliErr.Message) == 0 {
wErr = defaultError(res)
} else {
wErr = Error(cliErr)
2015-03-19 23:20:39 +00:00
}
}
wErr.Panic = res.StatusCode > 499 && res.StatusCode != 501 && res.StatusCode != 509
return wErr
}
2015-03-20 01:55:40 +00:00
func decodeApiResponse(res *http.Response, obj interface{}) *WrappedError {
2015-03-20 17:10:38 +00:00
ctype := res.Header.Get("Content-Type")
2015-03-22 18:32:22 +00:00
if !(lfsMediaTypeRE.MatchString(ctype) || jsonMediaTypeRE.MatchString(ctype)) {
2015-03-20 01:55:40 +00:00
return nil
}
err := json.NewDecoder(res.Body).Decode(obj)
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
2015-03-20 01:55:40 +00:00
if err != nil {
return Errorf(err, "Unable to parse HTTP response for %s %s", res.Request.Method, res.Request.URL)
}
return nil
}
2015-03-19 23:20:39 +00:00
func defaultError(res *http.Response) *WrappedError {
var msgFmt string
if f, ok := defaultErrors[res.StatusCode]; ok {
msgFmt = f
} else if res.StatusCode < 500 {
msgFmt = defaultErrors[400] + fmt.Sprintf(" from HTTP %d", res.StatusCode)
} else {
msgFmt = defaultErrors[500] + fmt.Sprintf(" from HTTP %d", res.StatusCode)
}
return Error(fmt.Errorf(msgFmt, res.Request.URL))
}
func saveCredentials(creds Creds, res *http.Response) {
if creds == nil {
return
}
if res.StatusCode < 300 {
execCreds(creds, "approve")
} else if res.StatusCode == 401 {
execCreds(creds, "reject")
}
}
2015-03-19 21:16:52 +00:00
func newApiRequest(method, oid string) (*http.Request, Creds, error) {
endpoint := Config.Endpoint()
objectOid := oid
operation := "download"
if method == "POST" {
objectOid = ""
operation = "upload"
}
res, err := sshAuthenticate(endpoint, operation, oid)
if err != nil {
tracerx.Printf("ssh: attempted with %s. Error: %s",
endpoint.SshUserAndHost, err.Error(),
)
}
if len(res.Href) > 0 {
endpoint.Url = res.Href
}
u, err := ObjectUrl(endpoint, objectOid)
2015-01-23 22:11:10 +00:00
if err != nil {
2015-03-19 21:16:52 +00:00
return nil, nil, err
2015-01-23 22:11:10 +00:00
}
2015-03-19 21:16:52 +00:00
req, creds, err := newClientRequest(method, u.String())
2015-05-11 14:42:34 +00:00
if err != nil {
return nil, nil, err
}
req.Header.Set("Accept", mediaType)
if res.Header != nil {
for key, value := range res.Header {
req.Header.Set(key, value)
}
2015-01-23 22:11:10 +00:00
}
2015-05-11 14:42:34 +00:00
return req, creds, nil
2015-01-23 22:11:10 +00:00
}
2015-03-19 21:16:52 +00:00
func newClientRequest(method, rawurl string) (*http.Request, Creds, error) {
req, err := http.NewRequest(method, rawurl, nil)
if err != nil {
2015-03-19 21:16:52 +00:00
return req, nil, err
}
2015-03-19 21:16:52 +00:00
req.Header.Set("User-Agent", UserAgent)
creds, err := getCreds(req)
return req, creds, err
}
2015-03-19 21:16:52 +00:00
func getCreds(req *http.Request) (Creds, error) {
if len(req.Header.Get("Authorization")) > 0 {
return nil, nil
2013-11-02 00:23:37 +00:00
}
2015-03-19 21:16:52 +00:00
apiUrl, err := Config.ObjectUrl("")
if err != nil {
return nil, err
2015-02-17 18:46:08 +00:00
}
2015-03-19 21:16:52 +00:00
if req.URL.Scheme == apiUrl.Scheme &&
req.URL.Host == apiUrl.Host {
creds, err := credentials(req.URL)
if err != nil {
return nil, err
2015-02-17 18:46:08 +00:00
}
2015-03-19 21:16:52 +00:00
token := fmt.Sprintf("%s:%s", creds["username"], creds["password"])
auth := "Basic " + base64.URLEncoding.EncodeToString([]byte(token))
req.Header.Set("Authorization", auth)
return creds, nil
2015-02-17 18:46:08 +00:00
}
2015-03-19 21:16:52 +00:00
return nil, nil
2014-08-08 20:02:44 +00:00
}
func setErrorRequestContext(err *WrappedError, req *http.Request) {
err.Set("Endpoint", Config.Endpoint().Url)
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 sendApiEvent(event int) {
select {
case apiEvent <- event:
default:
}
}