storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/httprange.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2015, 2016 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package cmd 18 19 import ( 20 "errors" 21 "fmt" 22 "strconv" 23 "strings" 24 ) 25 26 const ( 27 byteRangePrefix = "bytes=" 28 ) 29 30 // HTTPRangeSpec represents a range specification as supported by S3 GET 31 // object request. 32 // 33 // Case 1: Not present -> represented by a nil RangeSpec 34 // Case 2: bytes=1-10 (absolute start and end offsets) -> RangeSpec{false, 1, 10} 35 // Case 3: bytes=10- (absolute start offset with end offset unspecified) -> RangeSpec{false, 10, -1} 36 // Case 4: bytes=-30 (suffix length specification) -> RangeSpec{true, -30, -1} 37 type HTTPRangeSpec struct { 38 // Does the range spec refer to a suffix of the object? 39 IsSuffixLength bool 40 41 // Start and end offset specified in range spec 42 Start, End int64 43 } 44 45 // GetLength - get length of range 46 func (h *HTTPRangeSpec) GetLength(resourceSize int64) (rangeLength int64, err error) { 47 switch { 48 case resourceSize < 0: 49 return 0, errors.New("Resource size cannot be negative") 50 51 case h == nil: 52 rangeLength = resourceSize 53 54 case h.IsSuffixLength: 55 specifiedLen := -h.Start 56 rangeLength = specifiedLen 57 if specifiedLen > resourceSize { 58 rangeLength = resourceSize 59 } 60 61 case h.Start >= resourceSize: 62 return 0, errInvalidRange 63 64 case h.End > -1: 65 end := h.End 66 if resourceSize <= end { 67 end = resourceSize - 1 68 } 69 rangeLength = end - h.Start + 1 70 71 case h.End == -1: 72 rangeLength = resourceSize - h.Start 73 74 default: 75 return 0, errors.New("Unexpected range specification case") 76 } 77 78 return rangeLength, nil 79 } 80 81 // GetOffsetLength computes the start offset and length of the range 82 // given the size of the resource 83 func (h *HTTPRangeSpec) GetOffsetLength(resourceSize int64) (start, length int64, err error) { 84 if h == nil { 85 // No range specified, implies whole object. 86 return 0, resourceSize, nil 87 } 88 89 length, err = h.GetLength(resourceSize) 90 if err != nil { 91 return 0, 0, err 92 } 93 94 start = h.Start 95 if h.IsSuffixLength { 96 start = resourceSize + h.Start 97 if start < 0 { 98 start = 0 99 } 100 } 101 return start, length, nil 102 } 103 104 // Parse a HTTP range header value into a HTTPRangeSpec 105 func parseRequestRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) { 106 // Return error if given range string doesn't start with byte range prefix. 107 if !strings.HasPrefix(rangeString, byteRangePrefix) { 108 return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix) 109 } 110 111 // Trim byte range prefix. 112 byteRangeString := strings.TrimPrefix(rangeString, byteRangePrefix) 113 114 // Check if range string contains delimiter '-', else return error. eg. "bytes=8" 115 sepIndex := strings.Index(byteRangeString, "-") 116 if sepIndex == -1 { 117 return nil, fmt.Errorf("'%s' does not have a valid range value", rangeString) 118 } 119 120 offsetBeginString := byteRangeString[:sepIndex] 121 offsetBegin := int64(-1) 122 // Convert offsetBeginString only if its not empty. 123 if len(offsetBeginString) > 0 { 124 if offsetBeginString[0] == '+' { 125 return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetBeginString) 126 } else if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil { 127 return nil, fmt.Errorf("'%s' does not have a valid first byte position value", rangeString) 128 } else if offsetBegin < 0 { 129 return nil, fmt.Errorf("First byte position is negative ('%d')", offsetBegin) 130 } 131 } 132 133 offsetEndString := byteRangeString[sepIndex+1:] 134 offsetEnd := int64(-1) 135 // Convert offsetEndString only if its not empty. 136 if len(offsetEndString) > 0 { 137 if offsetEndString[0] == '+' { 138 return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetEndString) 139 } else if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil { 140 return nil, fmt.Errorf("'%s' does not have a valid last byte position value", rangeString) 141 } else if offsetEnd < 0 { 142 return nil, fmt.Errorf("Last byte position is negative ('%d')", offsetEnd) 143 } 144 } 145 146 switch { 147 case offsetBegin > -1 && offsetEnd > -1: 148 if offsetBegin > offsetEnd { 149 return nil, errInvalidRange 150 } 151 return &HTTPRangeSpec{false, offsetBegin, offsetEnd}, nil 152 case offsetBegin > -1: 153 return &HTTPRangeSpec{false, offsetBegin, -1}, nil 154 case offsetEnd > -1: 155 if offsetEnd == 0 { 156 return nil, errInvalidRange 157 } 158 return &HTTPRangeSpec{true, -offsetEnd, -1}, nil 159 default: 160 // rangeString contains first and last byte positions missing. eg. "bytes=-" 161 return nil, fmt.Errorf("'%s' does not have valid range value", rangeString) 162 } 163 } 164 165 // String returns stringified representation of range for a particular resource size. 166 func (h *HTTPRangeSpec) String(resourceSize int64) string { 167 if h == nil { 168 return "" 169 } 170 off, length, err := h.GetOffsetLength(resourceSize) 171 if err != nil { 172 return "" 173 } 174 return fmt.Sprintf("%d-%d", off, off+length-1) 175 }