diff --git a/api/swagger.yml b/api/swagger.yml index 37db50e468f..6c17051cff6 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -1299,6 +1299,9 @@ components: force: type: boolean default: false + if_absent: + type: boolean + default: false required: - staging - checksum diff --git a/clients/java-legacy/api/openapi.yaml b/clients/java-legacy/api/openapi.yaml index f0de60dd1c7..180170845e3 100644 --- a/clients/java-legacy/api/openapi.yaml +++ b/clients/java-legacy/api/openapi.yaml @@ -8269,6 +8269,7 @@ components: StagingMetadata: description: information about uploaded object example: + if_absent: false size_bytes: 0 user_metadata: key: user_metadata @@ -8299,6 +8300,9 @@ components: force: default: false type: boolean + if_absent: + default: false + type: boolean required: - checksum - size_bytes diff --git a/clients/java-legacy/docs/StagingMetadata.md b/clients/java-legacy/docs/StagingMetadata.md index 4228387befd..13f67097c93 100644 --- a/clients/java-legacy/docs/StagingMetadata.md +++ b/clients/java-legacy/docs/StagingMetadata.md @@ -14,6 +14,7 @@ Name | Type | Description | Notes **userMetadata** | **Map<String, String>** | | [optional] **contentType** | **String** | Object media type | [optional] **force** | **Boolean** | | [optional] +**ifAbsent** | **Boolean** | | [optional] diff --git a/clients/java-legacy/src/main/java/io/lakefs/clients/api/model/StagingMetadata.java b/clients/java-legacy/src/main/java/io/lakefs/clients/api/model/StagingMetadata.java index c8fedda6a6e..88d375b45b5 100644 --- a/clients/java-legacy/src/main/java/io/lakefs/clients/api/model/StagingMetadata.java +++ b/clients/java-legacy/src/main/java/io/lakefs/clients/api/model/StagingMetadata.java @@ -58,6 +58,10 @@ public class StagingMetadata { @SerializedName(SERIALIZED_NAME_FORCE) private Boolean force = false; + public static final String SERIALIZED_NAME_IF_ABSENT = "if_absent"; + @SerializedName(SERIALIZED_NAME_IF_ABSENT) + private Boolean ifAbsent = false; + public StagingMetadata staging(StagingLocation staging) { @@ -205,6 +209,29 @@ public void setForce(Boolean force) { } + public StagingMetadata ifAbsent(Boolean ifAbsent) { + + this.ifAbsent = ifAbsent; + return this; + } + + /** + * Get ifAbsent + * @return ifAbsent + **/ + @javax.annotation.Nullable + @ApiModelProperty(value = "") + + public Boolean getIfAbsent() { + return ifAbsent; + } + + + public void setIfAbsent(Boolean ifAbsent) { + this.ifAbsent = ifAbsent; + } + + @Override public boolean equals(Object o) { if (this == o) { @@ -219,12 +246,13 @@ public boolean equals(Object o) { Objects.equals(this.sizeBytes, stagingMetadata.sizeBytes) && Objects.equals(this.userMetadata, stagingMetadata.userMetadata) && Objects.equals(this.contentType, stagingMetadata.contentType) && - Objects.equals(this.force, stagingMetadata.force); + Objects.equals(this.force, stagingMetadata.force) && + Objects.equals(this.ifAbsent, stagingMetadata.ifAbsent); } @Override public int hashCode() { - return Objects.hash(staging, checksum, sizeBytes, userMetadata, contentType, force); + return Objects.hash(staging, checksum, sizeBytes, userMetadata, contentType, force, ifAbsent); } @Override @@ -237,6 +265,7 @@ public String toString() { sb.append(" userMetadata: ").append(toIndentedString(userMetadata)).append("\n"); sb.append(" contentType: ").append(toIndentedString(contentType)).append("\n"); sb.append(" force: ").append(toIndentedString(force)).append("\n"); + sb.append(" ifAbsent: ").append(toIndentedString(ifAbsent)).append("\n"); sb.append("}"); return sb.toString(); } diff --git a/clients/java-legacy/src/test/java/io/lakefs/clients/api/model/StagingMetadataTest.java b/clients/java-legacy/src/test/java/io/lakefs/clients/api/model/StagingMetadataTest.java index c6f0d0e5b17..2ce9c650219 100644 --- a/clients/java-legacy/src/test/java/io/lakefs/clients/api/model/StagingMetadataTest.java +++ b/clients/java-legacy/src/test/java/io/lakefs/clients/api/model/StagingMetadataTest.java @@ -92,4 +92,12 @@ public void forceTest() { // TODO: test force } + /** + * Test the property 'ifAbsent' + */ + @Test + public void ifAbsentTest() { + // TODO: test ifAbsent + } + } diff --git a/clients/java/api/openapi.yaml b/clients/java/api/openapi.yaml index 6b787ec2eac..af2e9f9715e 100644 --- a/clients/java/api/openapi.yaml +++ b/clients/java/api/openapi.yaml @@ -8243,6 +8243,7 @@ components: StagingMetadata: description: information about uploaded object example: + if_absent: false size_bytes: 0 user_metadata: key: user_metadata @@ -8273,6 +8274,9 @@ components: force: default: false type: boolean + if_absent: + default: false + type: boolean required: - checksum - size_bytes diff --git a/clients/java/docs/StagingMetadata.md b/clients/java/docs/StagingMetadata.md index f80987757bc..76c5649896a 100644 --- a/clients/java/docs/StagingMetadata.md +++ b/clients/java/docs/StagingMetadata.md @@ -14,6 +14,7 @@ information about uploaded object |**userMetadata** | **Map<String, String>** | | [optional] | |**contentType** | **String** | Object media type | [optional] | |**force** | **Boolean** | | [optional] | +|**ifAbsent** | **Boolean** | | [optional] | diff --git a/clients/java/src/main/java/io/lakefs/clients/sdk/model/StagingMetadata.java b/clients/java/src/main/java/io/lakefs/clients/sdk/model/StagingMetadata.java index 0e6ad6001c4..10a93fd3364 100644 --- a/clients/java/src/main/java/io/lakefs/clients/sdk/model/StagingMetadata.java +++ b/clients/java/src/main/java/io/lakefs/clients/sdk/model/StagingMetadata.java @@ -79,6 +79,10 @@ public class StagingMetadata { @SerializedName(SERIALIZED_NAME_FORCE) private Boolean force = false; + public static final String SERIALIZED_NAME_IF_ABSENT = "if_absent"; + @SerializedName(SERIALIZED_NAME_IF_ABSENT) + private Boolean ifAbsent = false; + public StagingMetadata() { } @@ -215,6 +219,27 @@ public void setForce(Boolean force) { this.force = force; } + + public StagingMetadata ifAbsent(Boolean ifAbsent) { + + this.ifAbsent = ifAbsent; + return this; + } + + /** + * Get ifAbsent + * @return ifAbsent + **/ + @javax.annotation.Nullable + public Boolean getIfAbsent() { + return ifAbsent; + } + + + public void setIfAbsent(Boolean ifAbsent) { + this.ifAbsent = ifAbsent; + } + /** * A container for additional, undeclared properties. * This is a holder for any undeclared properties as specified with @@ -275,13 +300,14 @@ public boolean equals(Object o) { Objects.equals(this.sizeBytes, stagingMetadata.sizeBytes) && Objects.equals(this.userMetadata, stagingMetadata.userMetadata) && Objects.equals(this.contentType, stagingMetadata.contentType) && - Objects.equals(this.force, stagingMetadata.force)&& + Objects.equals(this.force, stagingMetadata.force) && + Objects.equals(this.ifAbsent, stagingMetadata.ifAbsent)&& Objects.equals(this.additionalProperties, stagingMetadata.additionalProperties); } @Override public int hashCode() { - return Objects.hash(staging, checksum, sizeBytes, userMetadata, contentType, force, additionalProperties); + return Objects.hash(staging, checksum, sizeBytes, userMetadata, contentType, force, ifAbsent, additionalProperties); } @Override @@ -294,6 +320,7 @@ public String toString() { sb.append(" userMetadata: ").append(toIndentedString(userMetadata)).append("\n"); sb.append(" contentType: ").append(toIndentedString(contentType)).append("\n"); sb.append(" force: ").append(toIndentedString(force)).append("\n"); + sb.append(" ifAbsent: ").append(toIndentedString(ifAbsent)).append("\n"); sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n"); sb.append("}"); return sb.toString(); @@ -323,6 +350,7 @@ private String toIndentedString(Object o) { openapiFields.add("user_metadata"); openapiFields.add("content_type"); openapiFields.add("force"); + openapiFields.add("if_absent"); // a set of required properties/fields (JSON key names) openapiRequiredFields = new HashSet(); diff --git a/clients/java/src/test/java/io/lakefs/clients/sdk/model/StagingMetadataTest.java b/clients/java/src/test/java/io/lakefs/clients/sdk/model/StagingMetadataTest.java index d11a7d6b96e..acf01e2e396 100644 --- a/clients/java/src/test/java/io/lakefs/clients/sdk/model/StagingMetadataTest.java +++ b/clients/java/src/test/java/io/lakefs/clients/sdk/model/StagingMetadataTest.java @@ -88,4 +88,12 @@ public void forceTest() { // TODO: test force } + /** + * Test the property 'ifAbsent' + */ + @Test + public void ifAbsentTest() { + // TODO: test ifAbsent + } + } diff --git a/clients/python-legacy/docs/StagingApi.md b/clients/python-legacy/docs/StagingApi.md index 1cd80fd97e5..3d7b973ee95 100644 --- a/clients/python-legacy/docs/StagingApi.md +++ b/clients/python-legacy/docs/StagingApi.md @@ -215,6 +215,7 @@ with lakefs_client.ApiClient(configuration) as api_client: }, content_type="content_type_example", force=False, + if_absent=False, ) # StagingMetadata | # example passing only required values which don't have defaults set diff --git a/clients/python-legacy/docs/StagingMetadata.md b/clients/python-legacy/docs/StagingMetadata.md index 21cca48909a..f8e4f82bdf1 100644 --- a/clients/python-legacy/docs/StagingMetadata.md +++ b/clients/python-legacy/docs/StagingMetadata.md @@ -11,6 +11,7 @@ Name | Type | Description | Notes **user_metadata** | **{str: (str,)}** | | [optional] **content_type** | **str** | Object media type | [optional] **force** | **bool** | | [optional] if omitted the server will use the default value of False +**if_absent** | **bool** | | [optional] if omitted the server will use the default value of False **any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/clients/python-legacy/lakefs_client/model/staging_metadata.py b/clients/python-legacy/lakefs_client/model/staging_metadata.py index 4e6414fc15c..94e6eb60e00 100644 --- a/clients/python-legacy/lakefs_client/model/staging_metadata.py +++ b/clients/python-legacy/lakefs_client/model/staging_metadata.py @@ -94,6 +94,7 @@ def openapi_types(): 'user_metadata': ({str: (str,)},), # noqa: E501 'content_type': (str,), # noqa: E501 'force': (bool,), # noqa: E501 + 'if_absent': (bool,), # noqa: E501 } @cached_property @@ -108,6 +109,7 @@ def discriminator(): 'user_metadata': 'user_metadata', # noqa: E501 'content_type': 'content_type', # noqa: E501 'force': 'force', # noqa: E501 + 'if_absent': 'if_absent', # noqa: E501 } read_only_vars = { @@ -159,6 +161,7 @@ def _from_openapi_data(cls, staging, checksum, size_bytes, *args, **kwargs): # user_metadata ({str: (str,)}): [optional] # noqa: E501 content_type (str): Object media type. [optional] # noqa: E501 force (bool): [optional] if omitted the server will use the default value of False # noqa: E501 + if_absent (bool): [optional] if omitted the server will use the default value of False # noqa: E501 """ _check_type = kwargs.pop('_check_type', True) @@ -251,6 +254,7 @@ def __init__(self, staging, checksum, size_bytes, *args, **kwargs): # noqa: E50 user_metadata ({str: (str,)}): [optional] # noqa: E501 content_type (str): Object media type. [optional] # noqa: E501 force (bool): [optional] if omitted the server will use the default value of False # noqa: E501 + if_absent (bool): [optional] if omitted the server will use the default value of False # noqa: E501 """ _check_type = kwargs.pop('_check_type', True) diff --git a/clients/python/docs/StagingMetadata.md b/clients/python/docs/StagingMetadata.md index 7e52333ff9e..ef52a21c1c8 100644 --- a/clients/python/docs/StagingMetadata.md +++ b/clients/python/docs/StagingMetadata.md @@ -12,6 +12,7 @@ Name | Type | Description | Notes **user_metadata** | **Dict[str, str]** | | [optional] **content_type** | **str** | Object media type | [optional] **force** | **bool** | | [optional] [default to False] +**if_absent** | **bool** | | [optional] [default to False] ## Example diff --git a/clients/python/lakefs_sdk/models/staging_metadata.py b/clients/python/lakefs_sdk/models/staging_metadata.py index 6ae206d9d14..38647b3a9d0 100644 --- a/clients/python/lakefs_sdk/models/staging_metadata.py +++ b/clients/python/lakefs_sdk/models/staging_metadata.py @@ -33,7 +33,8 @@ class StagingMetadata(BaseModel): user_metadata: Optional[Dict[str, StrictStr]] = None content_type: Optional[StrictStr] = Field(None, description="Object media type") force: Optional[StrictBool] = False - __properties = ["staging", "checksum", "size_bytes", "user_metadata", "content_type", "force"] + if_absent: Optional[StrictBool] = False + __properties = ["staging", "checksum", "size_bytes", "user_metadata", "content_type", "force", "if_absent"] class Config: """Pydantic configuration""" @@ -79,7 +80,8 @@ def from_dict(cls, obj: dict) -> StagingMetadata: "size_bytes": obj.get("size_bytes"), "user_metadata": obj.get("user_metadata"), "content_type": obj.get("content_type"), - "force": obj.get("force") if obj.get("force") is not None else False + "force": obj.get("force") if obj.get("force") is not None else False, + "if_absent": obj.get("if_absent") if obj.get("if_absent") is not None else False }) return _obj diff --git a/clients/python/test/test_staging_metadata.py b/clients/python/test/test_staging_metadata.py index 3623b7c8884..40624e79aa7 100644 --- a/clients/python/test/test_staging_metadata.py +++ b/clients/python/test/test_staging_metadata.py @@ -49,7 +49,8 @@ def make_instance(self, include_optional): 'key' : '' }, content_type = '', - force = True + force = True, + if_absent = True ) else : return StagingMetadata( diff --git a/docs/assets/js/swagger.yml b/docs/assets/js/swagger.yml index 37db50e468f..6c17051cff6 100644 --- a/docs/assets/js/swagger.yml +++ b/docs/assets/js/swagger.yml @@ -1299,6 +1299,9 @@ components: force: type: boolean default: false + if_absent: + type: boolean + default: false required: - staging - checksum diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 2ba10dc8203..48e9b476329 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -683,8 +683,7 @@ func (c *Controller) LinkPhysicalAddress(w http.ResponseWriter, r *http.Request, entryBuilder.Metadata(body.UserMetadata.AdditionalProperties) } entry := entryBuilder.Build() - - err = c.Catalog.CreateEntry(ctx, repo.Name, branch, entry, graveler.WithForce(swag.BoolValue(body.Force))) + err = c.Catalog.CreateEntry(ctx, repo.Name, branch, entry, graveler.WithForce(swag.BoolValue(body.Force)), graveler.WithIfAbsent(swag.BoolValue(body.IfAbsent))) if c.handleAPIError(ctx, w, r, err) { return } diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index b83dc580b1d..17151cdcda2 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -3007,6 +3007,50 @@ func TestController_LinkPhysicalAddressHandler(t *testing.T) { } }) + t.Run("link physical address twice if absent", func(t *testing.T) { + linkResp, err := clt.GetPhysicalAddressWithResponse(ctx, repo, "main", &apigen.GetPhysicalAddressParams{Path: "foo/bar2"}) + verifyResponseOK(t, linkResp, err) + if linkResp.JSON200 == nil { + t.Fatalf("GetPhysicalAddress non 200 response - status code %d", linkResp.StatusCode()) + } + const expectedSizeBytes = 38 + ifAbsent := false + resp, err := clt.LinkPhysicalAddressWithResponse(ctx, repo, "main", &apigen.LinkPhysicalAddressParams{ + Path: "foo/bar2", + }, apigen.LinkPhysicalAddressJSONRequestBody{ + Checksum: "afb0689fe58b82c5f762991453edbbec", + SizeBytes: expectedSizeBytes, + Staging: apigen.StagingLocation{ + PhysicalAddress: linkResp.JSON200.PhysicalAddress, + }, + IfAbsent: &ifAbsent, + }) + verifyResponseOK(t, resp, err) + + // Try again with ifAbsent == true and expect failure + linkResp, err = clt.GetPhysicalAddressWithResponse(ctx, repo, "main", &apigen.GetPhysicalAddressParams{Path: "foo/bar2"}) + verifyResponseOK(t, linkResp, err) + if linkResp.JSON200 == nil { + t.Fatalf("GetPhysicalAddress non 200 response - status code %d", linkResp.StatusCode()) + } + ifAbsent = true + resp, err = clt.LinkPhysicalAddressWithResponse(ctx, repo, "main", &apigen.LinkPhysicalAddressParams{ + Path: "foo/bar2", + }, apigen.LinkPhysicalAddressJSONRequestBody{ + Checksum: "afb0689fe58b82c5f762991453edbbec", + SizeBytes: expectedSizeBytes, + Staging: apigen.StagingLocation{ + PhysicalAddress: linkResp.JSON200.PhysicalAddress, + }, + IfAbsent: &ifAbsent, + }) + testutil.Must(t, err) + expectedStatusCode := http.StatusPreconditionFailed + if resp.HTTPResponse.StatusCode != expectedStatusCode { + t.Fatalf("LinkPhysicalAddress status code: %d, expected: %d", resp.HTTPResponse.StatusCode, expectedStatusCode) + } + }) + t.Run("link physical address without getting it first", func(t *testing.T) { const expectedSizeBytes = 38 resp, err := clt.LinkPhysicalAddressWithResponse(ctx, repo, "main", &apigen.LinkPhysicalAddressParams{ diff --git a/pkg/graveler/graveler.go b/pkg/graveler/graveler.go index 11047ab3d29..f2285c62319 100644 --- a/pkg/graveler/graveler.go +++ b/pkg/graveler/graveler.go @@ -1711,7 +1711,10 @@ func (g *Graveler) Set(ctx context.Context, repository *RepositoryRecord, branch // verify the key not found _, err := g.Get(ctx, repository, Ref(branchID), key) - if err == nil || !errors.Is(err, ErrNotFound) { + if err == nil { // Entry found, return precondition failed + err = ErrPreconditionFailed + } + if !errors.Is(err, ErrNotFound) { return err }