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) {