diff --git a/lib/private/Files/ObjectStore/Azure.php b/lib/private/Files/ObjectStore/Azure.php
index 0b65a6b80e5b2077a83a35f4a3ee1c32e5fb9841..2ef13d60c56fad7533295c1a6c6b1f2fdf2f909a 100644
--- a/lib/private/Files/ObjectStore/Azure.php
+++ b/lib/private/Files/ObjectStore/Azure.php
@@ -130,4 +130,8 @@ class Azure implements IObjectStore {
 			}
 		}
 	}
+
+	public function copyObject($from, $to) {
+		$this->getBlobClient()->copyBlob($this->containerName, $to, $this->containerName, $from);
+	}
 }
diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php
index e675064eb1f74293ef4dc32462aafa4c0ad84d32..e855c166612c267703b1d00fdb1b492e27f1b6ed 100644
--- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php
+++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php
@@ -33,10 +33,15 @@ use Icewind\Streams\CallbackWrapper;
 use Icewind\Streams\CountWrapper;
 use Icewind\Streams\IteratorDirectory;
 use OC\Files\Cache\CacheEntry;
+use OC\Files\Storage\PolyFill\CopyDirectory;
+use OCP\Files\Cache\ICacheEntry;
+use OCP\Files\FileInfo;
 use OCP\Files\NotFoundException;
 use OCP\Files\ObjectStore\IObjectStore;
 
 class ObjectStoreStorage extends \OC\Files\Storage\Common {
+	use CopyDirectory;
+
 	/**
 	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
 	 */
@@ -319,7 +324,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common {
 				} else {
 					return false;
 				}
-				// no break
+			// no break
 			case 'w':
 			case 'wb':
 			case 'w+':
@@ -474,7 +479,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common {
 			if ($size === null) {
 				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
 					$this->getCache()->update($fileId, [
-						'size' => $writtenSize
+						'size' => $writtenSize,
 					]);
 					$size = $writtenSize;
 				});
@@ -523,4 +528,59 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common {
 	public function getObjectStore(): IObjectStore {
 		return $this->objectStore;
 	}
+
+	public function copy($path1, $path2) {
+		$path1 = $this->normalizePath($path1);
+		$path2 = $this->normalizePath($path2);
+
+		$cache = $this->getCache();
+		$sourceEntry = $cache->get($path1);
+		if (!$sourceEntry) {
+			throw new NotFoundException('Source object not found');
+		}
+
+		$this->copyInner($sourceEntry, $path2);
+
+		return true;
+	}
+
+	private function copyInner(ICacheEntry $sourceEntry, string $to) {
+		$cache = $this->getCache();
+
+		if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
+			if ($cache->inCache($to)) {
+				$cache->remove($to);
+			}
+			$this->mkdir($to);
+
+			foreach ($cache->getFolderContentsById($sourceEntry->getId()) as $child) {
+				$this->copyInner($child, $to . '/' . $child->getName());
+			}
+		} else {
+			$this->copyFile($sourceEntry, $to);
+		}
+	}
+
+	private function copyFile(ICacheEntry $sourceEntry, string $to) {
+		$cache = $this->getCache();
+
+		$sourceUrn = $this->getURN($sourceEntry->getId());
+
+		$cache->copyFromCache($cache, $sourceEntry, $to);
+		$targetEntry = $cache->get($to);
+
+		if (!$targetEntry) {
+			throw new \Exception('Target not in cache after copy');
+		}
+
+		$targetUrn = $this->getURN($targetEntry->getId());
+
+		try {
+			$this->objectStore->copyObject($sourceUrn, $targetUrn);
+		} catch (\Exception $e) {
+			$cache->remove($to);
+
+			throw $e;
+		}
+	}
 }
diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php
index a390c6b4c7d329e8d6ad038c618a01f605c9905a..80b8a6f132df56a695ebdaaa4c53140bf3c86f34 100644
--- a/lib/private/Files/ObjectStore/S3ObjectTrait.php
+++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php
@@ -124,4 +124,8 @@ trait S3ObjectTrait {
 	public function objectExists($urn) {
 		return $this->getConnection()->doesObjectExist($this->bucket, $urn);
 	}
+
+	public function copyObject($from, $to) {
+		$this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to);
+	}
 }
diff --git a/lib/private/Files/ObjectStore/StorageObjectStore.php b/lib/private/Files/ObjectStore/StorageObjectStore.php
index a7551385b34d51644928caddf46ce5622fde5786..acf46758956b888ea3b54e6762b54817280e31fe 100644
--- a/lib/private/Files/ObjectStore/StorageObjectStore.php
+++ b/lib/private/Files/ObjectStore/StorageObjectStore.php
@@ -93,4 +93,8 @@ class StorageObjectStore implements IObjectStore {
 	public function objectExists($urn) {
 		return $this->storage->file_exists($urn);
 	}
+
+	public function copyObject($from, $to) {
+		$this->storage->copy($from, $to);
+	}
 }
