diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
index dff71cbfaaf70..ec84486b55653 100644
--- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php
+++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php
@@ -599,6 +599,12 @@ function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $fi
throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node');
}
+ if ($value === null) {
+ $metadata->unset($metadataKey);
+ $filesMetadataManager->saveMetadata($metadata);
+ return true;
+ }
+
// If the metadata is unknown, it defaults to string.
try {
$type = $knownMetadata->getType($metadataKey);
diff --git a/build/integration/config/behat.yml b/build/integration/config/behat.yml
index 47f8f3c827c43..eec8b5b77fcad 100644
--- a/build/integration/config/behat.yml
+++ b/build/integration/config/behat.yml
@@ -113,6 +113,12 @@ default:
- CommandLineContext:
baseUrl: http://localhost:8080
ocPath: ../../
+ - MetadataContext:
+ baseUrl: http://localhost:8080
+ admin:
+ - admin
+ - admin
+ regular_user_password: 123456
files_conversion:
paths:
- "%paths.base%/../file_conversions"
diff --git a/build/integration/features/bootstrap/MetadataContext.php b/build/integration/features/bootstrap/MetadataContext.php
new file mode 100644
index 0000000000000..893c08a5467e9
--- /dev/null
+++ b/build/integration/features/bootstrap/MetadataContext.php
@@ -0,0 +1,123 @@
+baseUrl = substr($testServerUrl, 0, -5);
+ }
+ }
+
+ #[When('User :user sets the :metadataKey prop with value :metadataValue on :fileName')]
+ public function userSetsProp(string $user, string $metadataKey, string $metadataValue, string $fileName) {
+ $client = new SClient([
+ 'baseUri' => $this->baseUrl,
+ 'userName' => $user,
+ 'password' => '123456',
+ 'authType' => SClient::AUTH_BASIC,
+ ]);
+
+ $body = '
+
+
+
+ ' . $metadataValue . '
+
+
+';
+
+ $davUrl = $this->getDavUrl($user, $fileName);
+ $client->request('PROPPATCH', $this->baseUrl . $davUrl, $body);
+ }
+
+ #[When('User :user deletes the :metadataKey prop on :fileName')]
+ public function userDeletesProp(string $user, string $metadataKey, string $fileName) {
+ $client = new SClient([
+ 'baseUri' => $this->baseUrl,
+ 'userName' => $user,
+ 'password' => '123456',
+ 'authType' => SClient::AUTH_BASIC,
+ ]);
+
+ $body = '
+
+
+
+
+
+
+';
+
+ $davUrl = $this->getDavUrl($user, $fileName);
+ $client->request('PROPPATCH', $this->baseUrl . $davUrl, $body);
+ }
+
+ #[Then('User :user should see the prop :metadataKey equal to :metadataValue for file :fileName')]
+ public function checkPropForFile(string $user, string $metadataKey, string $metadataValue, string $fileName) {
+ $client = new SClient([
+ 'baseUri' => $this->baseUrl,
+ 'userName' => $user,
+ 'password' => '123456',
+ 'authType' => SClient::AUTH_BASIC,
+ ]);
+
+ $body = '
+
+
+
+
+';
+
+ $davUrl = $this->getDavUrl($user, $fileName);
+ $response = $client->request('PROPFIND', $this->baseUrl . $davUrl, $body);
+ $parsedResponse = $client->parseMultistatus($response['body']);
+
+ Assert::assertEquals($parsedResponse[$davUrl]['200']['{http://nextcloud.com/ns}' . $metadataKey], $metadataValue);
+ }
+
+ #[Then('User :user should not see the prop :metadataKey for file :fileName')]
+ public function checkPropDoesNotExistsForFile(string $user, string $metadataKey, string $fileName) {
+ $client = new SClient([
+ 'baseUri' => $this->baseUrl,
+ 'userName' => $user,
+ 'password' => '123456',
+ 'authType' => SClient::AUTH_BASIC,
+ ]);
+
+ $body = '
+
+
+
+
+';
+
+ $davUrl = $this->getDavUrl($user, $fileName);
+ $response = $client->request('PROPFIND', $this->baseUrl . $davUrl, $body);
+ $parsedResponse = $client->parseMultistatus($response['body']);
+
+ Assert::assertEquals($parsedResponse[$davUrl]['404']['{http://nextcloud.com/ns}' . $metadataKey], null);
+ }
+
+ private function getDavUrl(string $user, string $fileName) {
+ return $this->davPath . '/files/' . $user . $fileName;
+ }
+}
diff --git a/build/integration/files_features/metadata.feature b/build/integration/files_features/metadata.feature
new file mode 100644
index 0000000000000..553a7b623061f
--- /dev/null
+++ b/build/integration/files_features/metadata.feature
@@ -0,0 +1,16 @@
+# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-only
+Feature: metadata
+
+ Scenario: Setting metadata works
+ Given user "user0" exists
+ When User "user0" uploads file with content "AAA" to "/test.txt"
+ And User "user0" sets the "metadata-files-live-photo" prop with value "metadata-value" on "/test.txt"
+ Then User "user0" should see the prop "metadata-files-live-photo" equal to "metadata-value" for file "/test.txt"
+
+ Scenario: Deleting metadata works
+ Given user "user0" exists
+ When User "user0" uploads file with content "AAA" to "/test.txt"
+ And User "user0" sets the "metadata-files-live-photo" prop with value "metadata-value" on "/test.txt"
+ And User "user0" deletes the "metadata-files-live-photo" prop on "/test.txt"
+ Then User "user0" should not see the prop "metadata-files-live-photo" for file "/test.txt"