diff --git a/api/swagger.yml b/api/swagger.yml index 6f20673f3ba..5ff14ddc041 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -3850,6 +3850,13 @@ paths: schema: type: string pattern: '^bytes=((\d*-\d*,? ?)+)$' + - in: header + name: If-None-Match + description: Returns response only if the object does not have a matching ETag + example: "33a64df551425fcc55e4d42a148795d9f25f89d4" + required: false + schema: + type: string - in: query name: presign required: false @@ -3902,6 +3909,8 @@ paths: Location: schema: type: string + 304: + description: Content Not modified 401: $ref: "#/components/responses/Unauthorized" 404: diff --git a/clients/java-legacy/api/openapi.yaml b/clients/java-legacy/api/openapi.yaml index bcb5dc69922..3b9711a9c90 100644 --- a/clients/java-legacy/api/openapi.yaml +++ b/clients/java-legacy/api/openapi.yaml @@ -3990,6 +3990,16 @@ paths: pattern: ^bytes=((\d*-\d*,? ?)+)$ type: string style: simple + - description: Returns response only if the object does not have a matching + ETag + example: 33a64df551425fcc55e4d42a148795d9f25f89d4 + explode: false + in: header + name: If-None-Match + required: false + schema: + type: string + style: simple - explode: true in: query name: presign @@ -4060,6 +4070,8 @@ paths: schema: type: string style: simple + "304": + description: Content Not modified "401": content: application/json: diff --git a/clients/java-legacy/docs/ObjectsApi.md b/clients/java-legacy/docs/ObjectsApi.md index 1b325d1f9f4..b412a44ecc5 100644 --- a/clients/java-legacy/docs/ObjectsApi.md +++ b/clients/java-legacy/docs/ObjectsApi.md @@ -314,7 +314,7 @@ Name | Type | Description | Notes # **getObject** -> File getObject(repository, ref, path, range, presign) +> File getObject(repository, ref, path, range, ifNoneMatch, presign) get object content @@ -365,9 +365,10 @@ public class Example { 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 + String ifNoneMatch = "33a64df551425fcc55e4d42a148795d9f25f89d4"; // String | Returns response only if the object does not have a matching ETag Boolean presign = true; // Boolean | try { - File result = apiInstance.getObject(repository, ref, path, range, presign); + File result = apiInstance.getObject(repository, ref, path, range, ifNoneMatch, presign); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling ObjectsApi#getObject"); @@ -388,6 +389,7 @@ Name | Type | Description | Notes **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] + **ifNoneMatch** | **String**| Returns response only if the object does not have a matching ETag | [optional] **presign** | **Boolean**| | [optional] ### Return type @@ -409,6 +411,7 @@ Name | Type | Description | Notes **200** | object content | * Content-Length -
* Last-Modified -
* ETag -
| **206** | partial object content | * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
| **302** | Redirect to a pre-signed URL for the object | * Location - redirect to S3
| +**304** | Content Not modified | - | **401** | Unauthorized | - | **404** | Resource Not Found | - | **410** | object expired | - | diff --git a/clients/java-legacy/src/main/java/io/lakefs/clients/api/ObjectsApi.java b/clients/java-legacy/src/main/java/io/lakefs/clients/api/ObjectsApi.java index 15d0f5d2e14..6470a40bb89 100644 --- a/clients/java-legacy/src/main/java/io/lakefs/clients/api/ObjectsApi.java +++ b/clients/java-legacy/src/main/java/io/lakefs/clients/api/ObjectsApi.java @@ -553,6 +553,7 @@ public okhttp3.Call deleteObjectsAsync(String repository, String branch, PathLis * @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 ifNoneMatch Returns response only if the object does not have a matching ETag (optional) * @param presign (optional) * @param _callback Callback for upload/download progress * @return Call to execute @@ -563,6 +564,7 @@ public okhttp3.Call deleteObjectsAsync(String repository, String branch, PathLis 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - @@ -571,7 +573,7 @@ public okhttp3.Call deleteObjectsAsync(String repository, String branch, PathLis 0 Internal Server Error - */ - public okhttp3.Call getObjectCall(String repository, String ref, String path, String range, Boolean presign, final ApiCallback _callback) throws ApiException { + public okhttp3.Call getObjectCall(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign, final ApiCallback _callback) throws ApiException { Object localVarPostBody = null; // create path and map variables @@ -597,6 +599,10 @@ public okhttp3.Call getObjectCall(String repository, String ref, String path, St localVarHeaderParams.put("Range", localVarApiClient.parameterToString(range)); } + if (ifNoneMatch != null) { + localVarHeaderParams.put("If-None-Match", localVarApiClient.parameterToString(ifNoneMatch)); + } + final String[] localVarAccepts = { "application/octet-stream", "application/json" }; @@ -616,7 +622,7 @@ public okhttp3.Call getObjectCall(String repository, String ref, String path, St } @SuppressWarnings("rawtypes") - private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, String path, String range, Boolean presign, final ApiCallback _callback) throws ApiException { + private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign, final ApiCallback _callback) throws ApiException { // verify the required parameter 'repository' is set if (repository == null) { @@ -634,7 +640,7 @@ private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, } - okhttp3.Call localVarCall = getObjectCall(repository, ref, path, range, presign, _callback); + okhttp3.Call localVarCall = getObjectCall(repository, ref, path, range, ifNoneMatch, presign, _callback); return localVarCall; } @@ -646,6 +652,7 @@ private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, * @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 ifNoneMatch Returns response only if the object does not have a matching ETag (optional) * @param presign (optional) * @return File * @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body @@ -655,6 +662,7 @@ private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - @@ -663,8 +671,8 @@ private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, 0 Internal Server Error - */ - public File getObject(String repository, String ref, String path, String range, Boolean presign) throws ApiException { - ApiResponse localVarResp = getObjectWithHttpInfo(repository, ref, path, range, presign); + public File getObject(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign) throws ApiException { + ApiResponse localVarResp = getObjectWithHttpInfo(repository, ref, path, range, ifNoneMatch, presign); return localVarResp.getData(); } @@ -675,6 +683,7 @@ public File getObject(String repository, String ref, String path, String range, * @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 ifNoneMatch Returns response only if the object does not have a matching ETag (optional) * @param presign (optional) * @return ApiResponse<File> * @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body @@ -684,6 +693,7 @@ public File getObject(String repository, String ref, String path, String range, 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - @@ -692,8 +702,8 @@ public File getObject(String repository, String ref, String path, String range, 0 Internal Server Error - */ - public ApiResponse getObjectWithHttpInfo(String repository, String ref, String path, String range, Boolean presign) throws ApiException { - okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, presign, null); + public ApiResponse getObjectWithHttpInfo(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign) throws ApiException { + okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, ifNoneMatch, presign, null); Type localVarReturnType = new TypeToken(){}.getType(); return localVarApiClient.execute(localVarCall, localVarReturnType); } @@ -705,6 +715,7 @@ public ApiResponse getObjectWithHttpInfo(String repository, String ref, St * @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 ifNoneMatch Returns response only if the object does not have a matching ETag (optional) * @param presign (optional) * @param _callback The callback to be executed when the API call finishes * @return The request call @@ -715,6 +726,7 @@ public ApiResponse getObjectWithHttpInfo(String repository, String ref, St 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - @@ -723,9 +735,9 @@ public ApiResponse getObjectWithHttpInfo(String repository, String ref, St 0 Internal Server Error - */ - public okhttp3.Call getObjectAsync(String repository, String ref, String path, String range, Boolean presign, final ApiCallback _callback) throws ApiException { + public okhttp3.Call getObjectAsync(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign, final ApiCallback _callback) throws ApiException { - okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, presign, _callback); + okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, ifNoneMatch, presign, _callback); Type localVarReturnType = new TypeToken(){}.getType(); localVarApiClient.executeAsync(localVarCall, localVarReturnType, _callback); return localVarCall; diff --git a/clients/java-legacy/src/test/java/io/lakefs/clients/api/ObjectsApiTest.java b/clients/java-legacy/src/test/java/io/lakefs/clients/api/ObjectsApiTest.java index 1ddf9b32ae0..750fb22f5e4 100644 --- a/clients/java-legacy/src/test/java/io/lakefs/clients/api/ObjectsApiTest.java +++ b/clients/java-legacy/src/test/java/io/lakefs/clients/api/ObjectsApiTest.java @@ -107,8 +107,9 @@ public void getObjectTest() throws ApiException { String ref = null; String path = null; String range = null; + String ifNoneMatch = null; Boolean presign = null; - File response = api.getObject(repository, ref, path, range, presign); + File response = api.getObject(repository, ref, path, range, ifNoneMatch, presign); // TODO: test validations } diff --git a/clients/java/api/openapi.yaml b/clients/java/api/openapi.yaml index fb82ccf9b35..d04c45908fb 100644 --- a/clients/java/api/openapi.yaml +++ b/clients/java/api/openapi.yaml @@ -3990,6 +3990,16 @@ paths: pattern: "^bytes=((\\d*-\\d*,? ?)+)$" type: string style: simple + - description: Returns response only if the object does not have a matching + ETag + example: 33a64df551425fcc55e4d42a148795d9f25f89d4 + explode: false + in: header + name: If-None-Match + required: false + schema: + type: string + style: simple - explode: true in: query name: presign @@ -4060,6 +4070,8 @@ paths: schema: type: string style: simple + "304": + description: Content Not modified "401": content: application/json: diff --git a/clients/java/docs/ObjectsApi.md b/clients/java/docs/ObjectsApi.md index eed9bdf7e76..0481e83e995 100644 --- a/clients/java/docs/ObjectsApi.md +++ b/clients/java/docs/ObjectsApi.md @@ -319,7 +319,7 @@ public class Example { # **getObject** -> File getObject(repository, ref, path).range(range).presign(presign).execute(); +> File getObject(repository, ref, path).range(range).ifNoneMatch(ifNoneMatch).presign(presign).execute(); get object content @@ -370,10 +370,12 @@ public class Example { 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 + String ifNoneMatch = "33a64df551425fcc55e4d42a148795d9f25f89d4"; // String | Returns response only if the object does not have a matching ETag Boolean presign = true; // Boolean | try { File result = apiInstance.getObject(repository, ref, path) .range(range) + .ifNoneMatch(ifNoneMatch) .presign(presign) .execute(); System.out.println(result); @@ -396,6 +398,7 @@ public class Example { | **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] | +| **ifNoneMatch** | **String**| Returns response only if the object does not have a matching ETag | [optional] | | **presign** | **Boolean**| | [optional] | ### Return type @@ -417,6 +420,7 @@ public class Example { | **200** | object content | * Content-Length -
* Last-Modified -
* ETag -
| | **206** | partial object content | * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
| | **302** | Redirect to a pre-signed URL for the object | * Location - redirect to S3
| +| **304** | Content Not modified | - | | **401** | Unauthorized | - | | **404** | Resource Not Found | - | | **410** | object expired | - | diff --git a/clients/java/src/main/java/io/lakefs/clients/sdk/ObjectsApi.java b/clients/java/src/main/java/io/lakefs/clients/sdk/ObjectsApi.java index 061b128f4d6..9bc1f58b9cc 100644 --- a/clients/java/src/main/java/io/lakefs/clients/sdk/ObjectsApi.java +++ b/clients/java/src/main/java/io/lakefs/clients/sdk/ObjectsApi.java @@ -710,7 +710,7 @@ public okhttp3.Call executeAsync(final ApiCallback _callback) t public APIdeleteObjectsRequest deleteObjects(String repository, String branch, PathList pathList) { return new APIdeleteObjectsRequest(repository, branch, pathList); } - private okhttp3.Call getObjectCall(String repository, String ref, String path, String range, Boolean presign, final ApiCallback _callback) throws ApiException { + private okhttp3.Call getObjectCall(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign, final ApiCallback _callback) throws ApiException { String basePath = null; // Operation Servers String[] localBasePaths = new String[] { }; @@ -749,6 +749,10 @@ private okhttp3.Call getObjectCall(String repository, String ref, String path, S localVarHeaderParams.put("Range", localVarApiClient.parameterToString(range)); } + if (ifNoneMatch != null) { + localVarHeaderParams.put("If-None-Match", localVarApiClient.parameterToString(ifNoneMatch)); + } + final String[] localVarAccepts = { "application/octet-stream", "application/json" @@ -770,7 +774,7 @@ private okhttp3.Call getObjectCall(String repository, String ref, String path, S } @SuppressWarnings("rawtypes") - private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, String path, String range, Boolean presign, final ApiCallback _callback) throws ApiException { + private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign, final ApiCallback _callback) throws ApiException { // verify the required parameter 'repository' is set if (repository == null) { throw new ApiException("Missing the required parameter 'repository' when calling getObject(Async)"); @@ -786,20 +790,20 @@ private okhttp3.Call getObjectValidateBeforeCall(String repository, String ref, throw new ApiException("Missing the required parameter 'path' when calling getObject(Async)"); } - return getObjectCall(repository, ref, path, range, presign, _callback); + return getObjectCall(repository, ref, path, range, ifNoneMatch, presign, _callback); } - private ApiResponse getObjectWithHttpInfo(String repository, String ref, String path, String range, Boolean presign) throws ApiException { - okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, presign, null); + private ApiResponse getObjectWithHttpInfo(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign) throws ApiException { + okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, ifNoneMatch, presign, null); Type localVarReturnType = new TypeToken(){}.getType(); return localVarApiClient.execute(localVarCall, localVarReturnType); } - private okhttp3.Call getObjectAsync(String repository, String ref, String path, String range, Boolean presign, final ApiCallback _callback) throws ApiException { + private okhttp3.Call getObjectAsync(String repository, String ref, String path, String range, String ifNoneMatch, Boolean presign, final ApiCallback _callback) throws ApiException { - okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, presign, _callback); + okhttp3.Call localVarCall = getObjectValidateBeforeCall(repository, ref, path, range, ifNoneMatch, presign, _callback); Type localVarReturnType = new TypeToken(){}.getType(); localVarApiClient.executeAsync(localVarCall, localVarReturnType, _callback); return localVarCall; @@ -810,6 +814,7 @@ public class APIgetObjectRequest { private final String ref; private final String path; private String range; + private String ifNoneMatch; private Boolean presign; private APIgetObjectRequest(String repository, String ref, String path) { @@ -828,6 +833,16 @@ public APIgetObjectRequest range(String range) { return this; } + /** + * Set ifNoneMatch + * @param ifNoneMatch Returns response only if the object does not have a matching ETag (optional) + * @return APIgetObjectRequest + */ + public APIgetObjectRequest ifNoneMatch(String ifNoneMatch) { + this.ifNoneMatch = ifNoneMatch; + return this; + } + /** * Set presign * @param presign (optional) @@ -849,6 +864,7 @@ public APIgetObjectRequest presign(Boolean presign) { 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - @@ -858,7 +874,7 @@ public APIgetObjectRequest presign(Boolean presign) { */ public okhttp3.Call buildCall(final ApiCallback _callback) throws ApiException { - return getObjectCall(repository, ref, path, range, presign, _callback); + return getObjectCall(repository, ref, path, range, ifNoneMatch, presign, _callback); } /** @@ -871,6 +887,7 @@ public okhttp3.Call buildCall(final ApiCallback _callback) throws ApiException { 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - @@ -880,7 +897,7 @@ public okhttp3.Call buildCall(final ApiCallback _callback) throws ApiException { */ public File execute() throws ApiException { - ApiResponse localVarResp = getObjectWithHttpInfo(repository, ref, path, range, presign); + ApiResponse localVarResp = getObjectWithHttpInfo(repository, ref, path, range, ifNoneMatch, presign); return localVarResp.getData(); } @@ -894,6 +911,7 @@ public File execute() throws ApiException { 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - @@ -903,7 +921,7 @@ public File execute() throws ApiException { */ public ApiResponse executeWithHttpInfo() throws ApiException { - return getObjectWithHttpInfo(repository, ref, path, range, presign); + return getObjectWithHttpInfo(repository, ref, path, range, ifNoneMatch, presign); } /** @@ -917,6 +935,7 @@ public ApiResponse executeWithHttpInfo() throws ApiException { 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - @@ -926,7 +945,7 @@ public ApiResponse executeWithHttpInfo() throws ApiException { */ public okhttp3.Call executeAsync(final ApiCallback _callback) throws ApiException { - return getObjectAsync(repository, ref, path, range, presign, _callback); + return getObjectAsync(repository, ref, path, range, ifNoneMatch, presign, _callback); } } @@ -943,6 +962,7 @@ public okhttp3.Call executeAsync(final ApiCallback _callback) throws ApiEx 200 object content * Content-Length -
* Last-Modified -
* ETag -
206 partial object content * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
302 Redirect to a pre-signed URL for the object * Location - redirect to S3
+ 304 Content Not modified - 401 Unauthorized - 404 Resource Not Found - 410 object expired - diff --git a/clients/java/src/test/java/io/lakefs/clients/sdk/ObjectsApiTest.java b/clients/java/src/test/java/io/lakefs/clients/sdk/ObjectsApiTest.java index 54c9e54f93f..4580efc6547 100644 --- a/clients/java/src/test/java/io/lakefs/clients/sdk/ObjectsApiTest.java +++ b/clients/java/src/test/java/io/lakefs/clients/sdk/ObjectsApiTest.java @@ -99,9 +99,11 @@ public void getObjectTest() throws ApiException { String ref = null; String path = null; String range = null; + String ifNoneMatch = null; Boolean presign = null; File response = api.getObject(repository, ref, path) .range(range) + .ifNoneMatch(ifNoneMatch) .presign(presign) .execute(); // TODO: test validations diff --git a/clients/python-legacy/docs/ObjectsApi.md b/clients/python-legacy/docs/ObjectsApi.md index a09fcbad20e..01032ddd654 100644 --- a/clients/python-legacy/docs/ObjectsApi.md +++ b/clients/python-legacy/docs/ObjectsApi.md @@ -453,6 +453,7 @@ with lakefs_client.ApiClient(configuration) as api_client: 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) + if_none_match = "33a64df551425fcc55e4d42a148795d9f25f89d4" # str | Returns response only if the object does not have a matching ETag (optional) presign = True # bool | (optional) # example passing only required values which don't have defaults set @@ -467,7 +468,7 @@ with lakefs_client.ApiClient(configuration) as api_client: # and optional values try: # get object content - api_response = api_instance.get_object(repository, ref, path, range=range, presign=presign) + api_response = api_instance.get_object(repository, ref, path, range=range, if_none_match=if_none_match, presign=presign) pprint(api_response) except lakefs_client.ApiException as e: print("Exception when calling ObjectsApi->get_object: %s\n" % e) @@ -482,6 +483,7 @@ Name | Type | Description | Notes **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] + **if_none_match** | **str**| Returns response only if the object does not have a matching ETag | [optional] **presign** | **bool**| | [optional] ### Return type @@ -505,6 +507,7 @@ Name | Type | Description | Notes **200** | object content | * Content-Length -
* Last-Modified -
* ETag -
| **206** | partial object content | * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
| **302** | Redirect to a pre-signed URL for the object | * Location - redirect to S3
| +**304** | Content Not modified | - | **401** | Unauthorized | - | **404** | Resource Not Found | - | **410** | object expired | - | diff --git a/clients/python-legacy/lakefs_client/api/objects_api.py b/clients/python-legacy/lakefs_client/api/objects_api.py index 12bf9cf990c..5941b74039c 100644 --- a/clients/python-legacy/lakefs_client/api/objects_api.py +++ b/clients/python-legacy/lakefs_client/api/objects_api.py @@ -282,6 +282,7 @@ def __init__(self, api_client=None): 'ref', 'path', 'range', + 'if_none_match', 'presign', ], 'required': [ @@ -317,6 +318,8 @@ def __init__(self, api_client=None): (str,), 'range': (str,), + 'if_none_match': + (str,), 'presign': (bool,), }, @@ -325,6 +328,7 @@ def __init__(self, api_client=None): 'ref': 'ref', 'path': 'path', 'range': 'Range', + 'if_none_match': 'If-None-Match', 'presign': 'presign', }, 'location_map': { @@ -332,6 +336,7 @@ def __init__(self, api_client=None): 'ref': 'path', 'path': 'query', 'range': 'header', + 'if_none_match': 'header', 'presign': 'query', }, 'collection_format_map': { @@ -1002,6 +1007,7 @@ def get_object( Keyword Args: range (str): Byte range to retrieve. [optional] + if_none_match (str): Returns response only if the object does not have a matching ETag. [optional] presign (bool): [optional] _return_http_data_only (bool): response data without head status code and headers. Default is True. diff --git a/clients/python/docs/ObjectsApi.md b/clients/python/docs/ObjectsApi.md index 64470da2cb5..fa7af142ea9 100644 --- a/clients/python/docs/ObjectsApi.md +++ b/clients/python/docs/ObjectsApi.md @@ -367,7 +367,7 @@ Name | Type | Description | Notes [[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) # **get_object** -> bytearray get_object(repository, ref, path, range=range, presign=presign) +> bytearray get_object(repository, ref, path, range=range, if_none_match=if_none_match, presign=presign) get object content @@ -434,11 +434,12 @@ with lakefs_sdk.ApiClient(configuration) as api_client: 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) + if_none_match = '33a64df551425fcc55e4d42a148795d9f25f89d4' # str | Returns response only if the object does not have a matching ETag (optional) presign = True # bool | (optional) try: # get object content - api_response = api_instance.get_object(repository, ref, path, range=range, presign=presign) + api_response = api_instance.get_object(repository, ref, path, range=range, if_none_match=if_none_match, presign=presign) print("The response of ObjectsApi->get_object:\n") pprint(api_response) except Exception as e: @@ -456,6 +457,7 @@ Name | Type | Description | Notes **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] + **if_none_match** | **str**| Returns response only if the object does not have a matching ETag | [optional] **presign** | **bool**| | [optional] ### Return type @@ -478,6 +480,7 @@ Name | Type | Description | Notes **200** | object content | * Content-Length -
* Last-Modified -
* ETag -
| **206** | partial object content | * Content-Length -
* Content-Range -
* Last-Modified -
* ETag -
| **302** | Redirect to a pre-signed URL for the object | * Location - redirect to S3
| +**304** | Content Not modified | - | **401** | Unauthorized | - | **404** | Resource Not Found | - | **410** | object expired | - | diff --git a/clients/python/lakefs_sdk/api/objects_api.py b/clients/python/lakefs_sdk/api/objects_api.py index 2dc5501a36b..11a5229f3b6 100644 --- a/clients/python/lakefs_sdk/api/objects_api.py +++ b/clients/python/lakefs_sdk/api/objects_api.py @@ -556,13 +556,13 @@ def delete_objects_with_http_info(self, repository : StrictStr, branch : StrictS _request_auth=_params.get('_request_auth')) @validate_arguments - def get_object(self, repository : StrictStr, ref : Annotated[StrictStr, Field(..., description="a reference (could be either a branch or a commit ID)")], path : Annotated[StrictStr, Field(..., description="relative to the ref")], range : Annotated[Optional[constr(strict=True)], Field(description="Byte range to retrieve")] = None, presign : Optional[StrictBool] = None, **kwargs) -> bytearray: # noqa: E501 + def get_object(self, repository : StrictStr, ref : Annotated[StrictStr, Field(..., description="a reference (could be either a branch or a commit ID)")], path : Annotated[StrictStr, Field(..., description="relative to the ref")], range : Annotated[Optional[constr(strict=True)], Field(description="Byte range to retrieve")] = None, if_none_match : Annotated[Optional[StrictStr], Field(description="Returns response only if the object does not have a matching ETag")] = None, presign : Optional[StrictBool] = None, **kwargs) -> bytearray: # noqa: E501 """get object content # noqa: E501 This method makes a synchronous HTTP request by default. To make an asynchronous HTTP request, please pass async_req=True - >>> thread = api.get_object(repository, ref, path, range, presign, async_req=True) + >>> thread = api.get_object(repository, ref, path, range, if_none_match, presign, async_req=True) >>> result = thread.get() :param repository: (required) @@ -573,6 +573,8 @@ def get_object(self, repository : StrictStr, ref : Annotated[StrictStr, Field(.. :type path: str :param range: Byte range to retrieve :type range: str + :param if_none_match: Returns response only if the object does not have a matching ETag + :type if_none_match: str :param presign: :type presign: bool :param async_req: Whether to execute the request asynchronously. @@ -589,16 +591,16 @@ def get_object(self, repository : StrictStr, ref : Annotated[StrictStr, Field(.. kwargs['_return_http_data_only'] = True if '_preload_content' in kwargs: raise ValueError("Error! Please call the get_object_with_http_info method with `_preload_content` instead and obtain raw data from ApiResponse.raw_data") - return self.get_object_with_http_info(repository, ref, path, range, presign, **kwargs) # noqa: E501 + return self.get_object_with_http_info(repository, ref, path, range, if_none_match, presign, **kwargs) # noqa: E501 @validate_arguments - def get_object_with_http_info(self, repository : StrictStr, ref : Annotated[StrictStr, Field(..., description="a reference (could be either a branch or a commit ID)")], path : Annotated[StrictStr, Field(..., description="relative to the ref")], range : Annotated[Optional[constr(strict=True)], Field(description="Byte range to retrieve")] = None, presign : Optional[StrictBool] = None, **kwargs) -> ApiResponse: # noqa: E501 + def get_object_with_http_info(self, repository : StrictStr, ref : Annotated[StrictStr, Field(..., description="a reference (could be either a branch or a commit ID)")], path : Annotated[StrictStr, Field(..., description="relative to the ref")], range : Annotated[Optional[constr(strict=True)], Field(description="Byte range to retrieve")] = None, if_none_match : Annotated[Optional[StrictStr], Field(description="Returns response only if the object does not have a matching ETag")] = None, presign : Optional[StrictBool] = None, **kwargs) -> ApiResponse: # noqa: E501 """get object content # noqa: E501 This method makes a synchronous HTTP request by default. To make an asynchronous HTTP request, please pass async_req=True - >>> thread = api.get_object_with_http_info(repository, ref, path, range, presign, async_req=True) + >>> thread = api.get_object_with_http_info(repository, ref, path, range, if_none_match, presign, async_req=True) >>> result = thread.get() :param repository: (required) @@ -609,6 +611,8 @@ def get_object_with_http_info(self, repository : StrictStr, ref : Annotated[Stri :type path: str :param range: Byte range to retrieve :type range: str + :param if_none_match: Returns response only if the object does not have a matching ETag + :type if_none_match: str :param presign: :type presign: bool :param async_req: Whether to execute the request asynchronously. @@ -643,6 +647,7 @@ def get_object_with_http_info(self, repository : StrictStr, ref : Annotated[Stri 'ref', 'path', 'range', + 'if_none_match', 'presign' ] _all_params.extend( @@ -691,6 +696,9 @@ def get_object_with_http_info(self, repository : StrictStr, ref : Annotated[Stri if _params['range']: _header_params['Range'] = _params['range'] + if _params['if_none_match']: + _header_params['If-None-Match'] = _params['if_none_match'] + # process the form parameters _form_params = [] _files = {} @@ -707,6 +715,7 @@ def get_object_with_http_info(self, repository : StrictStr, ref : Annotated[Stri '200': "bytearray", '206': "bytearray", '302': None, + '304': None, '401': "Error", '404': "Error", '410': "Error", diff --git a/docs/assets/js/swagger.yml b/docs/assets/js/swagger.yml index 6f20673f3ba..5ff14ddc041 100644 --- a/docs/assets/js/swagger.yml +++ b/docs/assets/js/swagger.yml @@ -3850,6 +3850,13 @@ paths: schema: type: string pattern: '^bytes=((\d*-\d*,? ?)+)$' + - in: header + name: If-None-Match + description: Returns response only if the object does not have a matching ETag + example: "33a64df551425fcc55e4d42a148795d9f25f89d4" + required: false + schema: + type: string - in: query name: presign required: false @@ -3902,6 +3909,8 @@ paths: Location: schema: type: string + 304: + description: Content Not modified 401: $ref: "#/components/responses/Unauthorized" 404: diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 904f2331996..4ab2aaa9317 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -4197,6 +4197,14 @@ func (c *Controller) GetObject(w http.ResponseWriter, r *http.Request, repositor return } + etag := httputil.ETag(entry.Checksum) + + // check ETag if not modified in request + if swag.StringValue(params.IfNoneMatch) == etag { + w.WriteHeader(http.StatusNotModified) + return + } + // if pre-sign, return a redirect pointer := block.ObjectPointer{ StorageNamespace: repo.StorageNamespace, @@ -4214,7 +4222,6 @@ func (c *Controller) GetObject(w http.ResponseWriter, r *http.Request, repositor } // set response headers - etag := httputil.ETag(entry.Checksum) w.Header().Set("ETag", etag) lastModified := httputil.HeaderTimestamp(entry.CreationDate) w.Header().Set("Last-Modified", lastModified) diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index e2a5b4d1079..ce56d8eb20f 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -47,6 +47,7 @@ import ( const ( DefaultUserID = "example_user" + DefaultETag = `"3c4838fe975c762ee97cf39fbbe566f1"` ) type Statuser interface { @@ -2506,25 +2507,11 @@ func TestController_ObjectsHeadObjectHandler(t *testing.T) { t.Run("head object", func(t *testing.T) { resp, err := clt.HeadObjectWithResponse(ctx, repo, "main", &apigen.HeadObjectParams{Path: "foo/bar"}) - if err != nil { - t.Fatal(err) - } - if resp.HTTPResponse.StatusCode != http.StatusOK { - t.Errorf("HeadObject() status code %d, expected %d", resp.HTTPResponse.StatusCode, http.StatusOK) - } - - if resp.HTTPResponse.ContentLength != 37 { - t.Errorf("expected 37 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 != "" { - t.Errorf("got unexpected body: '%s'", body) - } + require.Nil(t, err) + require.Equal(t, http.StatusOK, resp.HTTPResponse.StatusCode) + require.Equal(t, int64(37), resp.HTTPResponse.ContentLength) + require.Equal(t, DefaultETag, resp.HTTPResponse.Header.Get("ETag")) + require.Empty(t, string(resp.Body)) }) t.Run("head object byte range", func(t *testing.T) { @@ -2533,26 +2520,11 @@ func TestController_ObjectsHeadObjectHandler(t *testing.T) { Path: "foo/bar", Range: &rng, }) - if err != nil { - t.Fatal(err) - } - if resp.HTTPResponse.StatusCode != http.StatusPartialContent { - t.Errorf("HeadObject() 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 != "" { - t.Errorf("got unexpected body: '%s'", body) - } + require.Nil(t, err) + require.Equal(t, http.StatusPartialContent, resp.HTTPResponse.StatusCode) + require.Equal(t, int64(10), resp.HTTPResponse.ContentLength) + require.Equal(t, DefaultETag, resp.HTTPResponse.Header.Get("ETag")) + require.Empty(t, string(resp.Body)) }) t.Run("head object bad byte range", func(t *testing.T) { @@ -2621,7 +2593,7 @@ func TestController_ObjectsGetObjectHandler(t *testing.T) { t.Errorf("expected 37 bytes in content length, got back %d", resp.HTTPResponse.ContentLength) } etag := resp.HTTPResponse.Header.Get("ETag") - if etag != `"3c4838fe975c762ee97cf39fbbe566f1"` { + if etag != DefaultETag { t.Errorf("got unexpected etag: %s", etag) } @@ -2649,7 +2621,7 @@ func TestController_ObjectsGetObjectHandler(t *testing.T) { } etag := resp.HTTPResponse.Header.Get("ETag") - if etag != `"3c4838fe975c762ee97cf39fbbe566f1"` { + if etag != DefaultETag { t.Errorf("got unexpected etag: %s", etag) } @@ -2687,6 +2659,31 @@ func TestController_ObjectsGetObjectHandler(t *testing.T) { t.Errorf("expected to get \"%s\" storage class, got %#v", expensiveString, properties) } }) + + t.Run("get object returns expected response with different etag", func(t *testing.T) { + newChecksum := `"11ee22ff33445566778899"` + eTagInput := "\"" + newChecksum + "\"" + resp, err := clt.GetObjectWithResponse(ctx, repo, "main", &apigen.GetObjectParams{ + Path: "foo/bar", + IfNoneMatch: &eTagInput, + }) + require.Nil(t, err) + require.Equal(t, http.StatusOK, resp.HTTPResponse.StatusCode) + require.Equal(t, int64(37), resp.HTTPResponse.ContentLength) + require.Equal(t, DefaultETag, resp.HTTPResponse.Header.Get("ETag")) + }) + + t.Run("get object returns not modified with same etag", func(t *testing.T) { + eTagInput := "\"" + blob.Checksum + "\"" + resp, err := clt.GetObjectWithResponse(ctx, repo, "main", &apigen.GetObjectParams{ + Path: "foo/bar", + IfNoneMatch: &eTagInput, + }) + require.Nil(t, err) + require.Equal(t, http.StatusNotModified, resp.HTTPResponse.StatusCode) + require.Equal(t, int64(0), resp.HTTPResponse.ContentLength) + require.Empty(t, resp.HTTPResponse.Header.Get("ETag")) + }) } func TestController_ObjectsUploadObjectHandler(t *testing.T) {