diff --git a/lib/private/Files/ObjectStore/Swift.php b/lib/private/Files/ObjectStore/Swift.php
index 5ee924c9de665a431963d85eca70e42c121982fd..1b0888b07006f07b67381bfda0fe2ac82cd956f8 100644
--- a/lib/private/Files/ObjectStore/Swift.php
+++ b/lib/private/Files/ObjectStore/Swift.php
@@ -87,13 +87,13 @@ class Swift implements IObjectStore {
 		if (filesize($tmpFile) < SWIFT_SEGMENT_SIZE) {
 			$this->getContainer()->createObject([
 				'name' => $urn,
-				'stream' => stream_for($handle)
+				'stream' => stream_for($handle),
 			]);
 		} else {
 			$this->getContainer()->createLargeObject([
 				'name' => $urn,
 				'stream' => stream_for($handle),
-				'segmentSize' => SWIFT_SEGMENT_SIZE
+				'segmentSize' => SWIFT_SEGMENT_SIZE,
 			]);
 		}
 	}
@@ -114,7 +114,7 @@ class Swift implements IObjectStore {
 					'stream' => true,
 					'headers' => [
 						'X-Auth-Token' => $tokenId,
-						'Cache-Control' => 'no-cache'
+						'Cache-Control' => 'no-cache',
 					],
 				]
 			);
@@ -149,4 +149,10 @@ class Swift implements IObjectStore {
 	public function objectExists($urn) {
 		return $this->getContainer()->objectExists($urn);
 	}
+
+	public function copyObject($from, $to) {
+		$this->getContainer()->getObject($from)->copy([
+			'destination' => $this->getContainer()->name . '/' . $to
+		]);
+	}
 }
diff --git a/lib/public/Files/ObjectStore/IObjectStore.php b/lib/public/Files/ObjectStore/IObjectStore.php
index 4925959d6c501a84dc28cab60438079b9958698f..e9d948682f8e788525db3d37a6e00ef8eb3bbb81 100644
--- a/lib/public/Files/ObjectStore/IObjectStore.php
+++ b/lib/public/Files/ObjectStore/IObjectStore.php
@@ -73,4 +73,12 @@ interface IObjectStore {
 	 * @since 16.0.0
 	 */
 	public function objectExists($urn);
+
+	/**
+	 * @param string $from the unified resource name used to identify the source object
+	 * @param string $to the unified resource name used to identify the target object
+	 * @return void
+	 * @since 21.0.0
+	 */
+	public function copyObject($from, $to);
 }
diff --git a/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php b/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php
index 1a3477090b9a6579d94f70fd78fc34fce64ea023..c755657faffa43212806d234712638b6fb11d328 100644
--- a/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php
+++ b/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php
@@ -51,4 +51,8 @@ class FailDeleteObjectStore implements IObjectStore {
 	public function objectExists($urn) {
 		return $this->objectStore->objectExists($urn);
 	}
+
+	public function copyObject($from, $to) {
+		$this->objectStore->copyObject($from, $to);
+	}
 }
diff --git a/tests/lib/Files/ObjectStore/FailWriteObjectStore.php b/tests/lib/Files/ObjectStore/FailWriteObjectStore.php
index ad2350ea36bb7451603ab354d2bffe2427461a50..b9c8751fda380ea00927d2eb7581ddf4504b617c 100644
--- a/tests/lib/Files/ObjectStore/FailWriteObjectStore.php
+++ b/tests/lib/Files/ObjectStore/FailWriteObjectStore.php
@@ -52,4 +52,8 @@ class FailWriteObjectStore implements IObjectStore {
 	public function objectExists($urn) {
 		return $this->objectStore->objectExists($urn);
 	}
+
+	public function copyObject($from, $to) {
+		$this->objectStore->copyObject($from, $to);
+	}
 }
diff --git a/tests/lib/Files/ObjectStore/ObjectStoreTest.php b/tests/lib/Files/ObjectStore/ObjectStoreTest.php
index 9300a9bdef6c16335160485aded3ae98b2e408f7..4ec44eb410de288f07604a4e82bcfee08a39a98d 100644
--- a/tests/lib/Files/ObjectStore/ObjectStoreTest.php
+++ b/tests/lib/Files/ObjectStore/ObjectStoreTest.php
@@ -108,4 +108,20 @@ abstract class ObjectStoreTest extends TestCase {
 
 		$this->assertFalse($instance->objectExists('2'));
 	}
+
+	public function testCopy() {
+		$stream = $this->stringToStream('foobar');
+
+		$instance = $this->getInstance();
+
+		$instance->writeObject('source', $stream);
+
+		$this->assertFalse($instance->objectExists('target'));
+
+		$instance->copyObject('source', 'target');
+
+		$this->assertTrue($instance->objectExists('target'));
+
+		$this->assertEquals('foobar', stream_get_contents($instance->readObject('target')));
+	}
 }