func (a *customAdapter) DoTransfer(ctx interface{}, t *Transfer, cb TransferProgressCallback, authOkFunc func()) error { if ctx == nil { return fmt.Errorf("Custom transfer %q was not properly initialized, see previous errors", a.name) } customCtx, ok := ctx.(*customAdapterWorkerContext) if !ok { return fmt.Errorf("Context object for custom transfer %q was of the wrong type", a.name) } var authCalled bool rel, ok := t.Object.Rel(a.getOperationName()) if !ok { return errors.New("Object not found on the server.") } var req *customAdapterTransferRequest if a.direction == Upload { req = NewCustomAdapterUploadRequest(t.Object.Oid, t.Object.Size, t.Path, rel) } else { req = NewCustomAdapterDownloadRequest(t.Object.Oid, t.Object.Size, rel) } err := a.sendMessage(customCtx, req) if err != nil { return err } // 1..N replies (including progress & one of download / upload) var complete bool for !complete { resp, err := a.readResponse(customCtx) if err != nil { return err } var wasAuthOk bool switch resp.Event { case "progress": // Progress if resp.Oid != t.Object.Oid { return fmt.Errorf("Unexpected oid %q in response, expecting %q", resp.Oid, t.Object.Oid) } if cb != nil { cb(t.Name, t.Object.Size, resp.BytesSoFar, resp.BytesSinceLast) } wasAuthOk = resp.BytesSoFar > 0 case "complete": // Download/Upload complete if resp.Oid != t.Object.Oid { return fmt.Errorf("Unexpected oid %q in response, expecting %q", resp.Oid, t.Object.Oid) } if resp.Error != nil { return fmt.Errorf("Error transferring %q: %v", t.Object.Oid, resp.Error) } if a.direction == Download { // So we don't have to blindly trust external providers, check SHA if err = tools.VerifyFileHash(t.Object.Oid, resp.Path); err != nil { return fmt.Errorf("Downloaded file failed checks: %v", err) } // Move file to final location if err = tools.RenameFileCopyPermissions(resp.Path, t.Path); err != nil { return fmt.Errorf("Failed to copy downloaded file: %v", err) } } else if a.direction == Upload { if err = api.VerifyUpload(config.Config, t.Object); err != nil { return err } } wasAuthOk = true complete = true default: return fmt.Errorf("Invalid message %q from custom adapter %q", resp.Event, a.name) } // Fall through from both progress and completion messages // Call auth on first progress or success to free up other workers if wasAuthOk && authOkFunc != nil && !authCalled { authOkFunc() authCalled = true } } return nil }
func (a *tusUploadAdapter) DoTransfer(ctx interface{}, t *Transfer, cb TransferProgressCallback, authOkFunc func()) error { rel, ok := t.Object.Rel("upload") if !ok { return fmt.Errorf("No upload action for this object.") } // Note not supporting the Creation extension since the batch API generates URLs // Also not supporting Concatenation to support parallel uploads of chunks; forward only // 1. Send HEAD request to determine upload start point // Request must include Tus-Resumable header (version) tracerx.Printf("xfer: sending tus.io HEAD request for %q", t.Object.Oid) req, err := httputil.NewHttpRequest("HEAD", rel.Href, rel.Header) if err != nil { return err } req.Header.Set("Tus-Resumable", TusVersion) res, err := httputil.DoHttpRequest(config.Config, req, false) if err != nil { return errors.NewRetriableError(err) } // Response will contain Upload-Offset if supported offHdr := res.Header.Get("Upload-Offset") if len(offHdr) == 0 { return fmt.Errorf("Missing Upload-Offset header from tus.io HEAD response at %q, contact server admin", rel.Href) } offset, err := strconv.ParseInt(offHdr, 10, 64) if err != nil || offset < 0 { return fmt.Errorf("Invalid Upload-Offset value %q in response from tus.io HEAD at %q, contact server admin", offHdr, rel.Href) } // Upload-Offset=size means already completed (skip) // Batch API will probably already detect this, but handle just in case if offset >= t.Object.Size { tracerx.Printf("xfer: tus.io HEAD offset %d indicates %q is already fully uploaded, skipping", offset, t.Object.Oid) advanceCallbackProgress(cb, t, t.Object.Size) return nil } // Open file for uploading f, err := os.OpenFile(t.Path, os.O_RDONLY, 0644) if err != nil { return errors.Wrap(err, "tus upload") } defer f.Close() // Upload-Offset=0 means start from scratch, but still send PATCH if offset == 0 { tracerx.Printf("xfer: tus.io uploading %q from start", t.Object.Oid) } else { tracerx.Printf("xfer: tus.io resuming upload %q from %d", t.Object.Oid, offset) advanceCallbackProgress(cb, t, offset) _, err := f.Seek(offset, os.SEEK_CUR) if err != nil { return errors.Wrap(err, "tus upload") } } // 2. Send PATCH request with byte start point (even if 0) in Upload-Offset // Response status must be 204 // Response Upload-Offset must be request Upload-Offset plus sent bytes // Response may include Upload-Expires header in which case check not passed tracerx.Printf("xfer: sending tus.io PATCH request for %q", t.Object.Oid) req, err = httputil.NewHttpRequest("PATCH", rel.Href, rel.Header) if err != nil { return err } req.Header.Set("Tus-Resumable", TusVersion) req.Header.Set("Upload-Offset", strconv.FormatInt(offset, 10)) req.Header.Set("Content-Type", "application/offset+octet-stream") req.Header.Set("Content-Length", strconv.FormatInt(t.Object.Size-offset, 10)) req.ContentLength = t.Object.Size - offset // 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(config.Config, req, false) if err != nil { return errors.NewRetriableError(err) } httputil.LogTransfer(config.Config, "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 { err = errors.New("http: received status 403") return errors.NewRetriableError(err) } if res.StatusCode > 299 { return errors.Wrapf(nil, "Invalid status for %s: %d", httputil.TraceHttpReq(req), res.StatusCode) } io.Copy(ioutil.Discard, res.Body) res.Body.Close() return api.VerifyUpload(config.Config, t.Object) }
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 TestUploadVerifyError(t *testing.T) { SetupTestCredentialsFunc() repo := test.NewRepo(t) repo.Pushd() defer func() { repo.Popd() repo.Cleanup() RestoreCredentialsFunc() }() mux := http.NewServeMux() server := httptest.NewServer(mux) tmp := tempdir(t) defer server.Close() defer os.RemoveAll(tmp) postCalled := false verifyCalled := false mux.HandleFunc("/media/objects", func(w http.ResponseWriter, r *http.Request) { t.Logf("Server: %s %s", r.Method, r.URL) if r.Method != "POST" { w.WriteHeader(405) return } if r.Header.Get("Accept") != api.MediaType { t.Errorf("Invalid Accept") } if r.Header.Get("Content-Type") != api.MediaType { t.Errorf("Invalid Content-Type") } buf := &bytes.Buffer{} tee := io.TeeReader(r.Body, buf) reqObj := &api.ObjectResource{} err := json.NewDecoder(tee).Decode(reqObj) t.Logf("request header: %v", r.Header) t.Logf("request body: %s", buf.String()) if err != nil { t.Fatal(err) } if reqObj.Oid != "988881adc9fc3655077dc2d4d757d480b5ea0e11" { t.Errorf("invalid oid from request: %s", reqObj.Oid) } if reqObj.Size != 4 { t.Errorf("invalid size from request: %d", reqObj.Size) } obj := &api.ObjectResource{ Oid: reqObj.Oid, Size: reqObj.Size, Actions: map[string]*api.LinkRelation{ "upload": &api.LinkRelation{ Href: server.URL + "/upload", Header: map[string]string{"A": "1"}, }, "verify": &api.LinkRelation{ Href: server.URL + "/verify", Header: map[string]string{"B": "2"}, }, }, } by, err := json.Marshal(obj) if err != nil { t.Fatal(err) } postCalled = true head := w.Header() head.Set("Content-Type", api.MediaType) head.Set("Content-Length", strconv.Itoa(len(by))) w.WriteHeader(202) w.Write(by) }) mux.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request) { verifyCalled = true w.WriteHeader(404) }) cfg := config.NewFrom(config.Values{ Git: map[string]string{ "lfs.url": server.URL + "/media", }, }) oidPath, _ := lfs.LocalMediaPath("988881adc9fc3655077dc2d4d757d480b5ea0e11") if err := ioutil.WriteFile(oidPath, []byte("test"), 0744); err != nil { t.Fatal(err) } oid := filepath.Base(oidPath) stat, _ := os.Stat(oidPath) o, _, err := api.BatchOrLegacySingle(cfg, &api.ObjectResource{Oid: oid, Size: stat.Size()}, "upload", []string{"basic"}) if err != nil { if isDockerConnectionError(err) { return } t.Fatal(err) } err = api.VerifyUpload(cfg, o) if err == nil { t.Fatal("verify should fail") } if errors.IsFatalError(err) { t.Fatal("should not panic") } expected := fmt.Sprintf(httputil.GetDefaultError(404), server.URL+"/verify") if err.Error() != expected { t.Fatalf("Expected: %s\nGot: %s", expected, err.Error()) } if !postCalled { t.Errorf("POST not called") } if !verifyCalled { t.Errorf("verify not called") } }