diff --git a/.github/services/webdav/0_nginx/action.yml b/.github/services/webdav/0_nginx/action.yml index b3733f330431..d7f46dda6cc8 100644 --- a/.github/services/webdav/0_nginx/action.yml +++ b/.github/services/webdav/0_nginx/action.yml @@ -30,5 +30,5 @@ runs: run: | cat << EOF >> $GITHUB_ENV OPENDAL_WEBDAV_ENDPOINT=http://127.0.0.1:8080 - OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,read_with_if_match=false,read_with_if_none_match=false,read_with_if_modified_since=false,read_with_if_unmodified_since=false + OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,read_with_if_match=false,read_with_if_none_match=false,read_with_if_modified_since=false,read_with_if_unmodified_since=false,delete_with_if_match=false EOF diff --git a/.github/services/webdav/nextcloud/action.yml b/.github/services/webdav/nextcloud/action.yml index 358a1a6328bd..2b0756f9aea3 100644 --- a/.github/services/webdav/nextcloud/action.yml +++ b/.github/services/webdav/nextcloud/action.yml @@ -25,6 +25,7 @@ runs: shell: bash working-directory: fixtures/webdav run: docker compose -f docker-compose-webdav-nextcloud.yml up -d --wait + # ETags have one-second resolution: https://github.com/nextcloud/server/issues/65078 - name: Setup shell: bash run: | @@ -32,5 +33,5 @@ runs: OPENDAL_WEBDAV_ENDPOINT=http://127.0.0.1:8080/remote.php/webdav/ OPENDAL_WEBDAV_USERNAME=admin OPENDAL_WEBDAV_PASSWORD=admin - OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false + OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,delete_with_if_match=false EOF diff --git a/.github/services/webdav/nginx_with_empty_password/action.yml b/.github/services/webdav/nginx_with_empty_password/action.yml index 7b9e6bf6a20e..9d2a1cafd59e 100644 --- a/.github/services/webdav/nginx_with_empty_password/action.yml +++ b/.github/services/webdav/nginx_with_empty_password/action.yml @@ -31,5 +31,5 @@ runs: cat << EOF >> $GITHUB_ENV OPENDAL_WEBDAV_ENDPOINT=http://127.0.0.1:8080 OPENDAL_WEBDAV_USERNAME=foo - OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,read_with_if_match=false,read_with_if_none_match=false,read_with_if_modified_since=false,read_with_if_unmodified_since=false + OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,read_with_if_match=false,read_with_if_none_match=false,read_with_if_modified_since=false,read_with_if_unmodified_since=false,delete_with_if_match=false EOF diff --git a/.github/services/webdav/nginx_with_password/action.yml b/.github/services/webdav/nginx_with_password/action.yml index 7ee8d9d8a882..8495e52fc38d 100644 --- a/.github/services/webdav/nginx_with_password/action.yml +++ b/.github/services/webdav/nginx_with_password/action.yml @@ -32,5 +32,5 @@ runs: OPENDAL_WEBDAV_ENDPOINT=http://127.0.0.1:8080 OPENDAL_WEBDAV_USERNAME=bar OPENDAL_WEBDAV_PASSWORD=bar - OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,read_with_if_match=false,read_with_if_none_match=false,read_with_if_modified_since=false,read_with_if_unmodified_since=false + OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,read_with_if_match=false,read_with_if_none_match=false,read_with_if_modified_since=false,read_with_if_unmodified_since=false,delete_with_if_match=false EOF diff --git a/.github/services/webdav/nginx_with_redirect/action.yml b/.github/services/webdav/nginx_with_redirect/action.yml index abd2606a8b31..1096b0bf915f 100644 --- a/.github/services/webdav/nginx_with_redirect/action.yml +++ b/.github/services/webdav/nginx_with_redirect/action.yml @@ -30,5 +30,5 @@ runs: run: | cat << EOF >> $GITHUB_ENV OPENDAL_WEBDAV_ENDPOINT=http://127.0.0.1:8081 - OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,read_with_if_match=false,read_with_if_none_match=false,read_with_if_modified_since=false,read_with_if_unmodified_since=false + OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,read_with_if_match=false,read_with_if_none_match=false,read_with_if_modified_since=false,read_with_if_unmodified_since=false,delete_with_if_match=false EOF diff --git a/.github/services/webdav/owncloud/action.yml b/.github/services/webdav/owncloud/action.yml index ed4861a2f8f5..63554f8760a0 100644 --- a/.github/services/webdav/owncloud/action.yml +++ b/.github/services/webdav/owncloud/action.yml @@ -32,5 +32,5 @@ runs: OPENDAL_WEBDAV_ENDPOINT=http://127.0.0.1:8080/remote.php/webdav/ OPENDAL_WEBDAV_USERNAME=admin OPENDAL_WEBDAV_PASSWORD=admin - OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false + OPENDAL_TEST_CAPABILITY_OVERRIDES=write_with_user_metadata=false,delete_with_if_match=false EOF diff --git a/core/services/webdav/src/backend.rs b/core/services/webdav/src/backend.rs index 0868a991bca2..fadecfe2e826 100644 --- a/core/services/webdav/src/backend.rs +++ b/core/services/webdav/src/backend.rs @@ -220,6 +220,7 @@ impl Builder for WebdavBuilder { create_dir: true, delete: true, + delete_with_if_match: true, copy: true, diff --git a/core/services/webdav/src/core.rs b/core/services/webdav/src/core.rs index 6bb5c672340c..60a3261b29fc 100644 --- a/core/services/webdav/src/core.rs +++ b/core/services/webdav/src/core.rs @@ -302,6 +302,7 @@ impl WebdavCore { &self, ctx: &OperationContext, path: &str, + args: &OpDelete, ) -> Result> { let path = build_rooted_abs_path(&self.root, path); let url = format!("{}{}", self.endpoint, percent_encode_path(&path)); @@ -312,6 +313,10 @@ impl WebdavCore { req = req.header(header::AUTHORIZATION, auth.clone()) } + if let Some(if_match) = args.if_match() { + req = req.header(header::IF_MATCH, if_match); + } + let req = req .extension(Operation::Delete) .extension(ServiceOperation("Delete")) diff --git a/core/services/webdav/src/deleter.rs b/core/services/webdav/src/deleter.rs index 67e24715558c..9940d989add7 100644 --- a/core/services/webdav/src/deleter.rs +++ b/core/services/webdav/src/deleter.rs @@ -37,12 +37,20 @@ impl WebdavDeleter { } impl oio::OneShotDelete for WebdavDeleter { - async fn delete_once(&self, path: String, _: OpDelete) -> Result<()> { - let resp = self.core.webdav_delete(&self.ctx, &path).await?; + async fn delete_once(&self, path: String, args: OpDelete) -> Result<()> { + let resp = self.core.webdav_delete(&self.ctx, &path, &args).await?; let status = resp.status(); match status { - StatusCode::NO_CONTENT | StatusCode::NOT_FOUND => Ok(()), + StatusCode::NO_CONTENT => Ok(()), + StatusCode::NOT_FOUND if args.if_match().is_some() => Err(Error::new( + ErrorKind::ConditionNotMatch, + "delete precondition requires a live target", + )), + StatusCode::NOT_FOUND => Ok(()), + // No `with_if_match` here: it maps SabreDAV's 412-on-missing to + // NotFound, but delete reports an unmet precondition as + // ConditionNotMatch. _ => Err(parse_error( ErrorContext::new(ServiceOperation("Delete")), resp, diff --git a/core/services/webdav/src/docs.md b/core/services/webdav/src/docs.md index 7c907e6a4eca..896056d26d26 100644 --- a/core/services/webdav/src/docs.md +++ b/core/services/webdav/src/docs.md @@ -20,6 +20,12 @@ Inspect the effective capability set with [`opendal_core::Operator::info`] and Bazel Remote Caching and Ccache HTTP Storage is also part of this service. Users can use `webdav` to connect those services. +`delete_with_if_match` is only as safe as the server's ETags. SabreDAV-based +servers such as Nextcloud and ownCloud derive ETags from one-second +modification times, so a stale ETag still matches after an overwrite within +the same second and the delete removes content the caller never observed. +Disable `delete_with_if_match` with `CapabilityOverrideLayer` for such servers. + ## Configuration Use [`crate::WebdavConfig`] for serializable configuration and this builder's