From fc4e1c275c26d362d294ab4cfb198f1e0e127d43 Mon Sep 17 00:00:00 2001 From: Oz Katz Date: Tue, 22 Nov 2022 11:06:58 +0200 Subject: [PATCH] Add byte range support to OpenAPI (server, spec and generated clients) (#4623) * added byte range support to OpenAPI * push generated clients * order imports, fix review comments * added tests for byte ranges * Changed Fatalf's to Errorf's where possible to aide debugging --- api/swagger.yml | 40 ++++++++++++++ clients/java/api/openapi.yaml | 51 ++++++++++++++++++ clients/java/docs/ObjectsApi.md | 8 ++- .../io/lakefs/clients/api/ObjectsApi.java | 34 ++++++++---- .../io/lakefs/clients/api/ObjectsApiTest.java | 3 +- clients/python/docs/ObjectsApi.md | 13 +++++ .../python/lakefs_client/api/objects_api.py | 13 +++++ pkg/api/controller.go | 37 ++++++++++--- pkg/api/controller_test.go | 54 ++++++++++++++++--- pkg/gateway/operations/getobject.go | 5 +- pkg/gateway/operations/putobject.go | 3 +- pkg/{gateway/http => httputil}/range.go | 2 +- pkg/{gateway/http => httputil}/range_test.go | 6 +-- 13 files changed, 235 insertions(+), 34 deletions(-) rename pkg/{gateway/http => httputil}/range.go (99%) rename pkg/{gateway/http => httputil}/range_test.go (90%) diff --git a/api/swagger.yml b/api/swagger.yml index 63c8ed55018..bf72f730154 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -2669,6 +2669,15 @@ paths: - objects operationId: getObject summary: get object content + parameters: + - in: header + name: Range + description: Byte range to retrieve + example: "bytes=0-1023" + required: false + schema: + type: string + pattern: '^bytes=((\d*-\d*,? ?)+)$' responses: 200: description: object content @@ -2691,6 +2700,31 @@ paths: Content-Disposition: schema: type: string + 206: + description: partial object content + content: + application/octet-stream: + schema: + type: string + format: binary + headers: + Content-Length: + schema: + type: integer + format: int64 + Content-Range: + schema: + type: string + pattern: '^bytes=((\d*-\d*,? ?)+)$' + Last-Modified: + schema: + type: string + ETag: + schema: + type: string + Content-Disposition: + schema: + type: string 401: $ref: "#/components/responses/Unauthorized" 404: @@ -2703,6 +2737,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + 416: + description: Requested Range Not Satisfiable + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /repositories/{repository}/branches/{branch}/staging/backing: parameters: diff --git a/clients/java/api/openapi.yaml b/clients/java/api/openapi.yaml index 568bb024f38..dfe310dfb73 100644 --- a/clients/java/api/openapi.yaml +++ b/clients/java/api/openapi.yaml @@ -2910,6 +2910,16 @@ paths: schema: type: string style: form + - description: Byte range to retrieve + example: bytes=0-1023 + explode: false + in: header + name: Range + required: false + schema: + pattern: ^bytes=((\d*-\d*,? ?)+)$ + type: string + style: simple responses: "200": content: @@ -2940,6 +2950,41 @@ paths: schema: type: string style: simple + "206": + content: + application/octet-stream: + schema: + format: binary + type: string + description: partial object content + headers: + Content-Length: + explode: false + schema: + format: int64 + type: integer + style: simple + Content-Range: + explode: false + schema: + pattern: ^bytes=((\d*-\d*,? ?)+)$ + type: string + style: simple + Last-Modified: + explode: false + schema: + type: string + style: simple + ETag: + explode: false + schema: + type: string + style: simple + Content-Disposition: + explode: false + schema: + type: string + style: simple "401": content: application/json: @@ -2964,6 +3009,12 @@ paths: schema: $ref: '#/components/schemas/Error' description: object expired + "416": + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: Requested Range Not Satisfiable summary: get object content tags: - objects diff --git a/clients/java/docs/ObjectsApi.md b/clients/java/docs/ObjectsApi.md index 853c2f316c4..37d2a065f02 100644 --- a/clients/java/docs/ObjectsApi.md +++ b/clients/java/docs/ObjectsApi.md @@ -193,7 +193,7 @@ Name | Type | Description | Notes # **getObject** -> File getObject(repository, ref, path) +> File getObject(repository, ref, path, range) get object content @@ -237,8 +237,9 @@ public class Example { String repository = "repository_example"; // String | String ref = "ref_example"; // String | a reference (could be either a branch or a commit ID) String path = "path_example"; // String | relative to the ref + String range = "bytes=0-1023"; // String | Byte range to retrieve try { - File result = apiInstance.getObject(repository, ref, path); + File result = apiInstance.getObject(repository, ref, path, range); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling ObjectsApi#getObject"); @@ -258,6 +259,7 @@ Name | Type | Description | Notes **repository** | **String**| | **ref** | **String**| a reference (could be either a branch or a commit ID) | **path** | **String**| relative to the ref | + **range** | **String**| Byte range to retrieve | [optional] ### Return type @@ -276,9 +278,11 @@ Name | Type | Description | Notes | Status code | Description | Response headers | |-------------|-------------|------------------| **200** | object content | * Content-Length -
* Last-Modified -
* ETag -
* Content-Disposition -
| +**206** | partial object content | * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
* Content-Disposition -
| **401** | Unauthorized | - | **404** | Resource Not Found | - | **410** | object expired | - | +**416** | Requested Range Not Satisfiable | - | **0** | Internal Server Error | - | diff --git a/clients/java/src/main/java/io/lakefs/clients/api/ObjectsApi.java b/clients/java/src/main/java/io/lakefs/clients/api/ObjectsApi.java index 60a20c39e3f..6be15a9bebb 100644 --- a/clients/java/src/main/java/io/lakefs/clients/api/ObjectsApi.java +++ b/clients/java/src/main/java/io/lakefs/clients/api/ObjectsApi.java @@ -352,6 +352,7 @@ public okhttp3.Call deleteObjectsAsync(String repository, String branch, PathLis * @param repository (required) * @param ref a reference (could be either a branch or a commit ID) (required) * @param path relative to the ref (required) + * @param range Byte range to retrieve (optional) * @param _callback Callback for upload/download progress * @return Call to execute * @throws ApiException If fail to serialize the request body object @@ -359,13 +360,15 @@ public okhttp3.Call deleteObjectsAsync(String repository, String branch, PathLis + +
Status Code Description Response Headers
200 object content * Content-Length -
* Last-Modified -
* ETag -
* Content-Disposition -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
* Content-Disposition -
401 Unauthorized -
404 Resource Not Found -
410 object expired -
416 Requested Range Not Satisfiable -
0 Internal Server Error -
*/ - public okhttp3.Call getObjectCall(String repository, String ref, String path, final ApiCallback _callback) throws ApiException { + public okhttp3.Call getObjectCall(String repository, String ref, String path, String range, final ApiCallback _callback) throws ApiException { Object localVarPostBody = null; // create path and map variables @@ -383,6 +386,10 @@ public okhttp3.Call getObjectCall(String repository, String ref, String path, fi localVarQueryParams.addAll(localVarApiClient.parameterToPair("path", path)); } + if (range != null) { + localVarHeaderParams.put("Range", localVarApiClient.parameterToString(range)); + } + final String[] localVarAccepts = { "application/octet-stream", "application/json" }; @@ -402,7 +409,7 @@ public okhttp3.Call getObjectCall(String repository, String ref, String path, fi } @SuppressWarnings("rawtypes") - private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, String path, final ApiCallback _callback) throws ApiException { + private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, String path, String range, final ApiCallback _callback) throws ApiException { // verify the required parameter 'repository' is set if (repository == null) { @@ -420,7 +427,7 @@ private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, } - okhttp3.Call localVarCall = getObjectCall(repository, ref, path, _callback); + okhttp3.Call localVarCall = getObjectCall(repository, ref, path, range, _callback); return localVarCall; } @@ -431,20 +438,23 @@ private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, * @param repository (required) * @param ref a reference (could be either a branch or a commit ID) (required) * @param path relative to the ref (required) + * @param range Byte range to retrieve (optional) * @return File * @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body * @http.response.details + +
Status Code Description Response Headers
200 object content * Content-Length -
* Last-Modified -
* ETag -
* Content-Disposition -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
* Content-Disposition -
401 Unauthorized -
404 Resource Not Found -
410 object expired -
416 Requested Range Not Satisfiable -
0 Internal Server Error -
*/ - public File getObject(String repository, String ref, String path) throws ApiException { - ApiResponse localVarResp = getObjectWithHttpInfo(repository, ref, path); + public File getObject(String repository, String ref, String path, String range) throws ApiException { + ApiResponse localVarResp = getObjectWithHttpInfo(repository, ref, path, range); return localVarResp.getData(); } @@ -454,20 +464,23 @@ public File getObject(String repository, String ref, String path) throws ApiExce * @param repository (required) * @param ref a reference (could be either a branch or a commit ID) (required) * @param path relative to the ref (required) + * @param range Byte range to retrieve (optional) * @return ApiResponse<File> * @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body * @http.response.details + +
Status Code Description Response Headers
200 object content * Content-Length -
* Last-Modified -
* ETag -
* Content-Disposition -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
* Content-Disposition -
401 Unauthorized -
404 Resource Not Found -
410 object expired -
416 Requested Range Not Satisfiable -
0 Internal Server Error -
*/ - public ApiResponse getObjectWithHttpInfo(String repository, String ref, String path) throws ApiException { - okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, null); + public ApiResponse getObjectWithHttpInfo(String repository, String ref, String path, String range) throws ApiException { + okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, null); Type localVarReturnType = new TypeToken(){}.getType(); return localVarApiClient.execute(localVarCall, localVarReturnType); } @@ -478,6 +491,7 @@ public ApiResponse getObjectWithHttpInfo(String repository, String ref, St * @param repository (required) * @param ref a reference (could be either a branch or a commit ID) (required) * @param path relative to the ref (required) + * @param range Byte range to retrieve (optional) * @param _callback The callback to be executed when the API call finishes * @return The request call * @throws ApiException If fail to process the API call, e.g. serializing the request body object @@ -485,15 +499,17 @@ public ApiResponse getObjectWithHttpInfo(String repository, String ref, St + +
Status Code Description Response Headers
200 object content * Content-Length -
* Last-Modified -
* ETag -
* Content-Disposition -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
* Content-Disposition -
401 Unauthorized -
404 Resource Not Found -
410 object expired -
416 Requested Range Not Satisfiable -
0 Internal Server Error -
*/ - public okhttp3.Call getObjectAsync(String repository, String ref, String path, final ApiCallback _callback) throws ApiException { + public okhttp3.Call getObjectAsync(String repository, String ref, String path, String range, final ApiCallback _callback) throws ApiException { - okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, _callback); + okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, _callback); Type localVarReturnType = new TypeToken(){}.getType(); localVarApiClient.executeAsync(localVarCall, localVarReturnType, _callback); return localVarCall; diff --git a/clients/java/src/test/java/io/lakefs/clients/api/ObjectsApiTest.java b/clients/java/src/test/java/io/lakefs/clients/api/ObjectsApiTest.java index 5aa189626f2..83ee9686a2e 100644 --- a/clients/java/src/test/java/io/lakefs/clients/api/ObjectsApiTest.java +++ b/clients/java/src/test/java/io/lakefs/clients/api/ObjectsApiTest.java @@ -86,7 +86,8 @@ public void getObjectTest() throws ApiException { String repository = null; String ref = null; String path = null; - File response = api.getObject(repository, ref, path); + String range = null; + File response = api.getObject(repository, ref, path, range); // TODO: test validations } diff --git a/clients/python/docs/ObjectsApi.md b/clients/python/docs/ObjectsApi.md index 34ddc062119..03c7760fee2 100644 --- a/clients/python/docs/ObjectsApi.md +++ b/clients/python/docs/ObjectsApi.md @@ -284,6 +284,7 @@ with lakefs_client.ApiClient(configuration) as api_client: repository = "repository_example" # str | ref = "ref_example" # str | a reference (could be either a branch or a commit ID) path = "path_example" # str | relative to the ref + range = "bytes=0-1023" # str | Byte range to retrieve (optional) # example passing only required values which don't have defaults set try: @@ -292,6 +293,15 @@ with lakefs_client.ApiClient(configuration) as api_client: pprint(api_response) except lakefs_client.ApiException as e: print("Exception when calling ObjectsApi->get_object: %s\n" % e) + + # example passing only required values which don't have defaults set + # and optional values + try: + # get object content + api_response = api_instance.get_object(repository, ref, path, range=range) + pprint(api_response) + except lakefs_client.ApiException as e: + print("Exception when calling ObjectsApi->get_object: %s\n" % e) ``` @@ -302,6 +312,7 @@ Name | Type | Description | Notes **repository** | **str**| | **ref** | **str**| a reference (could be either a branch or a commit ID) | **path** | **str**| relative to the ref | + **range** | **str**| Byte range to retrieve | [optional] ### Return type @@ -322,9 +333,11 @@ Name | Type | Description | Notes | Status code | Description | Response headers | |-------------|-------------|------------------| **200** | object content | * Content-Length -
* Last-Modified -
* ETag -
* Content-Disposition -
| +**206** | partial object content | * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
* Content-Disposition -
| **401** | Unauthorized | - | **404** | Resource Not Found | - | **410** | object expired | - | +**416** | Requested Range Not Satisfiable | - | **0** | Internal Server Error | - | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/clients/python/lakefs_client/api/objects_api.py b/clients/python/lakefs_client/api/objects_api.py index 92a136e57eb..e666a54aaac 100644 --- a/clients/python/lakefs_client/api/objects_api.py +++ b/clients/python/lakefs_client/api/objects_api.py @@ -194,6 +194,7 @@ def __init__(self, api_client=None): 'repository', 'ref', 'path', + 'range', ], 'required': [ 'repository', @@ -205,10 +206,17 @@ def __init__(self, api_client=None): 'enum': [ ], 'validation': [ + 'range', ] }, root_map={ 'validations': { + ('range',): { + + 'regex': { + 'pattern': r'^bytes=((\d*-\d*,? ?)+)$', # noqa: E501 + }, + }, }, 'allowed_values': { }, @@ -219,16 +227,20 @@ def __init__(self, api_client=None): (str,), 'path': (str,), + 'range': + (str,), }, 'attribute_map': { 'repository': 'repository', 'ref': 'ref', 'path': 'path', + 'range': 'Range', }, 'location_map': { 'repository': 'path', 'ref': 'path', 'path': 'query', + 'range': 'header', }, 'collection_format_map': { } @@ -801,6 +813,7 @@ def get_object( path (str): relative to the ref Keyword Args: + range (str): Byte range to retrieve. [optional] _return_http_data_only (bool): response data without head status code and headers. Default is True. _preload_content (bool): if False, the urllib3.HTTPResponse object diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 10686ee3e7d..8ae01ac94d5 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -2881,14 +2881,37 @@ func (c *Controller) GetObject(w http.ResponseWriter, r *http.Request, repositor } // setup response - reader, err := c.BlockAdapter.Get(ctx, block.ObjectPointer{StorageNamespace: repo.StorageNamespace, Identifier: entry.PhysicalAddress}, entry.Size) - if c.handleAPIError(ctx, w, err) { - return + var reader io.ReadCloser + pointer := block.ObjectPointer{StorageNamespace: repo.StorageNamespace, Identifier: entry.PhysicalAddress} + + // handle partial response if byte range supplied + if params.Range != nil { + rng, err := httputil.ParseRange(*params.Range, entry.Size) + if err != nil { + writeError(w, http.StatusRequestedRangeNotSatisfiable, "Requested Range Not Satisfiable") + return + } + reader, err = c.BlockAdapter.GetRange(ctx, pointer, rng.StartOffset, rng.EndOffset) + if c.handleAPIError(ctx, w, err) { + return + } + defer func() { + _ = reader.Close() + }() + w.WriteHeader(http.StatusPartialContent) + w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", rng.StartOffset, rng.EndOffset, entry.Size)) + w.Header().Set("Content-Length", fmt.Sprintf("%d", rng.EndOffset-rng.StartOffset+1)) + } else { + reader, err = c.BlockAdapter.Get(ctx, pointer, entry.Size) + if c.handleAPIError(ctx, w, err) { + return + } + defer func() { + _ = reader.Close() + }() + w.Header().Set("Content-Length", fmt.Sprint(entry.Size)) } - defer func() { - _ = reader.Close() - }() - w.Header().Set("Content-Length", fmt.Sprint(entry.Size)) + etag := httputil.ETag(entry.Checksum) w.Header().Set("ETag", etag) lastModified := httputil.HeaderTimestamp(entry.CreationDate) diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 79027707df2..e650cc1d824 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -1932,31 +1932,73 @@ func TestController_ObjectsGetObjectHandler(t *testing.T) { t.Fatal(err) } if resp.HTTPResponse.StatusCode != http.StatusOK { - t.Fatalf("GetObject() status code %d, expected %d", resp.HTTPResponse.StatusCode, http.StatusOK) + t.Errorf("GetObject() status code %d, expected %d", resp.HTTPResponse.StatusCode, http.StatusOK) } if resp.HTTPResponse.ContentLength != 37 { - t.Fatalf("expected 37 bytes in content length, got back %d", resp.HTTPResponse.ContentLength) + t.Errorf("expected 37 bytes in content length, got back %d", resp.HTTPResponse.ContentLength) } etag := resp.HTTPResponse.Header.Get("ETag") if etag != `"3c4838fe975c762ee97cf39fbbe566f1"` { - t.Fatalf("got unexpected etag: %s", etag) + t.Errorf("got unexpected etag: %s", etag) } body := string(resp.Body) if body != "this is file content made up of bytes" { - t.Fatalf("got unexpected body: '%s'", body) + t.Errorf("got unexpected body: '%s'", body) + } + }) + + t.Run("get object byte range", func(t *testing.T) { + rng := "bytes=0-9" + resp, err := clt.GetObjectWithResponse(ctx, "repo1", "main", &api.GetObjectParams{ + Path: "foo/bar", + Range: &rng, + }) + if err != nil { + t.Fatal(err) + } + if resp.HTTPResponse.StatusCode != http.StatusPartialContent { + t.Errorf("GetObject() status code %d, expected %d", resp.HTTPResponse.StatusCode, http.StatusPartialContent) + } + + if resp.HTTPResponse.ContentLength != 10 { + t.Errorf("expected 10 bytes in content length, got back %d", resp.HTTPResponse.ContentLength) + } + + etag := resp.HTTPResponse.Header.Get("ETag") + if etag != `"3c4838fe975c762ee97cf39fbbe566f1"` { + t.Errorf("got unexpected etag: %s", etag) + } + + body := string(resp.Body) + if body != "this is fi" { + t.Errorf("got unexpected body: '%s'", body) + } + }) + + t.Run("get object bad byte range", func(t *testing.T) { + rng := "bytes=380-390" + resp, err := clt.GetObjectWithResponse(ctx, "repo1", "main", &api.GetObjectParams{ + Path: "foo/bar", + Range: &rng, + }) + if err != nil { + t.Fatal(err) + } + if resp.HTTPResponse.StatusCode != http.StatusRequestedRangeNotSatisfiable { + t.Errorf("GetObject() status code %d, expected %d", resp.HTTPResponse.StatusCode, http.StatusRequestedRangeNotSatisfiable) } }) t.Run("get properties", func(t *testing.T) { resp, err := clt.GetUnderlyingPropertiesWithResponse(ctx, "repo1", "main", &api.GetUnderlyingPropertiesParams{Path: "foo/bar"}) if err != nil { - t.Fatalf("expected to get underlying properties, got %v", err) + t.Errorf("expected to get underlying properties, got %v", err) } properties := resp.JSON200 if properties == nil { - t.Fatalf("expected to get underlying properties, status code %d", resp.HTTPResponse.StatusCode) + t.Errorf("expected to get underlying properties, status code %d", resp.HTTPResponse.StatusCode) } if api.StringValue(properties.StorageClass) != expensiveString { diff --git a/pkg/gateway/operations/getobject.go b/pkg/gateway/operations/getobject.go index 6a56ab82086..2ab4d59cd0a 100644 --- a/pkg/gateway/operations/getobject.go +++ b/pkg/gateway/operations/getobject.go @@ -10,7 +10,6 @@ import ( "github.com/treeverse/lakefs/pkg/block" "github.com/treeverse/lakefs/pkg/catalog" gatewayerrors "github.com/treeverse/lakefs/pkg/gateway/errors" - ghttp "github.com/treeverse/lakefs/pkg/gateway/http" "github.com/treeverse/lakefs/pkg/gateway/serde" "github.com/treeverse/lakefs/pkg/graveler" "github.com/treeverse/lakefs/pkg/httputil" @@ -71,11 +70,11 @@ func (controller *GetObject) Handle(w http.ResponseWriter, req *http.Request, o // range query var expected int64 var data io.ReadCloser - var rng ghttp.Range + var rng httputil.Range // range query rangeSpec := req.Header.Get("Range") if len(rangeSpec) > 0 { - rng, err = ghttp.ParseRange(rangeSpec, entry.Size) + rng, err = httputil.ParseRange(rangeSpec, entry.Size) if err != nil { o.Log(req).WithError(err).WithField("range", rangeSpec).Debug("invalid range spec") } diff --git a/pkg/gateway/operations/putobject.go b/pkg/gateway/operations/putobject.go index 935ca12eea1..8f6537950a3 100644 --- a/pkg/gateway/operations/putobject.go +++ b/pkg/gateway/operations/putobject.go @@ -11,7 +11,6 @@ import ( "github.com/treeverse/lakefs/pkg/block" "github.com/treeverse/lakefs/pkg/catalog" gatewayErrors "github.com/treeverse/lakefs/pkg/gateway/errors" - ghttp "github.com/treeverse/lakefs/pkg/gateway/http" "github.com/treeverse/lakefs/pkg/gateway/path" "github.com/treeverse/lakefs/pkg/gateway/serde" "github.com/treeverse/lakefs/pkg/graveler" @@ -222,7 +221,7 @@ func handleUploadPart(w http.ResponseWriter, req *http.Request, o *PathOperation var resp *block.UploadPartResponse if rang := req.Header.Get(CopySourceRangeHeader); rang != "" { // if this is a copy part with a byte range: - parsedRange, parseErr := ghttp.ParseRange(rang, ent.Size) + parsedRange, parseErr := httputil.ParseRange(rang, ent.Size) if parseErr != nil { // invalid range will silently fallback to copying the entire object. ¯\_(ツ)_/¯ resp, err = o.BlockStore.UploadCopyPart(req.Context(), src, dst, uploadID, partNumber) diff --git a/pkg/gateway/http/range.go b/pkg/httputil/range.go similarity index 99% rename from pkg/gateway/http/range.go rename to pkg/httputil/range.go index 4553bfd0dbe..1405242e639 100644 --- a/pkg/gateway/http/range.go +++ b/pkg/httputil/range.go @@ -1,4 +1,4 @@ -package http +package httputil import ( "fmt" diff --git a/pkg/gateway/http/range_test.go b/pkg/httputil/range_test.go similarity index 90% rename from pkg/gateway/http/range_test.go rename to pkg/httputil/range_test.go index dc3cc8da80d..f7e46e53373 100644 --- a/pkg/gateway/http/range_test.go +++ b/pkg/httputil/range_test.go @@ -1,10 +1,10 @@ -package http_test +package httputil_test import ( "fmt" "testing" - "github.com/treeverse/lakefs/pkg/gateway/http" + "github.com/treeverse/lakefs/pkg/httputil" ) func TestParseRange(t *testing.T) { @@ -33,7 +33,7 @@ func TestParseRange(t *testing.T) { for _, c := range cases { t.Run(fmt.Sprintf("%s_length_%d", c.Spec, c.Length), func(t *testing.T) { - r, err := http.ParseRange(c.Spec, int64(c.Length)) + r, err := httputil.ParseRange(c.Spec, int64(c.Length)) if (err != nil) != c.ExpectedError { t.Fatalf("got err=%s, expected error %t", err, c.ExpectedError) }