// download starts or resumes and download. Always closes dlFile if non-nil func (a *basicDownloadAdapter) download(t *Transfer, cb TransferProgressCallback, authOkFunc func(), dlFile *os.File, fromByte int64, hash hash.Hash) error { if dlFile != nil { // ensure we always close dlFile. Note that this does not conflict with the // early close below, as close is idempotent. defer dlFile.Close() } rel, ok := t.Object.Rel("download") if !ok { return errors.New("Object not found on the server.") } req, err := httputil.NewHttpRequest("GET", rel.Href, rel.Header) if err != nil { return err } if fromByte > 0 { if dlFile == nil || hash == nil { return fmt.Errorf("Cannot restart %v from %d without a file & hash", t.Object.Oid, fromByte) } // We could just use a start byte, but since we know the length be specific req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", fromByte, t.Object.Size-1)) } res, err := httputil.DoHttpRequest(req, true) if err != nil { // Special-case status code 416 () - fall back if fromByte > 0 && dlFile != nil && res.StatusCode == 416 { tracerx.Printf("xfer: server rejected resume download request for %q from byte %d; re-downloading from start", t.Object.Oid, fromByte) dlFile.Close() os.Remove(dlFile.Name()) return a.download(t, cb, authOkFunc, nil, 0, nil) } return errutil.NewRetriableError(err) } httputil.LogTransfer("lfs.data.download", res) defer res.Body.Close() // Range request must return 206 & content range to confirm if fromByte > 0 { rangeRequestOk := false var failReason string // check 206 and Content-Range, fall back if either not as expected if res.StatusCode == 206 { // Probably a successful range request, check Content-Range if rangeHdr := res.Header.Get("Content-Range"); rangeHdr != "" { regex := regexp.MustCompile(`bytes (\d+)\-.*`) match := regex.FindStringSubmatch(rangeHdr) if match != nil && len(match) > 1 { contentStart, _ := strconv.ParseInt(match[1], 10, 64) if contentStart == fromByte { rangeRequestOk = true } else { failReason = fmt.Sprintf("Content-Range start byte incorrect: %s expected %d", match[1], fromByte) } } else { failReason = fmt.Sprintf("badly formatted Content-Range header: %q", rangeHdr) } } else { failReason = "missing Content-Range header in response" } } else { failReason = fmt.Sprintf("expected status code 206, received %d", res.StatusCode) } if rangeRequestOk { tracerx.Printf("xfer: server accepted resume download request: %q from byte %d", t.Object.Oid, fromByte) // Advance progress callback; must split into max int sizes though if cb != nil { const maxInt = int(^uint(0) >> 1) for read := int64(0); read < fromByte; { remainder := fromByte - read if remainder > int64(maxInt) { read += int64(maxInt) cb(t.Name, t.Object.Size, read, maxInt) } else { read += remainder cb(t.Name, t.Object.Size, read, int(remainder)) } } } } else { // Abort resume, perform regular download tracerx.Printf("xfer: failed to resume download for %q from byte %d: %s. Re-downloading from start", t.Object.Oid, fromByte, failReason) dlFile.Close() os.Remove(dlFile.Name()) if res.StatusCode == 200 { // If status code was 200 then server just ignored Range header and // sent everything. Don't re-request, use this one from byte 0 dlFile = nil fromByte = 0 hash = nil } else { // re-request needed return a.download(t, cb, authOkFunc, nil, 0, nil) } } } // Signal auth OK on success response, before starting download to free up // other workers immediately if authOkFunc != nil { authOkFunc() } var hasher *tools.HashingReader if fromByte > 0 && hash != nil { // pre-load hashing reader with previous content hasher = tools.NewHashingReaderPreloadHash(res.Body, hash) } else { hasher = tools.NewHashingReader(res.Body) } if dlFile == nil { // New file start dlFile, err = os.OpenFile(a.downloadFilename(t), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } defer dlFile.Close() } dlfilename := dlFile.Name() // Wrap callback to give name context ccb := func(totalSize int64, readSoFar int64, readSinceLast int) error { if cb != nil { return cb(t.Name, totalSize, readSoFar+fromByte, readSinceLast) } return nil } written, err := tools.CopyWithCallback(dlFile, hasher, res.ContentLength, ccb) if err != nil { return fmt.Errorf("cannot write data to tempfile %q: %v", dlfilename, err) } if err := dlFile.Close(); err != nil { return fmt.Errorf("can't close tempfile %q: %v", dlfilename, err) } if actual := hasher.Hash(); actual != t.Object.Oid { return fmt.Errorf("Expected OID %s, got %s after %d bytes written", t.Object.Oid, actual, written) } return tools.RenameFileCopyPermissions(dlfilename, t.Path) }
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 }