You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							553 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							553 lines
						
					
					
						
							13 KiB
						
					
					
				
								package http
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"compress/gzip"
							 | 
						|
									"context"
							 | 
						|
									"encoding/json"
							 | 
						|
									"errors"
							 | 
						|
									"fmt"
							 | 
						|
									"sync"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/util"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/util/mem"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/util/request_id"
							 | 
						|
								
							 | 
						|
									"io"
							 | 
						|
									"net/http"
							 | 
						|
									"net/url"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/security"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								var ErrNotFound = fmt.Errorf("not found")
							 | 
						|
								
							 | 
						|
								var (
							 | 
						|
									jwtSigningReadKey        security.SigningKey
							 | 
						|
									jwtSigningReadKeyExpires int
							 | 
						|
									loadJwtConfigOnce        sync.Once
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								func loadJwtConfig() {
							 | 
						|
									v := util.GetViper()
							 | 
						|
									jwtSigningReadKey = security.SigningKey(v.GetString("jwt.signing.read.key"))
							 | 
						|
									jwtSigningReadKeyExpires = v.GetInt("jwt.signing.read.expires_after_seconds")
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func Post(url string, values url.Values) ([]byte, error) {
							 | 
						|
									r, err := GetGlobalHttpClient().PostForm(url, values)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
									defer r.Body.Close()
							 | 
						|
									b, err := io.ReadAll(r.Body)
							 | 
						|
									if r.StatusCode >= 400 {
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, fmt.Errorf("%s: %d - %s", url, r.StatusCode, string(b))
							 | 
						|
										} else {
							 | 
						|
											return nil, fmt.Errorf("%s: %s", url, r.Status)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
									return b, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// github.com/seaweedfs/seaweedfs/unmaintained/repeated_vacuum/repeated_vacuum.go
							 | 
						|
								// may need increasing http.Client.Timeout
							 | 
						|
								func Get(url string) ([]byte, bool, error) {
							 | 
						|
									return GetAuthenticated(url, "")
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func GetAuthenticated(url, jwt string) ([]byte, bool, error) {
							 | 
						|
									request, err := http.NewRequest(http.MethodGet, url, nil)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, true, err
							 | 
						|
									}
							 | 
						|
									maybeAddAuth(request, jwt)
							 | 
						|
									request.Header.Add("Accept-Encoding", "gzip")
							 | 
						|
								
							 | 
						|
									response, err := GetGlobalHttpClient().Do(request)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, true, err
							 | 
						|
									}
							 | 
						|
									defer CloseResponse(response)
							 | 
						|
								
							 | 
						|
									var reader io.ReadCloser
							 | 
						|
									switch response.Header.Get("Content-Encoding") {
							 | 
						|
									case "gzip":
							 | 
						|
										reader, err = gzip.NewReader(response.Body)
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, true, err
							 | 
						|
										}
							 | 
						|
										defer reader.Close()
							 | 
						|
									default:
							 | 
						|
										reader = response.Body
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									b, err := io.ReadAll(reader)
							 | 
						|
									if response.StatusCode >= 400 {
							 | 
						|
										retryable := response.StatusCode >= 500
							 | 
						|
										return nil, retryable, fmt.Errorf("%s: %s", url, response.Status)
							 | 
						|
									}
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, false, err
							 | 
						|
									}
							 | 
						|
									return b, false, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func Head(url string) (http.Header, error) {
							 | 
						|
									r, err := GetGlobalHttpClient().Head(url)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
									defer CloseResponse(r)
							 | 
						|
									if r.StatusCode >= 400 {
							 | 
						|
										return nil, fmt.Errorf("%s: %s", url, r.Status)
							 | 
						|
									}
							 | 
						|
									return r.Header, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func maybeAddAuth(req *http.Request, jwt string) {
							 | 
						|
									if jwt != "" {
							 | 
						|
										req.Header.Set("Authorization", "BEARER "+string(jwt))
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func Delete(url string, jwt string) error {
							 | 
						|
									req, err := http.NewRequest(http.MethodDelete, url, nil)
							 | 
						|
									maybeAddAuth(req, jwt)
							 | 
						|
									if err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									resp, e := GetGlobalHttpClient().Do(req)
							 | 
						|
									if e != nil {
							 | 
						|
										return e
							 | 
						|
									}
							 | 
						|
									defer resp.Body.Close()
							 | 
						|
									body, err := io.ReadAll(resp.Body)
							 | 
						|
									if err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									switch resp.StatusCode {
							 | 
						|
									case http.StatusNotFound, http.StatusAccepted, http.StatusOK:
							 | 
						|
										return nil
							 | 
						|
									}
							 | 
						|
									m := make(map[string]interface{})
							 | 
						|
									if e := json.Unmarshal(body, &m); e == nil {
							 | 
						|
										if s, ok := m["error"].(string); ok {
							 | 
						|
											return errors.New(s)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
									return errors.New(string(body))
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func DeleteProxied(url string, jwt string) (body []byte, httpStatus int, err error) {
							 | 
						|
									req, err := http.NewRequest(http.MethodDelete, url, nil)
							 | 
						|
									maybeAddAuth(req, jwt)
							 | 
						|
									if err != nil {
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
									resp, err := GetGlobalHttpClient().Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
									defer resp.Body.Close()
							 | 
						|
									body, err = io.ReadAll(resp.Body)
							 | 
						|
									if err != nil {
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
									httpStatus = resp.StatusCode
							 | 
						|
									return
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func GetBufferStream(url string, values url.Values, allocatedBytes []byte, eachBuffer func([]byte)) error {
							 | 
						|
									r, err := GetGlobalHttpClient().PostForm(url, values)
							 | 
						|
									if err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									defer CloseResponse(r)
							 | 
						|
									if r.StatusCode != 200 {
							 | 
						|
										return fmt.Errorf("%s: %s", url, r.Status)
							 | 
						|
									}
							 | 
						|
									for {
							 | 
						|
										n, err := r.Body.Read(allocatedBytes)
							 | 
						|
										if n > 0 {
							 | 
						|
											eachBuffer(allocatedBytes[:n])
							 | 
						|
										}
							 | 
						|
										if err != nil {
							 | 
						|
											if err == io.EOF {
							 | 
						|
												return nil
							 | 
						|
											}
							 | 
						|
											return err
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func GetUrlStream(url string, values url.Values, readFn func(io.Reader) error) error {
							 | 
						|
									r, err := GetGlobalHttpClient().PostForm(url, values)
							 | 
						|
									if err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									defer CloseResponse(r)
							 | 
						|
									if r.StatusCode != 200 {
							 | 
						|
										return fmt.Errorf("%s: %s", url, r.Status)
							 | 
						|
									}
							 | 
						|
									return readFn(r.Body)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func DownloadFile(fileUrl string, jwt string) (filename string, header http.Header, resp *http.Response, e error) {
							 | 
						|
									req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
							 | 
						|
									if err != nil {
							 | 
						|
										return "", nil, nil, err
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									maybeAddAuth(req, jwt)
							 | 
						|
								
							 | 
						|
									response, err := GetGlobalHttpClient().Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return "", nil, nil, err
							 | 
						|
									}
							 | 
						|
									header = response.Header
							 | 
						|
									contentDisposition := response.Header["Content-Disposition"]
							 | 
						|
									if len(contentDisposition) > 0 {
							 | 
						|
										idx := strings.Index(contentDisposition[0], "filename=")
							 | 
						|
										if idx != -1 {
							 | 
						|
											filename = contentDisposition[0][idx+len("filename="):]
							 | 
						|
											filename = strings.Trim(filename, "\"")
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
									resp = response
							 | 
						|
									return
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func Do(req *http.Request) (resp *http.Response, err error) {
							 | 
						|
									return GetGlobalHttpClient().Do(req)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func NormalizeUrl(url string) (string, error) {
							 | 
						|
									return GetGlobalHttpClient().NormalizeHttpScheme(url)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func ReadUrl(ctx context.Context, fileUrl string, cipherKey []byte, isContentCompressed bool, isFullChunk bool, offset int64, size int, buf []byte) (int64, error) {
							 | 
						|
								
							 | 
						|
									if cipherKey != nil {
							 | 
						|
										var n int
							 | 
						|
										_, err := readEncryptedUrl(ctx, fileUrl, "", cipherKey, isContentCompressed, isFullChunk, offset, size, func(data []byte) {
							 | 
						|
											n = copy(buf, data)
							 | 
						|
										})
							 | 
						|
										return int64(n), err
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
							 | 
						|
									if err != nil {
							 | 
						|
										return 0, err
							 | 
						|
									}
							 | 
						|
									if !isFullChunk {
							 | 
						|
										req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+int64(size)-1))
							 | 
						|
									} else {
							 | 
						|
										req.Header.Set("Accept-Encoding", "gzip")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									r, err := GetGlobalHttpClient().Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return 0, err
							 | 
						|
									}
							 | 
						|
									defer CloseResponse(r)
							 | 
						|
								
							 | 
						|
									if r.StatusCode >= 400 {
							 | 
						|
										return 0, fmt.Errorf("%s: %s", fileUrl, r.Status)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var reader io.ReadCloser
							 | 
						|
									contentEncoding := r.Header.Get("Content-Encoding")
							 | 
						|
									switch contentEncoding {
							 | 
						|
									case "gzip":
							 | 
						|
										reader, err = gzip.NewReader(r.Body)
							 | 
						|
										if err != nil {
							 | 
						|
											return 0, err
							 | 
						|
										}
							 | 
						|
										defer reader.Close()
							 | 
						|
									default:
							 | 
						|
										reader = r.Body
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var (
							 | 
						|
										i, m int
							 | 
						|
										n    int64
							 | 
						|
									)
							 | 
						|
								
							 | 
						|
									// refers to https://github.com/golang/go/blob/master/src/bytes/buffer.go#L199
							 | 
						|
									// commit id c170b14c2c1cfb2fd853a37add92a82fd6eb4318
							 | 
						|
									for {
							 | 
						|
										m, err = reader.Read(buf[i:])
							 | 
						|
										i += m
							 | 
						|
										n += int64(m)
							 | 
						|
										if err == io.EOF {
							 | 
						|
											return n, nil
							 | 
						|
										}
							 | 
						|
										if err != nil {
							 | 
						|
											return n, err
							 | 
						|
										}
							 | 
						|
										if n == int64(len(buf)) {
							 | 
						|
											break
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
									// drains the response body to avoid memory leak
							 | 
						|
									data, _ := io.ReadAll(reader)
							 | 
						|
									if len(data) != 0 {
							 | 
						|
										glog.V(1).InfofCtx(ctx, "%s reader has remaining %d bytes", contentEncoding, len(data))
							 | 
						|
									}
							 | 
						|
									return n, err
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func ReadUrlAsStream(ctx context.Context, fileUrl string, cipherKey []byte, isContentGzipped bool, isFullChunk bool, offset int64, size int, fn func(data []byte)) (retryable bool, err error) {
							 | 
						|
									return ReadUrlAsStreamAuthenticated(ctx, fileUrl, "", cipherKey, isContentGzipped, isFullChunk, offset, size, fn)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func ReadUrlAsStreamAuthenticated(ctx context.Context, fileUrl, jwt string, cipherKey []byte, isContentGzipped bool, isFullChunk bool, offset int64, size int, fn func(data []byte)) (retryable bool, err error) {
							 | 
						|
									if cipherKey != nil {
							 | 
						|
										return readEncryptedUrl(ctx, fileUrl, jwt, cipherKey, isContentGzipped, isFullChunk, offset, size, fn)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
							 | 
						|
									maybeAddAuth(req, jwt)
							 | 
						|
									if err != nil {
							 | 
						|
										return false, err
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if isFullChunk {
							 | 
						|
										req.Header.Add("Accept-Encoding", "gzip")
							 | 
						|
									} else {
							 | 
						|
										req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+int64(size)-1))
							 | 
						|
									}
							 | 
						|
									request_id.InjectToRequest(ctx, req)
							 | 
						|
								
							 | 
						|
									r, err := GetGlobalHttpClient().Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return true, err
							 | 
						|
									}
							 | 
						|
									defer CloseResponse(r)
							 | 
						|
									if r.StatusCode >= 400 {
							 | 
						|
										if r.StatusCode == http.StatusNotFound {
							 | 
						|
											return true, fmt.Errorf("%s: %s: %w", fileUrl, r.Status, ErrNotFound)
							 | 
						|
										}
							 | 
						|
										retryable = r.StatusCode >= 499
							 | 
						|
										return retryable, fmt.Errorf("%s: %s", fileUrl, r.Status)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var reader io.ReadCloser
							 | 
						|
									contentEncoding := r.Header.Get("Content-Encoding")
							 | 
						|
									switch contentEncoding {
							 | 
						|
									case "gzip":
							 | 
						|
										reader, err = gzip.NewReader(r.Body)
							 | 
						|
										defer reader.Close()
							 | 
						|
									default:
							 | 
						|
										reader = r.Body
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var (
							 | 
						|
										m int
							 | 
						|
									)
							 | 
						|
									buf := mem.Allocate(64 * 1024)
							 | 
						|
									defer mem.Free(buf)
							 | 
						|
								
							 | 
						|
									for {
							 | 
						|
										// Check for context cancellation before each read
							 | 
						|
										select {
							 | 
						|
										case <-ctx.Done():
							 | 
						|
											return false, ctx.Err()
							 | 
						|
										default:
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										m, err = reader.Read(buf)
							 | 
						|
										if m > 0 {
							 | 
						|
											fn(buf[:m])
							 | 
						|
										}
							 | 
						|
										if err == io.EOF {
							 | 
						|
											return false, nil
							 | 
						|
										}
							 | 
						|
										if err != nil {
							 | 
						|
											return true, err
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func readEncryptedUrl(ctx context.Context, fileUrl, jwt string, cipherKey []byte, isContentCompressed bool, isFullChunk bool, offset int64, size int, fn func(data []byte)) (bool, error) {
							 | 
						|
									encryptedData, retryable, err := GetAuthenticated(fileUrl, jwt)
							 | 
						|
									if err != nil {
							 | 
						|
										return retryable, fmt.Errorf("fetch %s: %v", fileUrl, err)
							 | 
						|
									}
							 | 
						|
									decryptedData, err := util.Decrypt(encryptedData, util.CipherKey(cipherKey))
							 | 
						|
									if err != nil {
							 | 
						|
										return false, fmt.Errorf("decrypt %s: %v", fileUrl, err)
							 | 
						|
									}
							 | 
						|
									if isContentCompressed {
							 | 
						|
										decryptedData, err = util.DecompressData(decryptedData)
							 | 
						|
										if err != nil {
							 | 
						|
											glog.V(0).InfofCtx(ctx, "unzip decrypt %s: %v", fileUrl, err)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
									if len(decryptedData) < int(offset)+size {
							 | 
						|
										return false, fmt.Errorf("read decrypted %s size %d [%d, %d)", fileUrl, len(decryptedData), offset, int(offset)+size)
							 | 
						|
									}
							 | 
						|
									if isFullChunk {
							 | 
						|
										fn(decryptedData)
							 | 
						|
									} else {
							 | 
						|
										sliceEnd := int(offset) + size
							 | 
						|
										fn(decryptedData[int(offset):sliceEnd])
							 | 
						|
									}
							 | 
						|
									return false, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func ReadUrlAsReaderCloser(fileUrl string, jwt string, rangeHeader string) (*http.Response, io.ReadCloser, error) {
							 | 
						|
								
							 | 
						|
									req, err := http.NewRequest(http.MethodGet, fileUrl, nil)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, nil, err
							 | 
						|
									}
							 | 
						|
									if rangeHeader != "" {
							 | 
						|
										req.Header.Add("Range", rangeHeader)
							 | 
						|
									} else {
							 | 
						|
										req.Header.Add("Accept-Encoding", "gzip")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									maybeAddAuth(req, jwt)
							 | 
						|
								
							 | 
						|
									r, err := GetGlobalHttpClient().Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, nil, err
							 | 
						|
									}
							 | 
						|
									if r.StatusCode >= 400 {
							 | 
						|
										CloseResponse(r)
							 | 
						|
										return nil, nil, fmt.Errorf("%s: %s", fileUrl, r.Status)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var reader io.ReadCloser
							 | 
						|
									contentEncoding := r.Header.Get("Content-Encoding")
							 | 
						|
									switch contentEncoding {
							 | 
						|
									case "gzip":
							 | 
						|
										reader, err = gzip.NewReader(r.Body)
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, nil, err
							 | 
						|
										}
							 | 
						|
									default:
							 | 
						|
										reader = r.Body
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return r, reader, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func CloseResponse(resp *http.Response) {
							 | 
						|
									if resp == nil || resp.Body == nil {
							 | 
						|
										return
							 | 
						|
									}
							 | 
						|
									reader := &CountingReader{reader: resp.Body}
							 | 
						|
									io.Copy(io.Discard, reader)
							 | 
						|
									resp.Body.Close()
							 | 
						|
									if reader.BytesRead > 0 {
							 | 
						|
										glog.V(1).Infof("response leftover %d bytes", reader.BytesRead)
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func CloseRequest(req *http.Request) {
							 | 
						|
									reader := &CountingReader{reader: req.Body}
							 | 
						|
									io.Copy(io.Discard, reader)
							 | 
						|
									req.Body.Close()
							 | 
						|
									if reader.BytesRead > 0 {
							 | 
						|
										glog.V(1).Infof("request leftover %d bytes", reader.BytesRead)
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								type CountingReader struct {
							 | 
						|
									reader    io.Reader
							 | 
						|
									BytesRead int
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (r *CountingReader) Read(p []byte) (n int, err error) {
							 | 
						|
									n, err = r.reader.Read(p)
							 | 
						|
									r.BytesRead += n
							 | 
						|
									return n, err
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func RetriedFetchChunkData(ctx context.Context, buffer []byte, urlStrings []string, cipherKey []byte, isGzipped bool, isFullChunk bool, offset int64, fileId string) (n int, err error) {
							 | 
						|
								
							 | 
						|
									loadJwtConfigOnce.Do(loadJwtConfig)
							 | 
						|
									var jwt security.EncodedJwt
							 | 
						|
									if len(jwtSigningReadKey) > 0 {
							 | 
						|
										jwt = security.GenJwtForVolumeServer(
							 | 
						|
											jwtSigningReadKey,
							 | 
						|
											jwtSigningReadKeyExpires,
							 | 
						|
											fileId,
							 | 
						|
										)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var shouldRetry bool
							 | 
						|
								
							 | 
						|
									for waitTime := time.Second; waitTime < util.RetryWaitTime; waitTime += waitTime / 2 {
							 | 
						|
										// Check for context cancellation before starting retry loop
							 | 
						|
										select {
							 | 
						|
										case <-ctx.Done():
							 | 
						|
											return n, ctx.Err()
							 | 
						|
										default:
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										for _, urlString := range urlStrings {
							 | 
						|
											// Check for context cancellation before each volume server request
							 | 
						|
											select {
							 | 
						|
											case <-ctx.Done():
							 | 
						|
												return n, ctx.Err()
							 | 
						|
											default:
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											n = 0
							 | 
						|
											if strings.Contains(urlString, "%") {
							 | 
						|
												urlString = url.PathEscape(urlString)
							 | 
						|
											}
							 | 
						|
											shouldRetry, err = ReadUrlAsStreamAuthenticated(ctx, urlString+"?readDeleted=true", string(jwt), cipherKey, isGzipped, isFullChunk, offset, len(buffer), func(data []byte) {
							 | 
						|
												// Check for context cancellation during data processing
							 | 
						|
												select {
							 | 
						|
												case <-ctx.Done():
							 | 
						|
													// Stop processing data when context is cancelled
							 | 
						|
													return
							 | 
						|
												default:
							 | 
						|
												}
							 | 
						|
								
							 | 
						|
												if n < len(buffer) {
							 | 
						|
													x := copy(buffer[n:], data)
							 | 
						|
													n += x
							 | 
						|
												}
							 | 
						|
											})
							 | 
						|
											if !shouldRetry {
							 | 
						|
												break
							 | 
						|
											}
							 | 
						|
											if err != nil {
							 | 
						|
												glog.V(0).InfofCtx(ctx, "read %s failed, err: %v", urlString, err)
							 | 
						|
											} else {
							 | 
						|
												break
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
										if err != nil && shouldRetry {
							 | 
						|
											glog.V(0).InfofCtx(ctx, "retry reading in %v", waitTime)
							 | 
						|
											// Sleep with proper context cancellation and timer cleanup
							 | 
						|
											timer := time.NewTimer(waitTime)
							 | 
						|
											select {
							 | 
						|
											case <-ctx.Done():
							 | 
						|
												timer.Stop()
							 | 
						|
												return n, ctx.Err()
							 | 
						|
											case <-timer.C:
							 | 
						|
												// Continue with retry
							 | 
						|
											}
							 | 
						|
										} else {
							 | 
						|
											break
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return n, err
							 | 
						|
								
							 | 
						|
								}
							 |