// VerifyUpload calls the "verify" API link relation on obj if it exists func VerifyUpload(obj *ObjectResource) error { // Do we need to do verify? if _, ok := obj.Rel("verify"); !ok { return nil } req, err := obj.NewRequest("verify", "POST") if err != nil { return errutil.Error(err) } by, err := json.Marshal(obj) if err != nil { return errutil.Error(err) } req.Header.Set("Content-Type", MediaType) req.Header.Set("Content-Length", strconv.Itoa(len(by))) req.ContentLength = int64(len(by)) req.Body = ioutil.NopCloser(bytes.NewReader(by)) res, err := DoRequest(req, true) if err != nil { return err } httputil.LogTransfer("lfs.data.verify", res) io.Copy(ioutil.Discard, res.Body) res.Body.Close() return err }
// Batch calls the batch API and returns object results func Batch(objects []*ObjectResource, operation string, transferAdapters []string) (objs []*ObjectResource, transferAdapter string, e error) { if len(objects) == 0 { return nil, "", nil } o := &batchRequest{Operation: operation, Objects: objects, TransferAdapterNames: transferAdapters} by, err := json.Marshal(o) if err != nil { return nil, "", errutil.Error(err) } req, err := NewBatchRequest(operation) if err != nil { return nil, "", errutil.Error(err) } req.Header.Set("Content-Type", MediaType) req.Header.Set("Content-Length", strconv.Itoa(len(by))) req.ContentLength = int64(len(by)) req.Body = tools.NewReadSeekCloserWrapper(bytes.NewReader(by)) tracerx.Printf("api: batch %d files", len(objects)) res, bresp, err := DoBatchRequest(req) if err != nil { if res == nil { return nil, "", errutil.NewRetriableError(err) } if res.StatusCode == 0 { return nil, "", errutil.NewRetriableError(err) } if errutil.IsAuthError(err) { httputil.SetAuthType(req, res) return Batch(objects, operation, transferAdapters) } switch res.StatusCode { case 404, 410: tracerx.Printf("api: batch not implemented: %d", res.StatusCode) return nil, "", errutil.NewNotImplementedError(nil) } tracerx.Printf("api error: %s", err) return nil, "", errutil.Error(err) } httputil.LogTransfer("lfs.batch", res) if res.StatusCode != 200 { return nil, "", errutil.Error(fmt.Errorf("Invalid status for %s: %d", httputil.TraceHttpReq(req), res.StatusCode)) } return bresp.Objects, bresp.TransferAdapterName, nil }
// TODO LEGACY API: remove when legacy API removed func UploadCheck(oid string, size int64) (*ObjectResource, error) { reqObj := &ObjectResource{ Oid: oid, Size: size, } by, err := json.Marshal(reqObj) if err != nil { return nil, errutil.Error(err) } req, err := NewRequest("POST", oid) if err != nil { return nil, errutil.Error(err) } req.Header.Set("Content-Type", MediaType) req.Header.Set("Content-Length", strconv.Itoa(len(by))) req.ContentLength = int64(len(by)) req.Body = tools.NewReadSeekCloserWrapper(bytes.NewReader(by)) tracerx.Printf("api: uploading (%s)", oid) res, obj, err := DoLegacyRequest(req) if err != nil { if errutil.IsAuthError(err) { httputil.SetAuthType(req, res) return UploadCheck(oid, size) } return nil, errutil.NewRetriableError(err) } httputil.LogTransfer("lfs.upload", res) if res.StatusCode == 200 { return nil, nil } if obj.Oid == "" { obj.Oid = oid } if obj.Size == 0 { obj.Size = reqObj.Size } return obj, nil }
// Check the response from a HTTP request for problems func handleResponse(res *http.Response, creds auth.Creds) error { auth.SaveCredentials(creds, res) if res.StatusCode < 400 { return nil } defer func() { io.Copy(ioutil.Discard, res.Body) res.Body.Close() }() cliErr := &ClientError{} err := DecodeResponse(res, cliErr) if err == nil { if len(cliErr.Message) == 0 { err = defaultError(res) } else { err = errutil.Error(cliErr) } } if res.StatusCode == 401 { return errutil.NewAuthError(err) } if res.StatusCode > 499 && res.StatusCode != 501 && res.StatusCode != 509 { return errutil.NewFatalError(err) } return err }
// DoHttpRequestWithRedirects runs a HTTP request and responds to redirects func DoHttpRequestWithRedirects(req *http.Request, via []*http.Request, useCreds bool) (*http.Response, error) { var creds auth.Creds if useCreds { c, err := auth.GetCreds(req) if err != nil { return nil, err } creds = c } res, err := doHttpRequest(req, creds) if err != nil { return res, err } if res.StatusCode == 307 { redirectTo := res.Header.Get("Location") locurl, err := url.Parse(redirectTo) if err == nil && !locurl.IsAbs() { locurl = req.URL.ResolveReference(locurl) redirectTo = locurl.String() } redirectedReq, err := NewHttpRequest(req.Method, redirectTo, nil) if err != nil { return res, errutil.Errorf(err, err.Error()) } via = append(via, req) // Avoid seeking and re-wrapping the CountingReadCloser, just get the "real" body realBody := req.Body if wrappedBody, ok := req.Body.(*CountingReadCloser); ok { realBody = wrappedBody.ReadCloser } seeker, ok := realBody.(io.Seeker) if !ok { return res, errutil.Errorf(nil, "Request body needs to be an io.Seeker to handle redirects.") } if _, err := seeker.Seek(0, 0); err != nil { return res, errutil.Error(err) } redirectedReq.Body = realBody redirectedReq.ContentLength = req.ContentLength if err = CheckRedirect(redirectedReq, via); err != nil { return res, errutil.Errorf(err, err.Error()) } return DoHttpRequestWithRedirects(redirectedReq, via, useCreds) } return res, nil }
// TODO LEGACY API: remove when legacy API removed func DownloadCheck(oid string) (*ObjectResource, error) { req, err := NewRequest("GET", oid) if err != nil { return nil, errutil.Error(err) } res, obj, err := DoLegacyRequest(req) if err != nil { return nil, err } httputil.LogTransfer("lfs.download", res) _, err = obj.NewRequest("download", "GET") if err != nil { return nil, errutil.Error(err) } return obj, nil }
func defaultError(res *http.Response) error { 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 errutil.Error(fmt.Errorf(msgFmt, res.Request.URL)) }
// Internal http request management func doHttpRequest(req *http.Request, creds auth.Creds) (*http.Response, error) { var ( res *http.Response err error ) if config.Config.NtlmAccess(auth.GetOperationForRequest(req)) { res, err = doNTLMRequest(req, true) } else { res, err = NewHttpClient(config.Config, req.Host).Do(req) } if res == nil { res = &http.Response{ StatusCode: 0, Header: make(http.Header), Request: req, Body: ioutil.NopCloser(bytes.NewBufferString("")), } } if err != nil { if errutil.IsAuthError(err) { SetAuthType(req, res) doHttpRequest(req, creds) } else { err = errutil.Error(err) } } else { err = handleResponse(res, creds) } if err != nil { if res != nil { SetErrorResponseContext(err, res) } else { setErrorRequestContext(err, req) } } return res, err }
// getCreds gets the credentials for a HTTP request and sets the given // request's Authorization header with them using Basic Authentication. // 1. Check the URL for authentication. Ex: http://user:[email protected] // 2. Check netrc for authentication. // 3. Check the Git remote URL for authentication IF it's the same scheme and // host of the URL. // 4. Ask 'git credential' to fill in the password from one of the above URLs. // // This prefers the Git remote URL for checking credentials so that users only // have to enter their passwords once for Git and Git LFS. It uses the same // URL path that Git does, in case 'useHttpPath' is enabled in the Git config. func GetCreds(req *http.Request) (Creds, error) { if skipCredsCheck(req) { return nil, nil } credsUrl, err := getCredURLForAPI(req) if err != nil { return nil, errutil.Error(err) } if credsUrl == nil { return nil, nil } if setCredURLFromNetrc(req) { return nil, nil } return fillCredentials(req, credsUrl) }
func (a *basicUploadAdapter) DoTransfer(t *Transfer, cb TransferProgressCallback, authOkFunc func()) error { rel, ok := t.Object.Rel("upload") if !ok { return fmt.Errorf("No upload action for this object.") } req, err := httputil.NewHttpRequest("PUT", rel.Href, rel.Header) if err != nil { return err } if len(req.Header.Get("Content-Type")) == 0 { req.Header.Set("Content-Type", "application/octet-stream") } if req.Header.Get("Transfer-Encoding") == "chunked" { req.TransferEncoding = []string{"chunked"} } else { req.Header.Set("Content-Length", strconv.FormatInt(t.Object.Size, 10)) } req.ContentLength = t.Object.Size f, err := os.OpenFile(t.Path, os.O_RDONLY, 0644) if err != nil { return errutil.Error(err) } defer f.Close() // Ensure progress callbacks made while uploading // Wrap callback to give name context ccb := func(totalSize int64, readSoFar int64, readSinceLast int) error { if cb != nil { return cb(t.Name, totalSize, readSoFar, readSinceLast) } return nil } var reader io.Reader reader = &progress.CallbackReader{ C: ccb, TotalSize: t.Object.Size, Reader: f, } // Signal auth was ok on first read; this frees up other workers to start if authOkFunc != nil { reader = newStartCallbackReader(reader, func(*startCallbackReader) { authOkFunc() }) } req.Body = ioutil.NopCloser(reader) res, err := httputil.DoHttpRequest(req, true) if err != nil { return errutil.NewRetriableError(err) } httputil.LogTransfer("lfs.data.upload", res) // A status code of 403 likely means that an authentication token for the // upload has expired. This can be safely retried. if res.StatusCode == 403 { return errutil.NewRetriableError(err) } if res.StatusCode > 299 { return errutil.Errorf(nil, "Invalid status for %s: %d", httputil.TraceHttpReq(req), res.StatusCode) } io.Copy(ioutil.Discard, res.Body) res.Body.Close() return api.VerifyUpload(t.Object) }
func readLocalFile(writer io.Writer, ptr *Pointer, mediafile string, workingfile string, cb progress.CopyCallback) error { reader, err := os.Open(mediafile) if err != nil { return errutil.Errorf(err, "Error opening media file.") } defer reader.Close() if ptr.Size == 0 { if stat, _ := os.Stat(mediafile); stat != nil { ptr.Size = stat.Size() } } if len(ptr.Extensions) > 0 { registeredExts := config.Config.Extensions() extensions := make(map[string]config.Extension) for _, ptrExt := range ptr.Extensions { ext, ok := registeredExts[ptrExt.Name] if !ok { err := fmt.Errorf("Extension '%s' is not configured.", ptrExt.Name) return errutil.Error(err) } ext.Priority = ptrExt.Priority extensions[ext.Name] = ext } exts, err := config.SortExtensions(extensions) if err != nil { return errutil.Error(err) } // pipe extensions in reverse order var extsR []config.Extension for i := range exts { ext := exts[len(exts)-1-i] extsR = append(extsR, ext) } request := &pipeRequest{"smudge", reader, workingfile, extsR} response, err := pipeExtensions(request) if err != nil { return errutil.Error(err) } actualExts := make(map[string]*pipeExtResult) for _, result := range response.results { actualExts[result.name] = result } // verify name, order, and oids oid := response.results[0].oidIn if ptr.Oid != oid { err = fmt.Errorf("Actual oid %s during smudge does not match expected %s", oid, ptr.Oid) return errutil.Error(err) } for _, expected := range ptr.Extensions { actual := actualExts[expected.Name] if actual.name != expected.Name { err = fmt.Errorf("Actual extension name '%s' does not match expected '%s'", actual.name, expected.Name) return errutil.Error(err) } if actual.oidOut != expected.Oid { err = fmt.Errorf("Actual oid %s for extension '%s' does not match expected %s", actual.oidOut, expected.Name, expected.Oid) return errutil.Error(err) } } // setup reader reader, err = os.Open(response.file.Name()) if err != nil { return errutil.Errorf(err, "Error opening smudged file: %s", err) } defer reader.Close() } _, err = tools.CopyWithCallback(writer, reader, ptr.Size, cb) if err != nil { return errutil.Errorf(err, "Error reading from media file: %s", err) } return nil }